/**
* In-memory sliding-window rate limiter.
* Resets on cold start — acceptable for initial implementation.
*/
interface RateLimitEntry {
timestamps: number[];
}
const store = new Map<string, RateLimitEntry>();
// Clean up stale entries every 5 minutes
const CLEANUP_INTERVAL = 5 * 60 * 1000;
let lastCleanup = Date.now();
function cleanup(windowMs: number) {
const now = Date.now();
if (now - lastCleanup < CLEANUP_INTERVAL) return;
lastCleanup = now;
const cutoff = now - windowMs;
for (const [key, entry] of store) {
entry.timestamps = entry.timestamps.filter((t) => t > cutoff);
if (entry.timestamps.length === 0) {
store.delete(key);
}
}
}
export interface RateLimitResult {
allowed: boolean;
remaining: number;
retryAfterSeconds: number;
}
export function checkRateLimit(
key: string,
maxRequests: number,
windowMs: number
): RateLimitResult {
const now = Date.now();
cleanup(windowMs);
const entry = store.get(key) ?? { timestamps: [] };
const cutoff = now - windowMs;
// Remove expired timestamps
entry.timestamps = entry.timestamps.filter((t) => t > cutoff);
if (entry.timestamps.length >= maxRequests) {
const oldestInWindow = entry.timestamps[0];
const retryAfterMs = oldestInWindow + windowMs - now;
return {
allowed: false,
remaining: 0,
retryAfterSeconds: Math.ceil(retryAfterMs / 1000),
};
}
entry.timestamps.push(now);
store.set(key, entry);
return {
allowed: true,
remaining: maxRequests - entry.timestamps.length,
retryAfterSeconds: 0,
};
}