2026-05-18 07:27:06 +07:00
|
|
|
import logger from './logger';
|
2026-05-18 07:01:32 +07:00
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
interface RateLimitRecord {
|
|
|
|
|
count: number;
|
|
|
|
|
reset: number;
|
|
|
|
|
}
|
2026-05-18 07:01:32 +07:00
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
const rateLimitMap = new Map<string, RateLimitRecord>();
|
|
|
|
|
|
|
|
|
|
export const checkRateLimit = (key: string): boolean => {
|
2026-05-18 07:01:32 +07:00
|
|
|
const now = Date.now();
|
2026-05-18 07:27:06 +07:00
|
|
|
const windowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS!, 10) || 60000;
|
|
|
|
|
const maxRequests = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS!, 10) || 30;
|
2026-05-18 07:01:32 +07:00
|
|
|
|
|
|
|
|
if (!rateLimitMap.has(key)) {
|
|
|
|
|
rateLimitMap.set(key, { count: 0, reset: now + windowMs });
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
const record = rateLimitMap.get(key)!;
|
2026-05-18 07:01:32 +07:00
|
|
|
|
|
|
|
|
if (now > record.reset) {
|
|
|
|
|
record.count = 0;
|
|
|
|
|
record.reset = now + windowMs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (record.count >= maxRequests) {
|
|
|
|
|
logger.warn('Rate limit exceeded', { key, count: record.count, reset: record.reset });
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
record.count++;
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
export const cleanupRateLimitCache = (): void => {
|
2026-05-18 07:01:32 +07:00
|
|
|
const now = Date.now();
|
2026-05-18 07:27:06 +07:00
|
|
|
const keysToDelete: string[] = [];
|
2026-05-18 07:01:32 +07:00
|
|
|
|
|
|
|
|
for (const [key, record] of rateLimitMap.entries()) {
|
|
|
|
|
if (now > record.reset) {
|
|
|
|
|
keysToDelete.push(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
for (const key of keysToDelete) {
|
|
|
|
|
rateLimitMap.delete(key);
|
|
|
|
|
}
|
2026-05-18 07:01:32 +07:00
|
|
|
};
|