Files
esengine/packages/framework/server/src/ratelimit/strategies/FixedWindow.ts

205 lines
5.3 KiB
TypeScript
Raw Normal View History

/**
* @zh
* @en Fixed window rate limit strategy
*/
import type { IRateLimitStrategy, RateLimitResult, StrategyConfig } from '../types.js';
/**
* @zh
* @en Fixed window state
*/
interface WindowState {
/**
* @zh
* @en Current window count
*/
count: number;
/**
* @zh
* @en Window start time
*/
windowStart: number;
}
/**
* @zh
* @en Fixed window rate limit strategy
*
* @zh
*
* @en Fixed window algorithm divides time into fixed-length windows and counts requests in each window.
* Simple to implement with low memory overhead, but may have 2x burst issue at window boundaries.
*
* @example
* ```typescript
* const strategy = new FixedWindowStrategy({
* rate: 10, // 10 requests per second
* capacity: 10 // same as rate for 1-second window
* });
*
* const result = strategy.consume('player-123');
* if (result.allowed) {
* // Process message
* }
* ```
*/
export class FixedWindowStrategy implements IRateLimitStrategy {
readonly name = 'fixed-window';
private _rate: number;
private _capacity: number;
private _windowMs: number;
private _windows: Map<string, WindowState> = new Map();
/**
* @zh
* @en Create fixed window strategy
*
* @param config - @zh @en Configuration
* @param config.rate - @zh @en Requests allowed per second
* @param config.capacity - @zh @en Window capacity
*/
constructor(config: StrategyConfig) {
this._rate = config.rate;
this._capacity = config.capacity;
this._windowMs = 1000;
}
/**
* @zh
* @en Try to consume quota
*/
consume(key: string, cost: number = 1): RateLimitResult {
const now = Date.now();
const window = this._getOrCreateWindow(key, now);
this._maybeResetWindow(window, now);
if (window.count + cost <= this._capacity) {
window.count += cost;
return {
allowed: true,
remaining: this._capacity - window.count,
resetAt: window.windowStart + this._windowMs
};
}
const retryAfter = window.windowStart + this._windowMs - now;
return {
allowed: false,
remaining: 0,
resetAt: window.windowStart + this._windowMs,
retryAfter: Math.max(0, retryAfter)
};
}
/**
* @zh
* @en Get current status
*/
getStatus(key: string): RateLimitResult {
const now = Date.now();
const window = this._windows.get(key);
if (!window) {
return {
allowed: true,
remaining: this._capacity,
resetAt: this._getWindowStart(now) + this._windowMs
};
}
this._maybeResetWindow(window, now);
const remaining = Math.max(0, this._capacity - window.count);
return {
allowed: remaining > 0,
remaining,
resetAt: window.windowStart + this._windowMs
};
}
/**
* @zh
* @en Reset specified key
*/
reset(key: string): void {
this._windows.delete(key);
}
/**
* @zh
* @en Clean up all expired records
*/
cleanup(): void {
const now = Date.now();
const currentWindowStart = this._getWindowStart(now);
for (const [key, window] of this._windows) {
if (window.windowStart < currentWindowStart - this._windowMs) {
this._windows.delete(key);
}
}
}
/**
* @zh
* @en Get or create window
*/
private _getOrCreateWindow(key: string, now: number): WindowState {
let window = this._windows.get(key);
if (!window) {
window = {
count: 0,
windowStart: this._getWindowStart(now)
};
this._windows.set(key, window);
}
return window;
}
/**
* @zh
* @en Reset window if needed
*/
private _maybeResetWindow(window: WindowState, now: number): void {
const currentWindowStart = this._getWindowStart(now);
if (window.windowStart < currentWindowStart) {
window.count = 0;
window.windowStart = currentWindowStart;
}
}
/**
* @zh
* @en Get window start time
*/
private _getWindowStart(now: number): number {
return Math.floor(now / this._windowMs) * this._windowMs;
}
}
/**
* @zh
* @en Create fixed window strategy
*
* @example
* ```typescript
* const strategy = createFixedWindowStrategy({
* rate: 10,
* capacity: 10
* });
* ```
*/
export function createFixedWindowStrategy(config: StrategyConfig): FixedWindowStrategy {
return new FixedWindowStrategy(config);
}