Ratelimit
This commit is contained in:
71
src/lib/rate-limit.ts
Normal file
71
src/lib/rate-limit.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* In-memory sliding-window rate limiter.
|
||||
*
|
||||
* Suitable for single-node deployments (pilot scale).
|
||||
* For multi-replica, replace with Redis-backed store.
|
||||
*/
|
||||
|
||||
interface RateLimitEntry {
|
||||
timestamps: number[];
|
||||
}
|
||||
|
||||
const store = new Map<string, RateLimitEntry>();
|
||||
|
||||
// Cleanup stale entries every 10 minutes
|
||||
if (typeof globalThis !== "undefined") {
|
||||
// Use globalThis to survive HMR in dev — only one interval
|
||||
const key = "__rateLimitCleanup";
|
||||
if (!(globalThis as any)[key]) {
|
||||
(globalThis as any)[key] = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [k, entry] of store) {
|
||||
entry.timestamps = entry.timestamps.filter((t) => now - t < 3_600_000);
|
||||
if (entry.timestamps.length === 0) store.delete(k);
|
||||
}
|
||||
}, 600_000);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RateLimitResult {
|
||||
allowed: boolean;
|
||||
remaining: number;
|
||||
/** Milliseconds until the oldest request in the window expires */
|
||||
resetMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and record a rate-limited action.
|
||||
*
|
||||
* @param key - Unique key, e.g. `register:${ip}`
|
||||
* @param limit - Max allowed actions in the window
|
||||
* @param windowMs - Window size in milliseconds
|
||||
*/
|
||||
export function rateLimit(
|
||||
key: string,
|
||||
limit: number,
|
||||
windowMs: number,
|
||||
): RateLimitResult {
|
||||
const now = Date.now();
|
||||
const entry = store.get(key) ?? { timestamps: [] };
|
||||
|
||||
// Prune expired timestamps
|
||||
entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
|
||||
|
||||
if (entry.timestamps.length >= limit) {
|
||||
const oldest = entry.timestamps[0];
|
||||
return {
|
||||
allowed: false,
|
||||
remaining: 0,
|
||||
resetMs: oldest + windowMs - now,
|
||||
};
|
||||
}
|
||||
|
||||
entry.timestamps.push(now);
|
||||
store.set(key, entry);
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: limit - entry.timestamps.length,
|
||||
resetMs: entry.timestamps[0] + windowMs - now,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user