60 lines
1.4 KiB
TypeScript
60 lines
1.4 KiB
TypeScript
|
|
/**
|
||
|
|
* Simple in-memory rate limiter using fixed window approach.
|
||
|
|
* Tracks request counts per key (typically IP) within time windows.
|
||
|
|
*
|
||
|
|
* For production with multiple instances, replace with Redis-based solution.
|
||
|
|
*/
|
||
|
|
|
||
|
|
type RateLimitEntry = {
|
||
|
|
count: number
|
||
|
|
resetAt: number
|
||
|
|
}
|
||
|
|
|
||
|
|
const store = new Map<string, RateLimitEntry>()
|
||
|
|
|
||
|
|
export type RateLimitResult = {
|
||
|
|
success: boolean
|
||
|
|
remaining: number
|
||
|
|
resetAt: number
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check rate limit for a given key.
|
||
|
|
* @param key - Identifier (e.g., IP address)
|
||
|
|
* @param limit - Max requests per window
|
||
|
|
* @param windowMs - Window size in milliseconds
|
||
|
|
*/
|
||
|
|
export function checkRateLimit(
|
||
|
|
key: string,
|
||
|
|
limit: number,
|
||
|
|
windowMs: number
|
||
|
|
): RateLimitResult {
|
||
|
|
const now = Date.now()
|
||
|
|
const entry = store.get(key)
|
||
|
|
|
||
|
|
if (!entry || now > entry.resetAt) {
|
||
|
|
const resetAt = now + windowMs
|
||
|
|
store.set(key, { count: 1, resetAt })
|
||
|
|
return { success: true, remaining: limit - 1, resetAt }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (entry.count >= limit) {
|
||
|
|
return { success: false, remaining: 0, resetAt: entry.resetAt }
|
||
|
|
}
|
||
|
|
|
||
|
|
entry.count++
|
||
|
|
return { success: true, remaining: limit - entry.count, resetAt: entry.resetAt }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clean up stale entries every 5 minutes to prevent memory leaks
|
||
|
|
if (typeof setInterval !== 'undefined') {
|
||
|
|
setInterval(() => {
|
||
|
|
const now = Date.now()
|
||
|
|
for (const [key, entry] of store) {
|
||
|
|
if (now > entry.resetAt) {
|
||
|
|
store.delete(key)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, 5 * 60 * 1000).unref?.()
|
||
|
|
}
|