* feat(server): 添加可插拔速率限制系统 | add pluggable rate limiting system - 新增令牌桶策略 (TokenBucketStrategy) - 推荐用于一般场景 - 新增滑动窗口策略 (SlidingWindowStrategy) - 精确跟踪 - 新增固定窗口策略 (FixedWindowStrategy) - 简单高效 - 新增房间速率限制 mixin (withRateLimit) - 新增速率限制装饰器 (@rateLimit, @noRateLimit) - 新增按消息类型限流装饰器 (@rateLimitMessage, @noRateLimitMessage) - 支持与认证系统组合使用 - 添加中英文文档 - 导出路径: @esengine/server/ratelimit * docs: 更新 README 添加新模块 | update README with new modules - 添加程序化生成 (procgen) 模块 - 添加 RPC 框架模块 - 添加游戏服务器 (server) 模块 - 添加事务系统 (transaction) 模块 - 添加世界流送 (world-streaming) 模块 - 更新网络模块描述 - 更新项目结构目录
205 lines
5.3 KiB
TypeScript
205 lines
5.3 KiB
TypeScript
/**
|
|
* @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);
|
|
}
|