@upstash/ratelimit 2.0.6 → 2.0.8
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/index.d.mts +88 -5
- package/dist/index.d.ts +88 -5
- package/dist/index.js +390 -187
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +390 -187
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -115,9 +115,9 @@ var Cache = class {
|
|
|
115
115
|
get(key) {
|
|
116
116
|
return this.cache.get(key) || null;
|
|
117
117
|
}
|
|
118
|
-
incr(key) {
|
|
118
|
+
incr(key, incrementAmount = 1) {
|
|
119
119
|
let value = this.cache.get(key) ?? 0;
|
|
120
|
-
value +=
|
|
120
|
+
value += incrementAmount;
|
|
121
121
|
this.cache.set(key, value);
|
|
122
122
|
return value;
|
|
123
123
|
}
|
|
@@ -132,6 +132,10 @@ var Cache = class {
|
|
|
132
132
|
}
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
+
// src/constants.ts
|
|
136
|
+
var DYNAMIC_LIMIT_KEY_SUFFIX = ":dynamic:global";
|
|
137
|
+
var DEFAULT_PREFIX = "@upstash/ratelimit";
|
|
138
|
+
|
|
135
139
|
// src/duration.ts
|
|
136
140
|
function ms(d) {
|
|
137
141
|
const match = d.match(/^(\d+)\s?(ms|s|m|h|d)$/);
|
|
@@ -177,8 +181,19 @@ var safeEval = async (ctx, script, keys, args) => {
|
|
|
177
181
|
// src/lua-scripts/single.ts
|
|
178
182
|
var fixedWindowLimitScript = `
|
|
179
183
|
local key = KEYS[1]
|
|
180
|
-
local
|
|
181
|
-
local
|
|
184
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
185
|
+
local tokens = tonumber(ARGV[1]) -- default limit
|
|
186
|
+
local window = ARGV[2]
|
|
187
|
+
local incrementBy = ARGV[3] -- increment rate per request at a given value, default is 1
|
|
188
|
+
|
|
189
|
+
-- Check for dynamic limit
|
|
190
|
+
local effectiveLimit = tokens
|
|
191
|
+
if dynamicLimitKey ~= "" then
|
|
192
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
193
|
+
if dynamicLimit then
|
|
194
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
182
197
|
|
|
183
198
|
local r = redis.call("INCRBY", key, incrementBy)
|
|
184
199
|
if r == tonumber(incrementBy) then
|
|
@@ -187,25 +202,47 @@ var fixedWindowLimitScript = `
|
|
|
187
202
|
redis.call("PEXPIRE", key, window)
|
|
188
203
|
end
|
|
189
204
|
|
|
190
|
-
return r
|
|
205
|
+
return {r, effectiveLimit}
|
|
191
206
|
`;
|
|
192
207
|
var fixedWindowRemainingTokensScript = `
|
|
193
|
-
|
|
194
|
-
|
|
208
|
+
local key = KEYS[1]
|
|
209
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
210
|
+
local tokens = tonumber(ARGV[1]) -- default limit
|
|
195
211
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
212
|
+
-- Check for dynamic limit
|
|
213
|
+
local effectiveLimit = tokens
|
|
214
|
+
if dynamicLimitKey ~= "" then
|
|
215
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
216
|
+
if dynamicLimit then
|
|
217
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
local value = redis.call('GET', key)
|
|
222
|
+
local usedTokens = 0
|
|
223
|
+
if value then
|
|
224
|
+
usedTokens = tonumber(value)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
return {effectiveLimit - usedTokens, effectiveLimit}
|
|
228
|
+
`;
|
|
202
229
|
var slidingWindowLimitScript = `
|
|
203
230
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
204
231
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
205
|
-
local
|
|
232
|
+
local dynamicLimitKey = KEYS[3] -- optional: key for dynamic limit in redis
|
|
233
|
+
local tokens = tonumber(ARGV[1]) -- default tokens per window
|
|
206
234
|
local now = ARGV[2] -- current timestamp in milliseconds
|
|
207
235
|
local window = ARGV[3] -- interval in milliseconds
|
|
208
|
-
local incrementBy = ARGV[4]
|
|
236
|
+
local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
|
|
237
|
+
|
|
238
|
+
-- Check for dynamic limit
|
|
239
|
+
local effectiveLimit = tokens
|
|
240
|
+
if dynamicLimitKey ~= "" then
|
|
241
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
242
|
+
if dynamicLimit then
|
|
243
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
209
246
|
|
|
210
247
|
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
211
248
|
if requestsInCurrentWindow == false then
|
|
@@ -219,23 +256,36 @@ var slidingWindowLimitScript = `
|
|
|
219
256
|
local percentageInCurrent = ( now % window ) / window
|
|
220
257
|
-- weighted requests to consider from the previous window
|
|
221
258
|
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
222
|
-
|
|
223
|
-
|
|
259
|
+
|
|
260
|
+
-- Only check limit if not refunding (negative rate)
|
|
261
|
+
if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= effectiveLimit then
|
|
262
|
+
return {-1, effectiveLimit}
|
|
224
263
|
end
|
|
225
264
|
|
|
226
265
|
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
227
|
-
if newValue ==
|
|
266
|
+
if newValue == incrementBy then
|
|
228
267
|
-- The first time this key is set, the value will be equal to incrementBy.
|
|
229
268
|
-- So we only need the expire command once
|
|
230
269
|
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
231
270
|
end
|
|
232
|
-
return
|
|
271
|
+
return {effectiveLimit - ( newValue + requestsInPreviousWindow ), effectiveLimit}
|
|
233
272
|
`;
|
|
234
273
|
var slidingWindowRemainingTokensScript = `
|
|
235
274
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
236
275
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
237
|
-
local
|
|
238
|
-
local
|
|
276
|
+
local dynamicLimitKey = KEYS[3] -- optional: key for dynamic limit in redis
|
|
277
|
+
local tokens = tonumber(ARGV[1]) -- default tokens per window
|
|
278
|
+
local now = ARGV[2] -- current timestamp in milliseconds
|
|
279
|
+
local window = ARGV[3] -- interval in milliseconds
|
|
280
|
+
|
|
281
|
+
-- Check for dynamic limit
|
|
282
|
+
local effectiveLimit = tokens
|
|
283
|
+
if dynamicLimitKey ~= "" then
|
|
284
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
285
|
+
if dynamicLimit then
|
|
286
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
239
289
|
|
|
240
290
|
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
241
291
|
if requestsInCurrentWindow == false then
|
|
@@ -251,15 +301,26 @@ var slidingWindowRemainingTokensScript = `
|
|
|
251
301
|
-- weighted requests to consider from the previous window
|
|
252
302
|
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
253
303
|
|
|
254
|
-
|
|
304
|
+
local usedTokens = requestsInPreviousWindow + requestsInCurrentWindow
|
|
305
|
+
return {effectiveLimit - usedTokens, effectiveLimit}
|
|
255
306
|
`;
|
|
256
307
|
var tokenBucketLimitScript = `
|
|
257
308
|
local key = KEYS[1] -- identifier including prefixes
|
|
258
|
-
local
|
|
309
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
310
|
+
local maxTokens = tonumber(ARGV[1]) -- default maximum number of tokens
|
|
259
311
|
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
260
312
|
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
261
313
|
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
262
314
|
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
315
|
+
|
|
316
|
+
-- Check for dynamic limit
|
|
317
|
+
local effectiveLimit = maxTokens
|
|
318
|
+
if dynamicLimitKey ~= "" then
|
|
319
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
320
|
+
if dynamicLimit then
|
|
321
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
263
324
|
|
|
264
325
|
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
265
326
|
|
|
@@ -268,7 +329,7 @@ var tokenBucketLimitScript = `
|
|
|
268
329
|
|
|
269
330
|
if bucket[1] == false then
|
|
270
331
|
refilledAt = now
|
|
271
|
-
tokens =
|
|
332
|
+
tokens = effectiveLimit
|
|
272
333
|
else
|
|
273
334
|
refilledAt = tonumber(bucket[1])
|
|
274
335
|
tokens = tonumber(bucket[2])
|
|
@@ -276,34 +337,48 @@ var tokenBucketLimitScript = `
|
|
|
276
337
|
|
|
277
338
|
if now >= refilledAt + interval then
|
|
278
339
|
local numRefills = math.floor((now - refilledAt) / interval)
|
|
279
|
-
tokens = math.min(
|
|
340
|
+
tokens = math.min(effectiveLimit, tokens + numRefills * refillRate)
|
|
280
341
|
|
|
281
342
|
refilledAt = refilledAt + numRefills * interval
|
|
282
343
|
end
|
|
283
344
|
|
|
284
|
-
if tokens
|
|
285
|
-
|
|
345
|
+
-- Only reject if tokens are 0 and we're consuming (not refunding)
|
|
346
|
+
if tokens == 0 and incrementBy > 0 then
|
|
347
|
+
return {-1, refilledAt + interval, effectiveLimit}
|
|
286
348
|
end
|
|
287
349
|
|
|
288
350
|
local remaining = tokens - incrementBy
|
|
289
|
-
local expireAt = math.ceil(((
|
|
351
|
+
local expireAt = math.ceil(((effectiveLimit - remaining) / refillRate)) * interval
|
|
290
352
|
|
|
291
353
|
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
292
|
-
|
|
293
|
-
|
|
354
|
+
|
|
355
|
+
if (expireAt > 0) then
|
|
356
|
+
redis.call("PEXPIRE", key, expireAt)
|
|
357
|
+
end
|
|
358
|
+
return {remaining, refilledAt + interval, effectiveLimit}
|
|
294
359
|
`;
|
|
295
360
|
var tokenBucketIdentifierNotFound = -1;
|
|
296
361
|
var tokenBucketRemainingTokensScript = `
|
|
297
362
|
local key = KEYS[1]
|
|
298
|
-
local
|
|
363
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
364
|
+
local maxTokens = tonumber(ARGV[1]) -- default maximum number of tokens
|
|
365
|
+
|
|
366
|
+
-- Check for dynamic limit
|
|
367
|
+
local effectiveLimit = maxTokens
|
|
368
|
+
if dynamicLimitKey ~= "" then
|
|
369
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
370
|
+
if dynamicLimit then
|
|
371
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
299
374
|
|
|
300
375
|
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
301
376
|
|
|
302
377
|
if bucket[1] == false then
|
|
303
|
-
return {
|
|
378
|
+
return {effectiveLimit, ${tokenBucketIdentifierNotFound}, effectiveLimit}
|
|
304
379
|
end
|
|
305
380
|
|
|
306
|
-
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
381
|
+
return {tonumber(bucket[2]), tonumber(bucket[1]), effectiveLimit}
|
|
307
382
|
`;
|
|
308
383
|
var cachedFixedWindowLimitScript = `
|
|
309
384
|
local key = KEYS[1]
|
|
@@ -377,7 +452,9 @@ var slidingWindowLimitScript2 = `
|
|
|
377
452
|
end
|
|
378
453
|
|
|
379
454
|
local percentageInCurrent = ( now % window) / window
|
|
380
|
-
|
|
455
|
+
|
|
456
|
+
-- Only check limit if not refunding (negative rate)
|
|
457
|
+
if incrementBy > 0 and requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow + incrementBy > tokens then
|
|
381
458
|
return {currentFields, previousFields, false}
|
|
382
459
|
end
|
|
383
460
|
|
|
@@ -445,31 +522,31 @@ var SCRIPTS = {
|
|
|
445
522
|
fixedWindow: {
|
|
446
523
|
limit: {
|
|
447
524
|
script: fixedWindowLimitScript,
|
|
448
|
-
hash: "
|
|
525
|
+
hash: "472e55443b62f60d0991028456c57815a387066d"
|
|
449
526
|
},
|
|
450
527
|
getRemaining: {
|
|
451
528
|
script: fixedWindowRemainingTokensScript,
|
|
452
|
-
hash: "
|
|
529
|
+
hash: "40515c9dd0a08f8584f5f9b593935f6a87c1c1c3"
|
|
453
530
|
}
|
|
454
531
|
},
|
|
455
532
|
slidingWindow: {
|
|
456
533
|
limit: {
|
|
457
534
|
script: slidingWindowLimitScript,
|
|
458
|
-
hash: "
|
|
535
|
+
hash: "977fb636fb5ceb7e98a96d1b3a1272ba018efdae"
|
|
459
536
|
},
|
|
460
537
|
getRemaining: {
|
|
461
538
|
script: slidingWindowRemainingTokensScript,
|
|
462
|
-
hash: "
|
|
539
|
+
hash: "ee3a3265fad822f83acad23f8a1e2f5c0b156b03"
|
|
463
540
|
}
|
|
464
541
|
},
|
|
465
542
|
tokenBucket: {
|
|
466
543
|
limit: {
|
|
467
544
|
script: tokenBucketLimitScript,
|
|
468
|
-
hash: "
|
|
545
|
+
hash: "b35c5bc0b7fdae7dd0573d4529911cabaf9d1d89"
|
|
469
546
|
},
|
|
470
547
|
getRemaining: {
|
|
471
548
|
script: tokenBucketRemainingTokensScript,
|
|
472
|
-
hash: "
|
|
549
|
+
hash: "deb03663e8af5a968deee895dd081be553d2611b"
|
|
473
550
|
}
|
|
474
551
|
},
|
|
475
552
|
cachedFixedWindow: {
|
|
@@ -497,7 +574,7 @@ var SCRIPTS = {
|
|
|
497
574
|
slidingWindow: {
|
|
498
575
|
limit: {
|
|
499
576
|
script: slidingWindowLimitScript2,
|
|
500
|
-
hash: "
|
|
577
|
+
hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
|
|
501
578
|
},
|
|
502
579
|
getRemaining: {
|
|
503
580
|
script: slidingWindowRemainingTokensScript2,
|
|
@@ -682,14 +759,20 @@ var Ratelimit = class {
|
|
|
682
759
|
analytics;
|
|
683
760
|
enableProtection;
|
|
684
761
|
denyListThreshold;
|
|
762
|
+
dynamicLimits;
|
|
685
763
|
constructor(config) {
|
|
686
764
|
this.ctx = config.ctx;
|
|
687
765
|
this.limiter = config.limiter;
|
|
688
766
|
this.timeout = config.timeout ?? 5e3;
|
|
689
|
-
this.prefix = config.prefix ??
|
|
767
|
+
this.prefix = config.prefix ?? DEFAULT_PREFIX;
|
|
768
|
+
this.dynamicLimits = config.dynamicLimits ?? false;
|
|
690
769
|
this.enableProtection = config.enableProtection ?? false;
|
|
691
770
|
this.denyListThreshold = config.denyListThreshold ?? 6;
|
|
692
771
|
this.primaryRedis = "redis" in this.ctx ? this.ctx.redis : this.ctx.regionContexts[0].redis;
|
|
772
|
+
if ("redis" in this.ctx) {
|
|
773
|
+
this.ctx.dynamicLimits = this.dynamicLimits;
|
|
774
|
+
this.ctx.prefix = this.prefix;
|
|
775
|
+
}
|
|
693
776
|
this.analytics = config.analytics ? new Analytics({
|
|
694
777
|
redis: this.primaryRedis,
|
|
695
778
|
prefix: this.prefix
|
|
@@ -803,9 +886,9 @@ var Ratelimit = class {
|
|
|
803
886
|
* Returns the remaining token count together with a reset timestamps
|
|
804
887
|
*
|
|
805
888
|
* @param identifier identifir to check
|
|
806
|
-
* @returns object with `remaining` and
|
|
807
|
-
* the remaining tokens
|
|
808
|
-
* tokens reset.
|
|
889
|
+
* @returns object with `remaining`, `reset`, and `limit` fields. `remaining` denotes
|
|
890
|
+
* the remaining tokens, `limit` is the effective limit (considering dynamic
|
|
891
|
+
* limits if enabled), and `reset` denotes the timestamp when the tokens reset.
|
|
809
892
|
*/
|
|
810
893
|
getRemaining = async (identifier) => {
|
|
811
894
|
const pattern = [this.prefix, identifier].join(":");
|
|
@@ -921,6 +1004,59 @@ var Ratelimit = class {
|
|
|
921
1004
|
const members = [identifier, req?.ip, req?.userAgent, req?.country];
|
|
922
1005
|
return members.filter(Boolean);
|
|
923
1006
|
};
|
|
1007
|
+
/**
|
|
1008
|
+
* Set a dynamic rate limit globally.
|
|
1009
|
+
*
|
|
1010
|
+
* When dynamicLimits is enabled, this limit will override the default limit
|
|
1011
|
+
* set in the constructor for all requests.
|
|
1012
|
+
*
|
|
1013
|
+
* @example
|
|
1014
|
+
* ```ts
|
|
1015
|
+
* const ratelimit = new Ratelimit({
|
|
1016
|
+
* redis: Redis.fromEnv(),
|
|
1017
|
+
* limiter: Ratelimit.slidingWindow(10, "10 s"),
|
|
1018
|
+
* dynamicLimits: true
|
|
1019
|
+
* });
|
|
1020
|
+
*
|
|
1021
|
+
* // Set global dynamic limit to 120 requests
|
|
1022
|
+
* await ratelimit.setDynamicLimit({ limit: 120 });
|
|
1023
|
+
*
|
|
1024
|
+
* // Disable dynamic limit (falls back to default)
|
|
1025
|
+
* await ratelimit.setDynamicLimit({ limit: false });
|
|
1026
|
+
* ```
|
|
1027
|
+
*
|
|
1028
|
+
* @param options.limit - The new rate limit to apply globally, or false to disable
|
|
1029
|
+
*/
|
|
1030
|
+
setDynamicLimit = async (options) => {
|
|
1031
|
+
if (!this.dynamicLimits) {
|
|
1032
|
+
throw new Error(
|
|
1033
|
+
"dynamicLimits must be enabled in the Ratelimit constructor to use setDynamicLimit()"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
const globalKey = `${this.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`;
|
|
1037
|
+
await (options.limit === false ? this.primaryRedis.del(globalKey) : this.primaryRedis.set(globalKey, options.limit));
|
|
1038
|
+
};
|
|
1039
|
+
/**
|
|
1040
|
+
* Get the current global dynamic rate limit.
|
|
1041
|
+
*
|
|
1042
|
+
* @example
|
|
1043
|
+
* ```ts
|
|
1044
|
+
* const { dynamicLimit } = await ratelimit.getDynamicLimit();
|
|
1045
|
+
* console.log(dynamicLimit); // 120 or null if not set
|
|
1046
|
+
* ```
|
|
1047
|
+
*
|
|
1048
|
+
* @returns Object containing the current global dynamic limit, or null if not set
|
|
1049
|
+
*/
|
|
1050
|
+
getDynamicLimit = async () => {
|
|
1051
|
+
if (!this.dynamicLimits) {
|
|
1052
|
+
throw new Error(
|
|
1053
|
+
"dynamicLimits must be enabled in the Ratelimit constructor to use getDynamicLimit()"
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
const globalKey = `${this.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`;
|
|
1057
|
+
const result = await this.primaryRedis.get(globalKey);
|
|
1058
|
+
return { dynamicLimit: result === null ? null : Number(result) };
|
|
1059
|
+
};
|
|
924
1060
|
};
|
|
925
1061
|
|
|
926
1062
|
// src/multi.ts
|
|
@@ -943,13 +1079,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
943
1079
|
limiter: config.limiter,
|
|
944
1080
|
timeout: config.timeout,
|
|
945
1081
|
analytics: config.analytics,
|
|
1082
|
+
dynamicLimits: config.dynamicLimits,
|
|
946
1083
|
ctx: {
|
|
947
1084
|
regionContexts: config.redis.map((redis) => ({
|
|
948
|
-
redis
|
|
1085
|
+
redis,
|
|
1086
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
949
1087
|
})),
|
|
950
1088
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
951
1089
|
}
|
|
952
1090
|
});
|
|
1091
|
+
if (config.dynamicLimits) {
|
|
1092
|
+
console.warn(
|
|
1093
|
+
"Warning: Dynamic limits are not yet supported for multi-region rate limiters. The dynamicLimits option will be ignored."
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
953
1096
|
}
|
|
954
1097
|
/**
|
|
955
1098
|
* Each request inside a fixed time increases a counter.
|
|
@@ -973,7 +1116,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
973
1116
|
const windowDuration = ms(window);
|
|
974
1117
|
return () => ({
|
|
975
1118
|
async limit(ctx, identifier, rate) {
|
|
976
|
-
|
|
1119
|
+
const requestId = randomId();
|
|
1120
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1121
|
+
const key = [identifier, bucket].join(":");
|
|
1122
|
+
const incrementBy = rate ?? 1;
|
|
1123
|
+
if (ctx.cache && incrementBy > 0) {
|
|
977
1124
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
978
1125
|
if (blocked) {
|
|
979
1126
|
return {
|
|
@@ -986,10 +1133,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
986
1133
|
};
|
|
987
1134
|
}
|
|
988
1135
|
}
|
|
989
|
-
const requestId = randomId();
|
|
990
|
-
const bucket = Math.floor(Date.now() / windowDuration);
|
|
991
|
-
const key = [identifier, bucket].join(":");
|
|
992
|
-
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
993
1136
|
const dbs = ctx.regionContexts.map((regionContext) => ({
|
|
994
1137
|
redis: regionContext.redis,
|
|
995
1138
|
request: safeEval(
|
|
@@ -1000,24 +1143,29 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1000
1143
|
)
|
|
1001
1144
|
}));
|
|
1002
1145
|
const firstResponse = await Promise.any(dbs.map((s) => s.request));
|
|
1003
|
-
const usedTokens = firstResponse.reduce(
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1146
|
+
const usedTokens = firstResponse.reduce(
|
|
1147
|
+
(accTokens, usedToken, index) => {
|
|
1148
|
+
let parsedToken = 0;
|
|
1149
|
+
if (index % 2) {
|
|
1150
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1151
|
+
}
|
|
1152
|
+
return accTokens + parsedToken;
|
|
1153
|
+
},
|
|
1154
|
+
0
|
|
1155
|
+
);
|
|
1010
1156
|
const remaining = tokens - usedTokens;
|
|
1011
1157
|
async function sync() {
|
|
1012
1158
|
const individualIDs = await Promise.all(dbs.map((s) => s.request));
|
|
1013
|
-
const allIDs = [
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1159
|
+
const allIDs = [
|
|
1160
|
+
...new Set(
|
|
1161
|
+
individualIDs.flat().reduce((acc, curr, index) => {
|
|
1162
|
+
if (index % 2 === 0) {
|
|
1163
|
+
acc.push(curr);
|
|
1164
|
+
}
|
|
1165
|
+
return acc;
|
|
1166
|
+
}, [])
|
|
1167
|
+
).values()
|
|
1168
|
+
];
|
|
1021
1169
|
for (const db of dbs) {
|
|
1022
1170
|
const usedDbTokensRequest = await db.request;
|
|
1023
1171
|
const usedDbTokens = usedDbTokensRequest.reduce(
|
|
@@ -1031,12 +1179,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1031
1179
|
0
|
|
1032
1180
|
);
|
|
1033
1181
|
const dbIdsRequest = await db.request;
|
|
1034
|
-
const dbIds = dbIdsRequest.reduce(
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1182
|
+
const dbIds = dbIdsRequest.reduce(
|
|
1183
|
+
(ids, currentId, index) => {
|
|
1184
|
+
if (index % 2 === 0) {
|
|
1185
|
+
ids.push(currentId);
|
|
1186
|
+
}
|
|
1187
|
+
return ids;
|
|
1188
|
+
},
|
|
1189
|
+
[]
|
|
1190
|
+
);
|
|
1040
1191
|
if (usedDbTokens >= tokens) {
|
|
1041
1192
|
continue;
|
|
1042
1193
|
}
|
|
@@ -1049,10 +1200,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1049
1200
|
}
|
|
1050
1201
|
}
|
|
1051
1202
|
}
|
|
1052
|
-
const success = remaining
|
|
1203
|
+
const success = remaining >= 0;
|
|
1053
1204
|
const reset = (bucket + 1) * windowDuration;
|
|
1054
|
-
if (ctx.cache
|
|
1055
|
-
|
|
1205
|
+
if (ctx.cache) {
|
|
1206
|
+
if (!success) {
|
|
1207
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1208
|
+
} else if (incrementBy < 0) {
|
|
1209
|
+
ctx.cache.pop(identifier);
|
|
1210
|
+
}
|
|
1056
1211
|
}
|
|
1057
1212
|
return {
|
|
1058
1213
|
success,
|
|
@@ -1075,16 +1230,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1075
1230
|
)
|
|
1076
1231
|
}));
|
|
1077
1232
|
const firstResponse = await Promise.any(dbs.map((s) => s.request));
|
|
1078
|
-
const usedTokens = firstResponse.reduce(
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1233
|
+
const usedTokens = firstResponse.reduce(
|
|
1234
|
+
(accTokens, usedToken, index) => {
|
|
1235
|
+
let parsedToken = 0;
|
|
1236
|
+
if (index % 2) {
|
|
1237
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1238
|
+
}
|
|
1239
|
+
return accTokens + parsedToken;
|
|
1240
|
+
},
|
|
1241
|
+
0
|
|
1242
|
+
);
|
|
1085
1243
|
return {
|
|
1086
1244
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1087
|
-
reset: (bucket + 1) * windowDuration
|
|
1245
|
+
reset: (bucket + 1) * windowDuration,
|
|
1246
|
+
limit: tokens
|
|
1088
1247
|
};
|
|
1089
1248
|
},
|
|
1090
1249
|
async resetTokens(ctx, identifier) {
|
|
@@ -1092,14 +1251,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1092
1251
|
if (ctx.cache) {
|
|
1093
1252
|
ctx.cache.pop(identifier);
|
|
1094
1253
|
}
|
|
1095
|
-
await Promise.all(
|
|
1096
|
-
|
|
1097
|
-
regionContext,
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
[null]
|
|
1101
|
-
);
|
|
1102
|
-
}));
|
|
1254
|
+
await Promise.all(
|
|
1255
|
+
ctx.regionContexts.map((regionContext) => {
|
|
1256
|
+
safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
|
|
1257
|
+
})
|
|
1258
|
+
);
|
|
1103
1259
|
}
|
|
1104
1260
|
});
|
|
1105
1261
|
}
|
|
@@ -1124,7 +1280,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1124
1280
|
const windowDuration = ms(window);
|
|
1125
1281
|
return () => ({
|
|
1126
1282
|
async limit(ctx, identifier, rate) {
|
|
1127
|
-
|
|
1283
|
+
const requestId = randomId();
|
|
1284
|
+
const now = Date.now();
|
|
1285
|
+
const currentWindow = Math.floor(now / windowSize);
|
|
1286
|
+
const currentKey = [identifier, currentWindow].join(":");
|
|
1287
|
+
const previousWindow = currentWindow - 1;
|
|
1288
|
+
const previousKey = [identifier, previousWindow].join(":");
|
|
1289
|
+
const incrementBy = rate ?? 1;
|
|
1290
|
+
if (ctx.cache && incrementBy > 0) {
|
|
1128
1291
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1129
1292
|
if (blocked) {
|
|
1130
1293
|
return {
|
|
@@ -1137,13 +1300,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1137
1300
|
};
|
|
1138
1301
|
}
|
|
1139
1302
|
}
|
|
1140
|
-
const requestId = randomId();
|
|
1141
|
-
const now = Date.now();
|
|
1142
|
-
const currentWindow = Math.floor(now / windowSize);
|
|
1143
|
-
const currentKey = [identifier, currentWindow].join(":");
|
|
1144
|
-
const previousWindow = currentWindow - 1;
|
|
1145
|
-
const previousKey = [identifier, previousWindow].join(":");
|
|
1146
|
-
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1147
1303
|
const dbs = ctx.regionContexts.map((regionContext) => ({
|
|
1148
1304
|
redis: regionContext.redis,
|
|
1149
1305
|
request: safeEval(
|
|
@@ -1155,37 +1311,49 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1155
1311
|
)
|
|
1156
1312
|
}));
|
|
1157
1313
|
const percentageInCurrent = now % windowDuration / windowDuration;
|
|
1158
|
-
const [current, previous, success] = await Promise.any(
|
|
1314
|
+
const [current, previous, success] = await Promise.any(
|
|
1315
|
+
dbs.map((s) => s.request)
|
|
1316
|
+
);
|
|
1159
1317
|
if (success) {
|
|
1160
1318
|
current.push(requestId, incrementBy.toString());
|
|
1161
1319
|
}
|
|
1162
|
-
const previousUsedTokens = previous.reduce(
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1320
|
+
const previousUsedTokens = previous.reduce(
|
|
1321
|
+
(accTokens, usedToken, index) => {
|
|
1322
|
+
let parsedToken = 0;
|
|
1323
|
+
if (index % 2) {
|
|
1324
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1325
|
+
}
|
|
1326
|
+
return accTokens + parsedToken;
|
|
1327
|
+
},
|
|
1328
|
+
0
|
|
1329
|
+
);
|
|
1330
|
+
const currentUsedTokens = current.reduce(
|
|
1331
|
+
(accTokens, usedToken, index) => {
|
|
1332
|
+
let parsedToken = 0;
|
|
1333
|
+
if (index % 2) {
|
|
1334
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1335
|
+
}
|
|
1336
|
+
return accTokens + parsedToken;
|
|
1337
|
+
},
|
|
1338
|
+
0
|
|
1339
|
+
);
|
|
1340
|
+
const previousPartialUsed = Math.ceil(
|
|
1341
|
+
previousUsedTokens * (1 - percentageInCurrent)
|
|
1342
|
+
);
|
|
1177
1343
|
const usedTokens = previousPartialUsed + currentUsedTokens;
|
|
1178
1344
|
const remaining = tokens - usedTokens;
|
|
1179
1345
|
async function sync() {
|
|
1180
1346
|
const res = await Promise.all(dbs.map((s) => s.request));
|
|
1181
|
-
const allCurrentIds = [
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1347
|
+
const allCurrentIds = [
|
|
1348
|
+
...new Set(
|
|
1349
|
+
res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
|
|
1350
|
+
if (index % 2 === 0) {
|
|
1351
|
+
acc.push(curr);
|
|
1352
|
+
}
|
|
1353
|
+
return acc;
|
|
1354
|
+
}, [])
|
|
1355
|
+
).values()
|
|
1356
|
+
];
|
|
1189
1357
|
for (const db of dbs) {
|
|
1190
1358
|
const [current2, _previous, _success] = await db.request;
|
|
1191
1359
|
const dbIds = current2.reduce((ids, currentId, index) => {
|
|
@@ -1194,13 +1362,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1194
1362
|
}
|
|
1195
1363
|
return ids;
|
|
1196
1364
|
}, []);
|
|
1197
|
-
const usedDbTokens = current2.reduce(
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1365
|
+
const usedDbTokens = current2.reduce(
|
|
1366
|
+
(accTokens, usedToken, index) => {
|
|
1367
|
+
let parsedToken = 0;
|
|
1368
|
+
if (index % 2) {
|
|
1369
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1370
|
+
}
|
|
1371
|
+
return accTokens + parsedToken;
|
|
1372
|
+
},
|
|
1373
|
+
0
|
|
1374
|
+
);
|
|
1204
1375
|
if (usedDbTokens >= tokens) {
|
|
1205
1376
|
continue;
|
|
1206
1377
|
}
|
|
@@ -1214,8 +1385,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1214
1385
|
}
|
|
1215
1386
|
}
|
|
1216
1387
|
const reset = (currentWindow + 1) * windowDuration;
|
|
1217
|
-
if (ctx.cache
|
|
1218
|
-
|
|
1388
|
+
if (ctx.cache) {
|
|
1389
|
+
if (!success) {
|
|
1390
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1391
|
+
} else if (incrementBy < 0) {
|
|
1392
|
+
ctx.cache.pop(identifier);
|
|
1393
|
+
}
|
|
1219
1394
|
}
|
|
1220
1395
|
return {
|
|
1221
1396
|
success: Boolean(success),
|
|
@@ -1244,7 +1419,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1244
1419
|
const usedTokens = await Promise.any(dbs.map((s) => s.request));
|
|
1245
1420
|
return {
|
|
1246
1421
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1247
|
-
reset: (currentWindow + 1) * windowSize
|
|
1422
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1423
|
+
limit: tokens
|
|
1248
1424
|
};
|
|
1249
1425
|
},
|
|
1250
1426
|
async resetTokens(ctx, identifier) {
|
|
@@ -1252,14 +1428,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1252
1428
|
if (ctx.cache) {
|
|
1253
1429
|
ctx.cache.pop(identifier);
|
|
1254
1430
|
}
|
|
1255
|
-
await Promise.all(
|
|
1256
|
-
|
|
1257
|
-
regionContext,
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
[null]
|
|
1261
|
-
);
|
|
1262
|
-
}));
|
|
1431
|
+
await Promise.all(
|
|
1432
|
+
ctx.regionContexts.map((regionContext) => {
|
|
1433
|
+
safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
|
|
1434
|
+
})
|
|
1435
|
+
);
|
|
1263
1436
|
}
|
|
1264
1437
|
});
|
|
1265
1438
|
}
|
|
@@ -1277,11 +1450,13 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1277
1450
|
timeout: config.timeout,
|
|
1278
1451
|
analytics: config.analytics,
|
|
1279
1452
|
ctx: {
|
|
1280
|
-
redis: config.redis
|
|
1453
|
+
redis: config.redis,
|
|
1454
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
1281
1455
|
},
|
|
1282
1456
|
ephemeralCache: config.ephemeralCache,
|
|
1283
1457
|
enableProtection: config.enableProtection,
|
|
1284
|
-
denyListThreshold: config.denyListThreshold
|
|
1458
|
+
denyListThreshold: config.denyListThreshold,
|
|
1459
|
+
dynamicLimits: config.dynamicLimits
|
|
1285
1460
|
});
|
|
1286
1461
|
}
|
|
1287
1462
|
/**
|
|
@@ -1308,7 +1483,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1308
1483
|
async limit(ctx, identifier, rate) {
|
|
1309
1484
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1310
1485
|
const key = [identifier, bucket].join(":");
|
|
1311
|
-
|
|
1486
|
+
const incrementBy = rate ?? 1;
|
|
1487
|
+
if (ctx.cache && incrementBy > 0) {
|
|
1312
1488
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1313
1489
|
if (blocked) {
|
|
1314
1490
|
return {
|
|
@@ -1321,22 +1497,26 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1321
1497
|
};
|
|
1322
1498
|
}
|
|
1323
1499
|
}
|
|
1324
|
-
const
|
|
1325
|
-
const usedTokensAfterUpdate = await safeEval(
|
|
1500
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1501
|
+
const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
|
|
1326
1502
|
ctx,
|
|
1327
1503
|
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1328
|
-
[key],
|
|
1329
|
-
[windowDuration, incrementBy]
|
|
1504
|
+
[key, dynamicLimitKey],
|
|
1505
|
+
[tokens, windowDuration, incrementBy]
|
|
1330
1506
|
);
|
|
1331
|
-
const success = usedTokensAfterUpdate <=
|
|
1332
|
-
const remainingTokens = Math.max(0,
|
|
1507
|
+
const success = usedTokensAfterUpdate <= effectiveLimit;
|
|
1508
|
+
const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
|
|
1333
1509
|
const reset = (bucket + 1) * windowDuration;
|
|
1334
|
-
if (ctx.cache
|
|
1335
|
-
|
|
1510
|
+
if (ctx.cache) {
|
|
1511
|
+
if (!success) {
|
|
1512
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1513
|
+
} else if (incrementBy < 0) {
|
|
1514
|
+
ctx.cache.pop(identifier);
|
|
1515
|
+
}
|
|
1336
1516
|
}
|
|
1337
1517
|
return {
|
|
1338
1518
|
success,
|
|
1339
|
-
limit:
|
|
1519
|
+
limit: effectiveLimit,
|
|
1340
1520
|
remaining: remainingTokens,
|
|
1341
1521
|
reset,
|
|
1342
1522
|
pending: Promise.resolve()
|
|
@@ -1345,15 +1525,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1345
1525
|
async getRemaining(ctx, identifier) {
|
|
1346
1526
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1347
1527
|
const key = [identifier, bucket].join(":");
|
|
1348
|
-
const
|
|
1528
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1529
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1349
1530
|
ctx,
|
|
1350
1531
|
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1351
|
-
[key],
|
|
1352
|
-
[
|
|
1532
|
+
[key, dynamicLimitKey],
|
|
1533
|
+
[tokens]
|
|
1353
1534
|
);
|
|
1354
1535
|
return {
|
|
1355
|
-
remaining: Math.max(0,
|
|
1356
|
-
reset: (bucket + 1) * windowDuration
|
|
1536
|
+
remaining: Math.max(0, remaining),
|
|
1537
|
+
reset: (bucket + 1) * windowDuration,
|
|
1538
|
+
limit: effectiveLimit
|
|
1357
1539
|
};
|
|
1358
1540
|
},
|
|
1359
1541
|
async resetTokens(ctx, identifier) {
|
|
@@ -1395,7 +1577,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1395
1577
|
const currentKey = [identifier, currentWindow].join(":");
|
|
1396
1578
|
const previousWindow = currentWindow - 1;
|
|
1397
1579
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1398
|
-
|
|
1580
|
+
const incrementBy = rate ?? 1;
|
|
1581
|
+
if (ctx.cache && incrementBy > 0) {
|
|
1399
1582
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1400
1583
|
if (blocked) {
|
|
1401
1584
|
return {
|
|
@@ -1408,21 +1591,25 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1408
1591
|
};
|
|
1409
1592
|
}
|
|
1410
1593
|
}
|
|
1411
|
-
const
|
|
1412
|
-
const remainingTokens = await safeEval(
|
|
1594
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1595
|
+
const [remainingTokens, effectiveLimit] = await safeEval(
|
|
1413
1596
|
ctx,
|
|
1414
1597
|
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1415
|
-
[currentKey, previousKey],
|
|
1598
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1416
1599
|
[tokens, now, windowSize, incrementBy]
|
|
1417
1600
|
);
|
|
1418
1601
|
const success = remainingTokens >= 0;
|
|
1419
1602
|
const reset = (currentWindow + 1) * windowSize;
|
|
1420
|
-
if (ctx.cache
|
|
1421
|
-
|
|
1603
|
+
if (ctx.cache) {
|
|
1604
|
+
if (!success) {
|
|
1605
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1606
|
+
} else if (incrementBy < 0) {
|
|
1607
|
+
ctx.cache.pop(identifier);
|
|
1608
|
+
}
|
|
1422
1609
|
}
|
|
1423
1610
|
return {
|
|
1424
1611
|
success,
|
|
1425
|
-
limit:
|
|
1612
|
+
limit: effectiveLimit,
|
|
1426
1613
|
remaining: Math.max(0, remainingTokens),
|
|
1427
1614
|
reset,
|
|
1428
1615
|
pending: Promise.resolve()
|
|
@@ -1434,15 +1621,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1434
1621
|
const currentKey = [identifier, currentWindow].join(":");
|
|
1435
1622
|
const previousWindow = currentWindow - 1;
|
|
1436
1623
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1437
|
-
const
|
|
1624
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1625
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1438
1626
|
ctx,
|
|
1439
1627
|
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1440
|
-
[currentKey, previousKey],
|
|
1441
|
-
[now, windowSize]
|
|
1628
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1629
|
+
[tokens, now, windowSize]
|
|
1442
1630
|
);
|
|
1443
1631
|
return {
|
|
1444
|
-
remaining: Math.max(0,
|
|
1445
|
-
reset: (currentWindow + 1) * windowSize
|
|
1632
|
+
remaining: Math.max(0, remaining),
|
|
1633
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1634
|
+
limit: effectiveLimit
|
|
1446
1635
|
};
|
|
1447
1636
|
},
|
|
1448
1637
|
async resetTokens(ctx, identifier) {
|
|
@@ -1476,7 +1665,9 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1476
1665
|
const intervalDuration = ms(interval);
|
|
1477
1666
|
return () => ({
|
|
1478
1667
|
async limit(ctx, identifier, rate) {
|
|
1479
|
-
|
|
1668
|
+
const now = Date.now();
|
|
1669
|
+
const incrementBy = rate ?? 1;
|
|
1670
|
+
if (ctx.cache && incrementBy > 0) {
|
|
1480
1671
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1481
1672
|
if (blocked) {
|
|
1482
1673
|
return {
|
|
@@ -1489,38 +1680,43 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1489
1680
|
};
|
|
1490
1681
|
}
|
|
1491
1682
|
}
|
|
1492
|
-
const
|
|
1493
|
-
const
|
|
1494
|
-
const [remaining, reset] = await safeEval(
|
|
1683
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1684
|
+
const [remaining, reset, effectiveLimit] = await safeEval(
|
|
1495
1685
|
ctx,
|
|
1496
1686
|
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1497
|
-
[identifier],
|
|
1687
|
+
[identifier, dynamicLimitKey],
|
|
1498
1688
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1499
1689
|
);
|
|
1500
1690
|
const success = remaining >= 0;
|
|
1501
|
-
if (ctx.cache
|
|
1502
|
-
|
|
1691
|
+
if (ctx.cache) {
|
|
1692
|
+
if (!success) {
|
|
1693
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1694
|
+
} else if (incrementBy < 0) {
|
|
1695
|
+
ctx.cache.pop(identifier);
|
|
1696
|
+
}
|
|
1503
1697
|
}
|
|
1504
1698
|
return {
|
|
1505
1699
|
success,
|
|
1506
|
-
limit:
|
|
1507
|
-
remaining,
|
|
1700
|
+
limit: effectiveLimit,
|
|
1701
|
+
remaining: Math.max(0, remaining),
|
|
1508
1702
|
reset,
|
|
1509
1703
|
pending: Promise.resolve()
|
|
1510
1704
|
};
|
|
1511
1705
|
},
|
|
1512
1706
|
async getRemaining(ctx, identifier) {
|
|
1513
|
-
const
|
|
1707
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1708
|
+
const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
|
|
1514
1709
|
ctx,
|
|
1515
1710
|
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1516
|
-
[identifier],
|
|
1711
|
+
[identifier, dynamicLimitKey],
|
|
1517
1712
|
[maxTokens]
|
|
1518
1713
|
);
|
|
1519
1714
|
const freshRefillAt = Date.now() + intervalDuration;
|
|
1520
1715
|
const identifierRefillsAt = refilledAt + intervalDuration;
|
|
1521
1716
|
return {
|
|
1522
|
-
remaining: remainingTokens,
|
|
1523
|
-
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
|
|
1717
|
+
remaining: Math.max(0, remainingTokens),
|
|
1718
|
+
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
|
|
1719
|
+
limit: effectiveLimit
|
|
1524
1720
|
};
|
|
1525
1721
|
},
|
|
1526
1722
|
async resetTokens(ctx, identifier) {
|
|
@@ -1568,13 +1764,18 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1568
1764
|
if (!ctx.cache) {
|
|
1569
1765
|
throw new Error("This algorithm requires a cache");
|
|
1570
1766
|
}
|
|
1767
|
+
if (ctx.dynamicLimits) {
|
|
1768
|
+
console.warn(
|
|
1769
|
+
"Warning: Dynamic limits are not yet supported for cachedFixedWindow algorithm. The dynamicLimits option will be ignored."
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1571
1772
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1572
1773
|
const key = [identifier, bucket].join(":");
|
|
1573
1774
|
const reset = (bucket + 1) * windowDuration;
|
|
1574
|
-
const incrementBy = rate
|
|
1775
|
+
const incrementBy = rate ?? 1;
|
|
1575
1776
|
const hit = typeof ctx.cache.get(key) === "number";
|
|
1576
1777
|
if (hit) {
|
|
1577
|
-
const cachedTokensAfterUpdate = ctx.cache.incr(key);
|
|
1778
|
+
const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
|
|
1578
1779
|
const success = cachedTokensAfterUpdate < tokens;
|
|
1579
1780
|
const pending = success ? safeEval(
|
|
1580
1781
|
ctx,
|
|
@@ -1617,7 +1818,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1617
1818
|
const cachedUsedTokens = ctx.cache.get(key) ?? 0;
|
|
1618
1819
|
return {
|
|
1619
1820
|
remaining: Math.max(0, tokens - cachedUsedTokens),
|
|
1620
|
-
reset: (bucket + 1) * windowDuration
|
|
1821
|
+
reset: (bucket + 1) * windowDuration,
|
|
1822
|
+
limit: tokens
|
|
1621
1823
|
};
|
|
1622
1824
|
}
|
|
1623
1825
|
const usedTokens = await safeEval(
|
|
@@ -1628,7 +1830,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1628
1830
|
);
|
|
1629
1831
|
return {
|
|
1630
1832
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1631
|
-
reset: (bucket + 1) * windowDuration
|
|
1833
|
+
reset: (bucket + 1) * windowDuration,
|
|
1834
|
+
limit: tokens
|
|
1632
1835
|
};
|
|
1633
1836
|
},
|
|
1634
1837
|
async resetTokens(ctx, identifier) {
|