File size: 1,179 Bytes
1c7b998
 
 
ef84ace
 
 
 
 
 
 
 
 
 
1c7b998
 
 
 
 
ef84ace
 
 
 
1c7b998
 
ef84ace
 
1c7b998
 
 
 
 
 
ef84ace
 
 
 
b11153a
 
 
 
ef84ace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** 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<string, number[]>();

	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);
	}
}