/** * 内存 store:实现 `RateLimitStore`,单机 / 单测用。 * * 用 `Map` 持桶状态,所有桶数学复用 `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} */ 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 } }, } }