feat(server): 添加可插拔速率限制系统 | add pluggable rate limiting system (#388)

* 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) 模块
- 更新网络模块描述
- 更新项目结构目录
This commit is contained in:
YHH
2025-12-29 17:12:54 +08:00
committed by GitHub
parent 764ce67742
commit afdeb00b4d
23 changed files with 3467 additions and 6 deletions

View File

@@ -0,0 +1,204 @@
/**
* @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);
}