You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
/**
|
|
* RateLimiter - Token Bucket en memoria por usuario.
|
|
* - Límite global por remitente (ID normalizado).
|
|
* - Capacidad y tasa configurables por entorno:
|
|
* - RATE_LIMIT_PER_MIN: tokens que se recargan por minuto (default 15)
|
|
* - RATE_LIMIT_BURST: capacidad del bucket (default = RATE_LIMIT_PER_MIN)
|
|
* - Desactivación en tests: la integración en server evita usar el limiter con NODE_ENV='test'.
|
|
*/
|
|
export class RateLimiter {
|
|
private static readonly buckets = new Map<string, { tokens: number; last: number }>();
|
|
private static readonly lastNotifyAt = new Map<string, number>();
|
|
private static readonly NOTIFY_COOLDOWN_MS = 60_000; // 1 minuto
|
|
|
|
private static get ratePerMin(): number {
|
|
const v = Number(process.env.RATE_LIMIT_PER_MIN);
|
|
return Number.isFinite(v) && v > 0 ? v : 15;
|
|
}
|
|
|
|
private static get capacity(): number {
|
|
const burst = Number(process.env.RATE_LIMIT_BURST);
|
|
if (Number.isFinite(burst) && burst > 0) return burst;
|
|
return this.ratePerMin; // por defecto, burst = rate
|
|
}
|
|
|
|
private static get refillPerMs(): number {
|
|
return this.ratePerMin / 60_000; // tokens por ms
|
|
}
|
|
|
|
static checkAndConsume(userId: string, now: number = Date.now()): boolean {
|
|
if (!userId) return true; // por seguridad, no bloquear IDs vacíos
|
|
|
|
let bucket = this.buckets.get(userId);
|
|
if (!bucket) {
|
|
bucket = { tokens: this.capacity, last: now };
|
|
this.buckets.set(userId, bucket);
|
|
}
|
|
|
|
// Refill lineal en función del tiempo transcurrido
|
|
const elapsed = Math.max(0, now - bucket.last);
|
|
if (elapsed > 0) {
|
|
bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsed * this.refillPerMs);
|
|
bucket.last = now;
|
|
}
|
|
|
|
if (bucket.tokens >= 1) {
|
|
bucket.tokens -= 1;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static shouldNotify(userId: string, now: number = Date.now()): boolean {
|
|
const last = this.lastNotifyAt.get(userId);
|
|
// Si nunca se ha notificado, permitir notificar inmediatamente
|
|
if (last === undefined) {
|
|
this.lastNotifyAt.set(userId, now);
|
|
return true;
|
|
}
|
|
if (now - last >= this.NOTIFY_COOLDOWN_MS) {
|
|
this.lastNotifyAt.set(userId, now);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Solo para tests
|
|
static reset(): void {
|
|
this.buckets.clear();
|
|
this.lastNotifyAt.clear();
|
|
}
|
|
}
|