/** Above this many tracked keys, prune stale entries on the next `allow` before inserting. */ const MAX_TRACKED_KEYS = 10_000; /** Sliding-window limiter: at most `max` allowed calls per key within `windowMs`. In-memory only. */ export class RateLimiter { private hits = new Map(); constructor( private readonly max: number, private readonly windowMs: number, private readonly now: () => number = Date.now ) {} /** Number of keys currently tracked. Exposed for tests. */ get size(): number { return this.hits.size; } allow(key: string): boolean { const t = this.now(); const recent = (this.hits.get(key) ?? []).filter((h) => t - h < this.windowMs); if (recent.length >= this.max) { if (recent.length === 0) this.hits.delete(key); else this.hits.set(key, recent); return false; } if (this.hits.size > MAX_TRACKED_KEYS) { for (const [k, hs] of this.hits) { const mostRecent = hs.length > 0 ? Math.max(...hs) : -Infinity; if (t - mostRecent >= this.windowMs) this.hits.delete(k); } } recent.push(t); this.hits.set(key, recent); return true; } reset(key: string): void { this.hits.delete(key); } }