profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
rate-limit.ts 1,417 bytes
1 /**
2 * In-memory sliding-window rate limiter for the debate-start endpoint.
3 *
4 * This protects the *server* (each debate spawns many concurrent model calls
5 * and DB writes); inference itself is paid by each user's own key. A process-
6 * local map is sufficient for a single-VPS deployment; swap for Redis if the
7 * app is ever horizontally scaled.
8 */
9 const hits = new Map<string, number[]>();
10
11 export interface RateLimitResult {
12 allowed: boolean;
13 remaining: number;
14 limit: number;
15 resetMs: number;
16 }
17
18 export function rateLimit(key: string, limit: number, windowMs: number): RateLimitResult {
19 const now = Date.now();
20 const windowStart = now - windowMs;
21 const timestamps = (hits.get(key) ?? []).filter((t) => t > windowStart);
22
23 if (timestamps.length >= limit) {
24 const oldest = timestamps[0]!;
25 hits.set(key, timestamps);
26 return { allowed: false, remaining: 0, limit, resetMs: oldest + windowMs - now };
27 }
28
29 timestamps.push(now);
30 hits.set(key, timestamps);
31 return { allowed: true, remaining: limit - timestamps.length, limit, resetMs: windowMs };
32 }
33
34 /** Periodically drop empty buckets so the map can't grow unbounded. */
35 export function pruneRateLimiter(windowMs: number): void {
36 const cutoff = Date.now() - windowMs;
37 for (const [key, ts] of hits) {
38 const kept = ts.filter((t) => t > cutoff);
39 if (kept.length === 0) hits.delete(key);
40 else hits.set(key, kept);
41 }
42 }
43