memory-store.js 2.08 KB
/**
 * 内存 store:实现 `RateLimitStore`,单机 / 单测用。
 *
 * 用 `Map<key, BucketState>` 持桶状态,所有桶数学复用 `token-bucket.js`——保证与
 * 未来 redis 实现行为一致。JS 单线程下「读-补-扣」在一次同步函数调用内完成、其间
 * 不交还事件循环,因此天然原子:并发的 take 顺序执行、不会放行超过 capacity。
 * redis 实现需用 Lua/单命令复刻这一原子性。
 *
 * @typedef {import('./rules.js').RateRule} RateRule
 * @typedef {import('./token-bucket.js').BucketState} BucketState
 *
 * RateLimitStore 接口(§3,本包对其唯一要求):
 * {
 *   take(key, cost, rule, nowMs): Promise<{ allowed, remaining, retryAfterMs }>,
 *   peek(key, rule, nowMs):       Promise<{ remaining, retryAfterMs }>
 * }
 */

import * as bucket from './token-bucket.js'

/**
 * 创建一个内存 store。
 * @returns {{
 *   take(key: string, cost: number, rule: RateRule, nowMs: number): Promise<{ allowed: boolean, remaining: number, retryAfterMs: number }>,
 *   peek(key: string, rule: RateRule, nowMs: number): Promise<{ remaining: number, retryAfterMs: number }>
 * }}
 */
export function createMemoryStore() {
  /** @type {Map<string, BucketState>} */
  const buckets = new Map()

  /**
   * @param {string} key
   * @param {RateRule} rule
   * @param {number} nowMs
   * @returns {BucketState}
   */
  function stateOf(key, rule, nowMs) {
    return buckets.get(key) ?? bucket.initialState(rule, nowMs)
  }

  return {
    async take(key, cost, rule, nowMs) {
      const state = stateOf(key, rule, nowMs)
      const r = bucket.take(state, cost, rule, nowMs)
      buckets.set(key, r.next)
      return { allowed: r.allowed, remaining: r.remaining, retryAfterMs: r.retryAfterMs }
    },

    async peek(key, rule, nowMs) {
      const state = stateOf(key, rule, nowMs)
      // 只读:不写回,不消耗令牌。cost 固定为 1(判定「下一次单位调用是否会放行」)。
      const r = bucket.peek(state, 1, rule, nowMs)
      return { remaining: r.remaining, retryAfterMs: r.retryAfterMs }
    },
  }
}