/**
* Smart Rate Limiter with Sliding Window Algorithm
*
* Features:
* - Sliding window for accurate rate limiting
* - Memory-efficient with automatic cleanup
* - Configurable per-route limits
* - TypeScript support
*/
interface RateLimitConfig {
windowMs: number; // Time window in milliseconds
maxRequests: number; // Max requests per window
message?: string; // Error message
}
interface RequestRecord {
count: number;
timestamps: number[];
}
class SlidingWindowRateLimiter {
private requests: Map<string, RequestRecord> = new Map();
private config: RateLimitConfig;
private cleanupInterval: NodeJS.Timeout;
constructor(config: RateLimitConfig) {
this.config = {
message: "Too many requests, please try again later.",
...config
};
// Auto-cleanup every minute
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
}
/**
* Check if request is allowed
* @returns { allowed: boolean, remaining: number, resetIn: number }
*/
check(identifier: string): { allowed: boolean; remaining: number; resetIn: number } {
const now = Date.now();
const windowStart = now - this.config.windowMs;
let record = this.requests.get(identifier);
if (!record) {
record = { count: 0, timestamps: [] };
this.requests.set(identifier, record);
}
// Remove timestamps outside the window
record.timestamps = record.timestamps.filter(ts => ts > windowStart);
record.count = record.timestamps.length;
const remaining = Math.max(0, this.config.maxRequests - record.count);
const oldestTimestamp = record.timestamps[0] || now;
const resetIn = Math.max(0, oldestTimestamp + this.config.windowMs - now);
if (record.count >= this.config.maxRequests) {
return { allowed: false, remaining: 0, resetIn };
}
// Allow request and record timestamp
record.timestamps.push(now);
record.count++;
return {
allowed: true,
remaining: remaining - 1,
resetIn: this.config.windowMs
};
}
/**
* Express middleware factory
*/
middleware() {
return (req: any, res: any, next: any) => {
const identifier = req.ip || req.connection.remoteAddress || "unknown";
const result = this.check(identifier);
// Set rate limit headers
res.setHeader("X-RateLimit-Limit", this.config.maxRequests);
res.setHeader("X-RateLimit-Remaining", result.remaining);
res.setHeader("X-RateLimit-Reset", Math.ceil(result.resetIn / 1000));
if (!result.allowed) {
res.status(429).json({
error: this.config.message,
retryAfter: Math.ceil(result.resetIn / 1000)
});
return;
}
next();
};
}
/**
* Cleanup expired entries
*/
private cleanup(): void {
const now = Date.now();
const windowStart = now - this.config.windowMs;
for (const [key, record] of this.requests.entries()) {
record.timestamps = record.timestamps.filter(ts => ts > windowStart);
if (record.timestamps.length === 0) {
this.requests.delete(key);
}
}
}
/**
* Stop the cleanup interval
*/
destroy(): void {
clearInterval(this.cleanupInterval);
this.requests.clear();
}
}
// Usage Example:
const apiLimiter = new SlidingWindowRateLimiter({
windowMs: 60 * 1000, // 1 minute
maxRequests: 100, // 100 requests per minute
message: "Rate limit exceeded. Please wait before making more requests."
});
// Express usage:
// app.use("/api", apiLimiter.middleware());
// Manual check:
// const result = apiLimiter.check("user-123");
// console.log(result); // { allowed: true, remaining: 99, resetIn: 60000 }
export { SlidingWindowRateLimiter, RateLimitConfig };