@esengine/server 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ratelimit/index.d.ts +787 -0
- package/dist/ratelimit/index.js +818 -0
- package/dist/ratelimit/index.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { __name, __publicField } from '../chunk-T626JPC7.js';
|
|
2
|
+
|
|
3
|
+
// src/ratelimit/strategies/TokenBucket.ts
|
|
4
|
+
var _TokenBucketStrategy = class _TokenBucketStrategy {
|
|
5
|
+
/**
|
|
6
|
+
* @zh 创建令牌桶策略
|
|
7
|
+
* @en Create token bucket strategy
|
|
8
|
+
*
|
|
9
|
+
* @param config - @zh 配置 @en Configuration
|
|
10
|
+
* @param config.rate - @zh 每秒添加的令牌数 @en Tokens added per second
|
|
11
|
+
* @param config.capacity - @zh 桶容量(最大令牌数)@en Bucket capacity (max tokens)
|
|
12
|
+
*/
|
|
13
|
+
constructor(config) {
|
|
14
|
+
__publicField(this, "name", "token-bucket");
|
|
15
|
+
__publicField(this, "_rate");
|
|
16
|
+
__publicField(this, "_capacity");
|
|
17
|
+
__publicField(this, "_buckets", /* @__PURE__ */ new Map());
|
|
18
|
+
this._rate = config.rate;
|
|
19
|
+
this._capacity = config.capacity;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @zh 尝试消费令牌
|
|
23
|
+
* @en Try to consume tokens
|
|
24
|
+
*/
|
|
25
|
+
consume(key, cost = 1) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const bucket = this._getOrCreateBucket(key, now);
|
|
28
|
+
this._refillBucket(bucket, now);
|
|
29
|
+
if (bucket.tokens >= cost) {
|
|
30
|
+
bucket.tokens -= cost;
|
|
31
|
+
return {
|
|
32
|
+
allowed: true,
|
|
33
|
+
remaining: Math.floor(bucket.tokens),
|
|
34
|
+
resetAt: now + Math.ceil((this._capacity - bucket.tokens) / this._rate * 1e3)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const tokensNeeded = cost - bucket.tokens;
|
|
38
|
+
const retryAfter = Math.ceil(tokensNeeded / this._rate * 1e3);
|
|
39
|
+
return {
|
|
40
|
+
allowed: false,
|
|
41
|
+
remaining: 0,
|
|
42
|
+
resetAt: now + retryAfter,
|
|
43
|
+
retryAfter
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @zh 获取当前状态
|
|
48
|
+
* @en Get current status
|
|
49
|
+
*/
|
|
50
|
+
getStatus(key) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const bucket = this._buckets.get(key);
|
|
53
|
+
if (!bucket) {
|
|
54
|
+
return {
|
|
55
|
+
allowed: true,
|
|
56
|
+
remaining: this._capacity,
|
|
57
|
+
resetAt: now
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
this._refillBucket(bucket, now);
|
|
61
|
+
return {
|
|
62
|
+
allowed: bucket.tokens >= 1,
|
|
63
|
+
remaining: Math.floor(bucket.tokens),
|
|
64
|
+
resetAt: now + Math.ceil((this._capacity - bucket.tokens) / this._rate * 1e3)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* @zh 重置指定键
|
|
69
|
+
* @en Reset specified key
|
|
70
|
+
*/
|
|
71
|
+
reset(key) {
|
|
72
|
+
this._buckets.delete(key);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* @zh 清理所有记录
|
|
76
|
+
* @en Clean up all records
|
|
77
|
+
*/
|
|
78
|
+
cleanup() {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const expireThreshold = 6e4;
|
|
81
|
+
for (const [key, bucket] of this._buckets) {
|
|
82
|
+
if (now - bucket.lastUpdate > expireThreshold && bucket.tokens >= this._capacity) {
|
|
83
|
+
this._buckets.delete(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @zh 获取或创建桶
|
|
89
|
+
* @en Get or create bucket
|
|
90
|
+
*/
|
|
91
|
+
_getOrCreateBucket(key, now) {
|
|
92
|
+
let bucket = this._buckets.get(key);
|
|
93
|
+
if (!bucket) {
|
|
94
|
+
bucket = {
|
|
95
|
+
tokens: this._capacity,
|
|
96
|
+
lastUpdate: now
|
|
97
|
+
};
|
|
98
|
+
this._buckets.set(key, bucket);
|
|
99
|
+
}
|
|
100
|
+
return bucket;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* @zh 补充令牌
|
|
104
|
+
* @en Refill tokens
|
|
105
|
+
*/
|
|
106
|
+
_refillBucket(bucket, now) {
|
|
107
|
+
const elapsed = now - bucket.lastUpdate;
|
|
108
|
+
const tokensToAdd = elapsed / 1e3 * this._rate;
|
|
109
|
+
bucket.tokens = Math.min(this._capacity, bucket.tokens + tokensToAdd);
|
|
110
|
+
bucket.lastUpdate = now;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
__name(_TokenBucketStrategy, "TokenBucketStrategy");
|
|
114
|
+
var TokenBucketStrategy = _TokenBucketStrategy;
|
|
115
|
+
function createTokenBucketStrategy(config) {
|
|
116
|
+
return new TokenBucketStrategy(config);
|
|
117
|
+
}
|
|
118
|
+
__name(createTokenBucketStrategy, "createTokenBucketStrategy");
|
|
119
|
+
|
|
120
|
+
// src/ratelimit/strategies/SlidingWindow.ts
|
|
121
|
+
var _SlidingWindowStrategy = class _SlidingWindowStrategy {
|
|
122
|
+
/**
|
|
123
|
+
* @zh 创建滑动窗口策略
|
|
124
|
+
* @en Create sliding window strategy
|
|
125
|
+
*
|
|
126
|
+
* @param config - @zh 配置 @en Configuration
|
|
127
|
+
* @param config.rate - @zh 每秒允许的请求数 @en Requests allowed per second
|
|
128
|
+
* @param config.capacity - @zh 窗口容量 @en Window capacity
|
|
129
|
+
*/
|
|
130
|
+
constructor(config) {
|
|
131
|
+
__publicField(this, "name", "sliding-window");
|
|
132
|
+
__publicField(this, "_rate");
|
|
133
|
+
__publicField(this, "_capacity");
|
|
134
|
+
__publicField(this, "_windowMs");
|
|
135
|
+
__publicField(this, "_windows", /* @__PURE__ */ new Map());
|
|
136
|
+
this._rate = config.rate;
|
|
137
|
+
this._capacity = config.capacity;
|
|
138
|
+
this._windowMs = 1e3;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* @zh 尝试消费配额
|
|
142
|
+
* @en Try to consume quota
|
|
143
|
+
*/
|
|
144
|
+
consume(key, cost = 1) {
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const window = this._getOrCreateWindow(key);
|
|
147
|
+
this._cleanExpiredTimestamps(window, now);
|
|
148
|
+
const currentCount = window.timestamps.length;
|
|
149
|
+
if (currentCount + cost <= this._capacity) {
|
|
150
|
+
for (let i = 0; i < cost; i++) {
|
|
151
|
+
window.timestamps.push(now);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
allowed: true,
|
|
155
|
+
remaining: this._capacity - window.timestamps.length,
|
|
156
|
+
resetAt: this._getResetAt(window, now)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const oldestTimestamp = window.timestamps[0] || now;
|
|
160
|
+
const retryAfter = Math.max(0, oldestTimestamp + this._windowMs - now);
|
|
161
|
+
return {
|
|
162
|
+
allowed: false,
|
|
163
|
+
remaining: 0,
|
|
164
|
+
resetAt: oldestTimestamp + this._windowMs,
|
|
165
|
+
retryAfter
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* @zh 获取当前状态
|
|
170
|
+
* @en Get current status
|
|
171
|
+
*/
|
|
172
|
+
getStatus(key) {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
const window = this._windows.get(key);
|
|
175
|
+
if (!window) {
|
|
176
|
+
return {
|
|
177
|
+
allowed: true,
|
|
178
|
+
remaining: this._capacity,
|
|
179
|
+
resetAt: now + this._windowMs
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
this._cleanExpiredTimestamps(window, now);
|
|
183
|
+
const remaining = Math.max(0, this._capacity - window.timestamps.length);
|
|
184
|
+
return {
|
|
185
|
+
allowed: remaining > 0,
|
|
186
|
+
remaining,
|
|
187
|
+
resetAt: this._getResetAt(window, now)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* @zh 重置指定键
|
|
192
|
+
* @en Reset specified key
|
|
193
|
+
*/
|
|
194
|
+
reset(key) {
|
|
195
|
+
this._windows.delete(key);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* @zh 清理所有过期记录
|
|
199
|
+
* @en Clean up all expired records
|
|
200
|
+
*/
|
|
201
|
+
cleanup() {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
for (const [key, window] of this._windows) {
|
|
204
|
+
this._cleanExpiredTimestamps(window, now);
|
|
205
|
+
if (window.timestamps.length === 0) {
|
|
206
|
+
this._windows.delete(key);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* @zh 获取或创建窗口
|
|
212
|
+
* @en Get or create window
|
|
213
|
+
*/
|
|
214
|
+
_getOrCreateWindow(key) {
|
|
215
|
+
let window = this._windows.get(key);
|
|
216
|
+
if (!window) {
|
|
217
|
+
window = {
|
|
218
|
+
timestamps: []
|
|
219
|
+
};
|
|
220
|
+
this._windows.set(key, window);
|
|
221
|
+
}
|
|
222
|
+
return window;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* @zh 清理过期的时间戳
|
|
226
|
+
* @en Clean expired timestamps
|
|
227
|
+
*/
|
|
228
|
+
_cleanExpiredTimestamps(window, now) {
|
|
229
|
+
const cutoff = now - this._windowMs;
|
|
230
|
+
window.timestamps = window.timestamps.filter((ts) => ts > cutoff);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* @zh 获取重置时间
|
|
234
|
+
* @en Get reset time
|
|
235
|
+
*/
|
|
236
|
+
_getResetAt(window, now) {
|
|
237
|
+
if (window.timestamps.length === 0) {
|
|
238
|
+
return now + this._windowMs;
|
|
239
|
+
}
|
|
240
|
+
return window.timestamps[0] + this._windowMs;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
__name(_SlidingWindowStrategy, "SlidingWindowStrategy");
|
|
244
|
+
var SlidingWindowStrategy = _SlidingWindowStrategy;
|
|
245
|
+
function createSlidingWindowStrategy(config) {
|
|
246
|
+
return new SlidingWindowStrategy(config);
|
|
247
|
+
}
|
|
248
|
+
__name(createSlidingWindowStrategy, "createSlidingWindowStrategy");
|
|
249
|
+
|
|
250
|
+
// src/ratelimit/strategies/FixedWindow.ts
|
|
251
|
+
var _FixedWindowStrategy = class _FixedWindowStrategy {
|
|
252
|
+
/**
|
|
253
|
+
* @zh 创建固定窗口策略
|
|
254
|
+
* @en Create fixed window strategy
|
|
255
|
+
*
|
|
256
|
+
* @param config - @zh 配置 @en Configuration
|
|
257
|
+
* @param config.rate - @zh 每秒允许的请求数 @en Requests allowed per second
|
|
258
|
+
* @param config.capacity - @zh 窗口容量 @en Window capacity
|
|
259
|
+
*/
|
|
260
|
+
constructor(config) {
|
|
261
|
+
__publicField(this, "name", "fixed-window");
|
|
262
|
+
__publicField(this, "_rate");
|
|
263
|
+
__publicField(this, "_capacity");
|
|
264
|
+
__publicField(this, "_windowMs");
|
|
265
|
+
__publicField(this, "_windows", /* @__PURE__ */ new Map());
|
|
266
|
+
this._rate = config.rate;
|
|
267
|
+
this._capacity = config.capacity;
|
|
268
|
+
this._windowMs = 1e3;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* @zh 尝试消费配额
|
|
272
|
+
* @en Try to consume quota
|
|
273
|
+
*/
|
|
274
|
+
consume(key, cost = 1) {
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
const window = this._getOrCreateWindow(key, now);
|
|
277
|
+
this._maybeResetWindow(window, now);
|
|
278
|
+
if (window.count + cost <= this._capacity) {
|
|
279
|
+
window.count += cost;
|
|
280
|
+
return {
|
|
281
|
+
allowed: true,
|
|
282
|
+
remaining: this._capacity - window.count,
|
|
283
|
+
resetAt: window.windowStart + this._windowMs
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const retryAfter = window.windowStart + this._windowMs - now;
|
|
287
|
+
return {
|
|
288
|
+
allowed: false,
|
|
289
|
+
remaining: 0,
|
|
290
|
+
resetAt: window.windowStart + this._windowMs,
|
|
291
|
+
retryAfter: Math.max(0, retryAfter)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* @zh 获取当前状态
|
|
296
|
+
* @en Get current status
|
|
297
|
+
*/
|
|
298
|
+
getStatus(key) {
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
const window = this._windows.get(key);
|
|
301
|
+
if (!window) {
|
|
302
|
+
return {
|
|
303
|
+
allowed: true,
|
|
304
|
+
remaining: this._capacity,
|
|
305
|
+
resetAt: this._getWindowStart(now) + this._windowMs
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
this._maybeResetWindow(window, now);
|
|
309
|
+
const remaining = Math.max(0, this._capacity - window.count);
|
|
310
|
+
return {
|
|
311
|
+
allowed: remaining > 0,
|
|
312
|
+
remaining,
|
|
313
|
+
resetAt: window.windowStart + this._windowMs
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* @zh 重置指定键
|
|
318
|
+
* @en Reset specified key
|
|
319
|
+
*/
|
|
320
|
+
reset(key) {
|
|
321
|
+
this._windows.delete(key);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* @zh 清理所有过期记录
|
|
325
|
+
* @en Clean up all expired records
|
|
326
|
+
*/
|
|
327
|
+
cleanup() {
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
const currentWindowStart = this._getWindowStart(now);
|
|
330
|
+
for (const [key, window] of this._windows) {
|
|
331
|
+
if (window.windowStart < currentWindowStart - this._windowMs) {
|
|
332
|
+
this._windows.delete(key);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* @zh 获取或创建窗口
|
|
338
|
+
* @en Get or create window
|
|
339
|
+
*/
|
|
340
|
+
_getOrCreateWindow(key, now) {
|
|
341
|
+
let window = this._windows.get(key);
|
|
342
|
+
if (!window) {
|
|
343
|
+
window = {
|
|
344
|
+
count: 0,
|
|
345
|
+
windowStart: this._getWindowStart(now)
|
|
346
|
+
};
|
|
347
|
+
this._windows.set(key, window);
|
|
348
|
+
}
|
|
349
|
+
return window;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* @zh 如果需要则重置窗口
|
|
353
|
+
* @en Reset window if needed
|
|
354
|
+
*/
|
|
355
|
+
_maybeResetWindow(window, now) {
|
|
356
|
+
const currentWindowStart = this._getWindowStart(now);
|
|
357
|
+
if (window.windowStart < currentWindowStart) {
|
|
358
|
+
window.count = 0;
|
|
359
|
+
window.windowStart = currentWindowStart;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* @zh 获取窗口开始时间
|
|
364
|
+
* @en Get window start time
|
|
365
|
+
*/
|
|
366
|
+
_getWindowStart(now) {
|
|
367
|
+
return Math.floor(now / this._windowMs) * this._windowMs;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
__name(_FixedWindowStrategy, "FixedWindowStrategy");
|
|
371
|
+
var FixedWindowStrategy = _FixedWindowStrategy;
|
|
372
|
+
function createFixedWindowStrategy(config) {
|
|
373
|
+
return new FixedWindowStrategy(config);
|
|
374
|
+
}
|
|
375
|
+
__name(createFixedWindowStrategy, "createFixedWindowStrategy");
|
|
376
|
+
|
|
377
|
+
// src/ratelimit/context.ts
|
|
378
|
+
var _RateLimitContext = class _RateLimitContext {
|
|
379
|
+
/**
|
|
380
|
+
* @zh 创建速率限制上下文
|
|
381
|
+
* @en Create rate limit context
|
|
382
|
+
*
|
|
383
|
+
* @param key - @zh 限流键(通常是玩家ID)@en Rate limit key (usually player ID)
|
|
384
|
+
* @param globalStrategy - @zh 全局限流策略 @en Global rate limit strategy
|
|
385
|
+
*/
|
|
386
|
+
constructor(key, globalStrategy) {
|
|
387
|
+
__publicField(this, "_key");
|
|
388
|
+
__publicField(this, "_globalStrategy");
|
|
389
|
+
__publicField(this, "_messageStrategies", /* @__PURE__ */ new Map());
|
|
390
|
+
__publicField(this, "_consecutiveLimitCount", 0);
|
|
391
|
+
this._key = key;
|
|
392
|
+
this._globalStrategy = globalStrategy;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* @zh 获取连续被限流次数
|
|
396
|
+
* @en Get consecutive limit count
|
|
397
|
+
*/
|
|
398
|
+
get consecutiveLimitCount() {
|
|
399
|
+
return this._consecutiveLimitCount;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* @zh 检查是否允许(不消费)
|
|
403
|
+
* @en Check if allowed (without consuming)
|
|
404
|
+
*/
|
|
405
|
+
check(messageType) {
|
|
406
|
+
if (messageType && this._messageStrategies.has(messageType)) {
|
|
407
|
+
return this._messageStrategies.get(messageType).getStatus(this._key);
|
|
408
|
+
}
|
|
409
|
+
return this._globalStrategy.getStatus(this._key);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* @zh 消费配额
|
|
413
|
+
* @en Consume quota
|
|
414
|
+
*/
|
|
415
|
+
consume(messageType, cost = 1) {
|
|
416
|
+
let result;
|
|
417
|
+
if (messageType && this._messageStrategies.has(messageType)) {
|
|
418
|
+
result = this._messageStrategies.get(messageType).consume(this._key, cost);
|
|
419
|
+
} else {
|
|
420
|
+
result = this._globalStrategy.consume(this._key, cost);
|
|
421
|
+
}
|
|
422
|
+
if (result.allowed) {
|
|
423
|
+
this._consecutiveLimitCount = 0;
|
|
424
|
+
} else {
|
|
425
|
+
this._consecutiveLimitCount++;
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* @zh 重置限流状态
|
|
431
|
+
* @en Reset rate limit status
|
|
432
|
+
*/
|
|
433
|
+
reset(messageType) {
|
|
434
|
+
if (messageType) {
|
|
435
|
+
if (this._messageStrategies.has(messageType)) {
|
|
436
|
+
this._messageStrategies.get(messageType).reset(this._key);
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
this._globalStrategy.reset(this._key);
|
|
440
|
+
for (const strategy of this._messageStrategies.values()) {
|
|
441
|
+
strategy.reset(this._key);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* @zh 重置连续限流计数
|
|
447
|
+
* @en Reset consecutive limit count
|
|
448
|
+
*/
|
|
449
|
+
resetConsecutiveCount() {
|
|
450
|
+
this._consecutiveLimitCount = 0;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* @zh 为特定消息类型设置独立的限流策略
|
|
454
|
+
* @en Set independent rate limit strategy for specific message type
|
|
455
|
+
*
|
|
456
|
+
* @param messageType - @zh 消息类型 @en Message type
|
|
457
|
+
* @param strategy - @zh 限流策略 @en Rate limit strategy
|
|
458
|
+
*/
|
|
459
|
+
setMessageStrategy(messageType, strategy) {
|
|
460
|
+
this._messageStrategies.set(messageType, strategy);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* @zh 移除特定消息类型的限流策略
|
|
464
|
+
* @en Remove rate limit strategy for specific message type
|
|
465
|
+
*
|
|
466
|
+
* @param messageType - @zh 消息类型 @en Message type
|
|
467
|
+
*/
|
|
468
|
+
removeMessageStrategy(messageType) {
|
|
469
|
+
this._messageStrategies.delete(messageType);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* @zh 检查是否有特定消息类型的限流策略
|
|
473
|
+
* @en Check if has rate limit strategy for specific message type
|
|
474
|
+
*
|
|
475
|
+
* @param messageType - @zh 消息类型 @en Message type
|
|
476
|
+
*/
|
|
477
|
+
hasMessageStrategy(messageType) {
|
|
478
|
+
return this._messageStrategies.has(messageType);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
__name(_RateLimitContext, "RateLimitContext");
|
|
482
|
+
var RateLimitContext = _RateLimitContext;
|
|
483
|
+
|
|
484
|
+
// src/ratelimit/decorators/rateLimit.ts
|
|
485
|
+
var RATE_LIMIT_METADATA_KEY = /* @__PURE__ */ Symbol("rateLimitMetadata");
|
|
486
|
+
function getRateLimitMetadata(target, messageType) {
|
|
487
|
+
const metadataMap = target[RATE_LIMIT_METADATA_KEY];
|
|
488
|
+
return metadataMap?.get(messageType);
|
|
489
|
+
}
|
|
490
|
+
__name(getRateLimitMetadata, "getRateLimitMetadata");
|
|
491
|
+
function setRateLimitMetadata(target, messageType, metadata) {
|
|
492
|
+
if (!target[RATE_LIMIT_METADATA_KEY]) {
|
|
493
|
+
target[RATE_LIMIT_METADATA_KEY] = /* @__PURE__ */ new Map();
|
|
494
|
+
}
|
|
495
|
+
const metadataMap = target[RATE_LIMIT_METADATA_KEY];
|
|
496
|
+
const existing = metadataMap.get(messageType) ?? {
|
|
497
|
+
enabled: true
|
|
498
|
+
};
|
|
499
|
+
metadataMap.set(messageType, {
|
|
500
|
+
...existing,
|
|
501
|
+
...metadata
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
__name(setRateLimitMetadata, "setRateLimitMetadata");
|
|
505
|
+
function getMessageTypeFromMethod(target, methodName) {
|
|
506
|
+
for (const sym of Object.getOwnPropertySymbols(target.constructor)) {
|
|
507
|
+
const desc = Object.getOwnPropertyDescriptor(target.constructor, sym);
|
|
508
|
+
if (desc?.value && Array.isArray(desc.value)) {
|
|
509
|
+
for (const handler of desc.value) {
|
|
510
|
+
if (handler.method === methodName) {
|
|
511
|
+
return handler.type;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const handlers = target.constructor[/* @__PURE__ */ Symbol.for("messageHandlers")];
|
|
517
|
+
if (handlers) {
|
|
518
|
+
for (const handler of handlers) {
|
|
519
|
+
if (handler.method === methodName) {
|
|
520
|
+
return handler.type;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return void 0;
|
|
525
|
+
}
|
|
526
|
+
__name(getMessageTypeFromMethod, "getMessageTypeFromMethod");
|
|
527
|
+
function rateLimit(config) {
|
|
528
|
+
return function(target, propertyKey, descriptor) {
|
|
529
|
+
const methodName = String(propertyKey);
|
|
530
|
+
queueMicrotask(() => {
|
|
531
|
+
const msgType = getMessageTypeFromMethod(target, methodName);
|
|
532
|
+
if (msgType) {
|
|
533
|
+
setRateLimitMetadata(target, msgType, {
|
|
534
|
+
enabled: true,
|
|
535
|
+
config
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
if (!target.hasOwnProperty(RATE_LIMIT_METADATA_KEY)) {
|
|
540
|
+
Object.defineProperty(target, RATE_LIMIT_METADATA_KEY, {
|
|
541
|
+
value: /* @__PURE__ */ new Map(),
|
|
542
|
+
writable: false,
|
|
543
|
+
enumerable: false
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return descriptor;
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
__name(rateLimit, "rateLimit");
|
|
550
|
+
function noRateLimit() {
|
|
551
|
+
return function(target, propertyKey, descriptor) {
|
|
552
|
+
const methodName = String(propertyKey);
|
|
553
|
+
queueMicrotask(() => {
|
|
554
|
+
const msgType = getMessageTypeFromMethod(target, methodName);
|
|
555
|
+
if (msgType) {
|
|
556
|
+
setRateLimitMetadata(target, msgType, {
|
|
557
|
+
enabled: false,
|
|
558
|
+
exempt: true
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
return descriptor;
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
__name(noRateLimit, "noRateLimit");
|
|
566
|
+
function rateLimitMessage(messageType, config) {
|
|
567
|
+
return function(target, propertyKey, descriptor) {
|
|
568
|
+
setRateLimitMetadata(target, messageType, {
|
|
569
|
+
enabled: true,
|
|
570
|
+
config
|
|
571
|
+
});
|
|
572
|
+
return descriptor;
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
__name(rateLimitMessage, "rateLimitMessage");
|
|
576
|
+
function noRateLimitMessage(messageType) {
|
|
577
|
+
return function(target, propertyKey, descriptor) {
|
|
578
|
+
setRateLimitMetadata(target, messageType, {
|
|
579
|
+
enabled: false,
|
|
580
|
+
exempt: true
|
|
581
|
+
});
|
|
582
|
+
return descriptor;
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
__name(noRateLimitMessage, "noRateLimitMessage");
|
|
586
|
+
|
|
587
|
+
// src/ratelimit/mixin/withRateLimit.ts
|
|
588
|
+
var PLAYER_RATE_LIMIT_CONTEXT = /* @__PURE__ */ Symbol("playerRateLimitContext");
|
|
589
|
+
function createStrategy(config) {
|
|
590
|
+
const rate = config.messagesPerSecond ?? 10;
|
|
591
|
+
const capacity = config.burstSize ?? rate * 2;
|
|
592
|
+
switch (config.strategy) {
|
|
593
|
+
case "sliding-window":
|
|
594
|
+
return new SlidingWindowStrategy({
|
|
595
|
+
rate,
|
|
596
|
+
capacity
|
|
597
|
+
});
|
|
598
|
+
case "fixed-window":
|
|
599
|
+
return new FixedWindowStrategy({
|
|
600
|
+
rate,
|
|
601
|
+
capacity
|
|
602
|
+
});
|
|
603
|
+
case "token-bucket":
|
|
604
|
+
default:
|
|
605
|
+
return new TokenBucketStrategy({
|
|
606
|
+
rate,
|
|
607
|
+
capacity
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
__name(createStrategy, "createStrategy");
|
|
612
|
+
function getPlayerRateLimitContext(player) {
|
|
613
|
+
const data = player;
|
|
614
|
+
return data[PLAYER_RATE_LIMIT_CONTEXT] ?? null;
|
|
615
|
+
}
|
|
616
|
+
__name(getPlayerRateLimitContext, "getPlayerRateLimitContext");
|
|
617
|
+
function setPlayerRateLimitContext(player, context) {
|
|
618
|
+
const data = player;
|
|
619
|
+
data[PLAYER_RATE_LIMIT_CONTEXT] = context;
|
|
620
|
+
Object.defineProperty(player, "rateLimit", {
|
|
621
|
+
get: /* @__PURE__ */ __name(() => data[PLAYER_RATE_LIMIT_CONTEXT], "get"),
|
|
622
|
+
enumerable: true,
|
|
623
|
+
configurable: false
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
__name(setPlayerRateLimitContext, "setPlayerRateLimitContext");
|
|
627
|
+
function withRateLimit(Base, config = {}) {
|
|
628
|
+
var _a;
|
|
629
|
+
const { messagesPerSecond = 10, burstSize = 20, strategy = "token-bucket", onLimited, disconnectOnLimit = false, maxConsecutiveLimits = 0, getKey = /* @__PURE__ */ __name((player) => player.id, "getKey"), cleanupInterval = 6e4 } = config;
|
|
630
|
+
let RateLimitRoom = (_a = class extends Base {
|
|
631
|
+
constructor(...args) {
|
|
632
|
+
super(...args);
|
|
633
|
+
__publicField(this, "_rateLimitStrategy");
|
|
634
|
+
__publicField(this, "_playerContexts", /* @__PURE__ */ new WeakMap());
|
|
635
|
+
__publicField(this, "_cleanupTimer", null);
|
|
636
|
+
__publicField(this, "_messageStrategies", /* @__PURE__ */ new Map());
|
|
637
|
+
this._rateLimitStrategy = createStrategy({
|
|
638
|
+
messagesPerSecond,
|
|
639
|
+
burstSize,
|
|
640
|
+
strategy
|
|
641
|
+
});
|
|
642
|
+
this._startCleanup();
|
|
643
|
+
this._initMessageStrategies();
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* @zh 全局速率限制策略
|
|
647
|
+
* @en Global rate limit strategy
|
|
648
|
+
*/
|
|
649
|
+
get rateLimitStrategy() {
|
|
650
|
+
return this._rateLimitStrategy;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* @zh 获取玩家的速率限制上下文
|
|
654
|
+
* @en Get rate limit context for player
|
|
655
|
+
*/
|
|
656
|
+
getRateLimitContext(player) {
|
|
657
|
+
return this._playerContexts.get(player) ?? null;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* @internal
|
|
661
|
+
* @zh 重写消息处理以添加速率限制检查
|
|
662
|
+
* @en Override message handling to add rate limit check
|
|
663
|
+
*/
|
|
664
|
+
_handleMessage(type, data, playerId) {
|
|
665
|
+
const player = this.getPlayer(playerId);
|
|
666
|
+
if (!player) return;
|
|
667
|
+
let context = this._playerContexts.get(player);
|
|
668
|
+
if (!context) {
|
|
669
|
+
context = this._createPlayerContext(player);
|
|
670
|
+
}
|
|
671
|
+
const metadata = this._getMessageMetadata(type);
|
|
672
|
+
if (metadata?.exempt) {
|
|
673
|
+
super._handleMessage(type, data, playerId);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const cost = metadata?.config?.cost ?? 1;
|
|
677
|
+
let result;
|
|
678
|
+
if (metadata?.config && (metadata.config.messagesPerSecond || metadata.config.burstSize)) {
|
|
679
|
+
if (!context.hasMessageStrategy(type)) {
|
|
680
|
+
const msgStrategy = createStrategy({
|
|
681
|
+
messagesPerSecond: metadata.config.messagesPerSecond ?? messagesPerSecond,
|
|
682
|
+
burstSize: metadata.config.burstSize ?? burstSize,
|
|
683
|
+
strategy
|
|
684
|
+
});
|
|
685
|
+
context.setMessageStrategy(type, msgStrategy);
|
|
686
|
+
}
|
|
687
|
+
result = context.consume(type, cost);
|
|
688
|
+
} else {
|
|
689
|
+
result = context.consume(void 0, cost);
|
|
690
|
+
}
|
|
691
|
+
if (!result.allowed) {
|
|
692
|
+
this._handleRateLimited(player, type, result, context);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
super._handleMessage(type, data, playerId);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* @internal
|
|
699
|
+
* @zh 重写 _addPlayer 以初始化速率限制上下文
|
|
700
|
+
* @en Override _addPlayer to initialize rate limit context
|
|
701
|
+
*/
|
|
702
|
+
async _addPlayer(id, conn) {
|
|
703
|
+
const player = await super._addPlayer(id, conn);
|
|
704
|
+
if (player) {
|
|
705
|
+
this._createPlayerContext(player);
|
|
706
|
+
}
|
|
707
|
+
return player;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* @internal
|
|
711
|
+
* @zh 重写 _removePlayer 以清理速率限制上下文
|
|
712
|
+
* @en Override _removePlayer to cleanup rate limit context
|
|
713
|
+
*/
|
|
714
|
+
async _removePlayer(id, reason) {
|
|
715
|
+
const player = this.getPlayer(id);
|
|
716
|
+
if (player) {
|
|
717
|
+
const context = this._playerContexts.get(player);
|
|
718
|
+
if (context) {
|
|
719
|
+
context.reset();
|
|
720
|
+
}
|
|
721
|
+
this._playerContexts.delete(player);
|
|
722
|
+
}
|
|
723
|
+
await super._removePlayer(id, reason);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* @zh 重写 dispose 以清理定时器
|
|
727
|
+
* @en Override dispose to cleanup timer
|
|
728
|
+
*/
|
|
729
|
+
dispose() {
|
|
730
|
+
this._stopCleanup();
|
|
731
|
+
super.dispose();
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* @zh 创建玩家的速率限制上下文
|
|
735
|
+
* @en Create rate limit context for player
|
|
736
|
+
*/
|
|
737
|
+
_createPlayerContext(player) {
|
|
738
|
+
const key = getKey(player);
|
|
739
|
+
const context = new RateLimitContext(key, this._rateLimitStrategy);
|
|
740
|
+
this._playerContexts.set(player, context);
|
|
741
|
+
setPlayerRateLimitContext(player, context);
|
|
742
|
+
return context;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* @zh 处理被限流的情况
|
|
746
|
+
* @en Handle rate limited situation
|
|
747
|
+
*/
|
|
748
|
+
_handleRateLimited(player, messageType, result, context) {
|
|
749
|
+
if (this.onRateLimited) {
|
|
750
|
+
this.onRateLimited(player, messageType, result);
|
|
751
|
+
}
|
|
752
|
+
onLimited?.(player, messageType, result);
|
|
753
|
+
if (disconnectOnLimit) {
|
|
754
|
+
this.kick(player, "rate_limited");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (maxConsecutiveLimits > 0 && context.consecutiveLimitCount >= maxConsecutiveLimits) {
|
|
758
|
+
this.kick(player, "too_many_rate_limits");
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* @zh 获取消息的元数据
|
|
763
|
+
* @en Get message metadata
|
|
764
|
+
*/
|
|
765
|
+
_getMessageMetadata(type) {
|
|
766
|
+
return getRateLimitMetadata(this.constructor.prototype, type);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* @zh 初始化消息策略(从装饰器元数据)
|
|
770
|
+
* @en Initialize message strategies (from decorator metadata)
|
|
771
|
+
*/
|
|
772
|
+
_initMessageStrategies() {
|
|
773
|
+
const metadataMap = this.constructor.prototype[RATE_LIMIT_METADATA_KEY];
|
|
774
|
+
if (metadataMap instanceof Map) {
|
|
775
|
+
for (const [msgType, metadata] of metadataMap) {
|
|
776
|
+
if (metadata.config && (metadata.config.messagesPerSecond || metadata.config.burstSize)) {
|
|
777
|
+
const msgStrategy = createStrategy({
|
|
778
|
+
messagesPerSecond: metadata.config.messagesPerSecond ?? messagesPerSecond,
|
|
779
|
+
burstSize: metadata.config.burstSize ?? burstSize,
|
|
780
|
+
strategy
|
|
781
|
+
});
|
|
782
|
+
this._messageStrategies.set(msgType, msgStrategy);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* @zh 开始清理定时器
|
|
789
|
+
* @en Start cleanup timer
|
|
790
|
+
*/
|
|
791
|
+
_startCleanup() {
|
|
792
|
+
if (cleanupInterval > 0) {
|
|
793
|
+
this._cleanupTimer = setInterval(() => {
|
|
794
|
+
this._rateLimitStrategy.cleanup();
|
|
795
|
+
for (const strategy2 of this._messageStrategies.values()) {
|
|
796
|
+
strategy2.cleanup();
|
|
797
|
+
}
|
|
798
|
+
}, cleanupInterval);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* @zh 停止清理定时器
|
|
803
|
+
* @en Stop cleanup timer
|
|
804
|
+
*/
|
|
805
|
+
_stopCleanup() {
|
|
806
|
+
if (this._cleanupTimer) {
|
|
807
|
+
clearInterval(this._cleanupTimer);
|
|
808
|
+
this._cleanupTimer = null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}, __name(_a, "RateLimitRoom"), _a);
|
|
812
|
+
return RateLimitRoom;
|
|
813
|
+
}
|
|
814
|
+
__name(withRateLimit, "withRateLimit");
|
|
815
|
+
|
|
816
|
+
export { FixedWindowStrategy, RATE_LIMIT_METADATA_KEY, RateLimitContext, SlidingWindowStrategy, TokenBucketStrategy, createFixedWindowStrategy, createSlidingWindowStrategy, createTokenBucketStrategy, getPlayerRateLimitContext, getRateLimitMetadata, noRateLimit, noRateLimitMessage, rateLimit, rateLimitMessage, withRateLimit };
|
|
817
|
+
//# sourceMappingURL=index.js.map
|
|
818
|
+
//# sourceMappingURL=index.js.map
|