60 lines
1.6 KiB
JavaScript
60 lines
1.6 KiB
JavaScript
/**
|
|
* 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);
|
|
}
|
|
}
|
|
}
|
|
}
|