/** * Limits how often something may happen, with token buckets kept in memory, * which a restart empties. Logins, password reset requests and the chat's * typing messages use it. * * @module */ interface Bucket { tokens: number; at: number; } const buckets = new Map(); /** * Passed to {@link takeToken} by each limited action, which declares its own * as a constant. */ export interface RateLimit { /** How many tokens a full bucket holds, and so the largest burst. */ capacity: number; /** Tokens regained per second. */ refill: number; } /** * Takes one token from `key`'s bucket, or returns false when it is empty. The * caller picks the key, such as `typing:`, and keys by user or account * so that more tabs or parallel requests don't buy more tokens. */ export function takeToken(key: string, limit: RateLimit): boolean { const now = Date.now(); const bucket = buckets.get(key) ?? { tokens: limit.capacity, at: now }; bucket.tokens = Math.min( limit.capacity, bucket.tokens + ((now - bucket.at) / 1000) * limit.refill, ); bucket.at = now; buckets.set(key, bucket); if (bucket.tokens < 1) return false; bucket.tokens -= 1; return true; } // Every limit in use refills within the hour, so an older bucket is full and // dropping it changes nothing. setInterval(() => { const cutoff = Date.now() - 60 * 60_000; for (const [key, bucket] of buckets) { if (bucket.at < cutoff) buckets.delete(key); } }, 60_000);