/** * RateLimiter limits connection attempts using a sliding window. */ export class RateLimiter { /** * @param {number} limit Maximum connections allowed * @param {number} windowMs Sliding window length in milliseconds */ constructor(limit, windowMs) { this.limit = limit; this.windowMs = windowMs; this.attempts = new Map(); // Start background cleanup worker every 5 minutes this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1000); if (this.cleanupInterval.unref) { this.cleanupInterval.unref(); } } /** * Checks if an attempt is allowed under rate limits for the given IP. * @param {string} ip * @returns {boolean} True if connection attempt is within rate limits */ allow(ip) { const now = Date.now(); const cutoff = now - this.windowMs; let timestamps = this.attempts.get(ip) || []; // Filter out historical connection attempts outside the current window timestamps = timestamps.filter(t => t > cutoff); if (timestamps.length >= this.limit) { this.attempts.set(ip, timestamps); return false; } timestamps.push(now); this.attempts.set(ip, timestamps); return true; } /** * Sweeps inactive keys to reclaim memory. */ cleanup() { const cutoff = Date.now() - this.windowMs; for (const [ip, timestamps] of this.attempts.entries()) { const active = timestamps.filter(t => t > cutoff); if (active.length === 0) { this.attempts.delete(ip); } else { this.attempts.set(ip, active); } } } }