@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.mjs
CHANGED
|
@@ -92,9 +92,9 @@ var Cache = class {
|
|
|
92
92
|
get(key) {
|
|
93
93
|
return this.cache.get(key) || null;
|
|
94
94
|
}
|
|
95
|
-
incr(key) {
|
|
95
|
+
incr(key, incrementAmount = 1) {
|
|
96
96
|
let value = this.cache.get(key) ?? 0;
|
|
97
|
-
value +=
|
|
97
|
+
value += incrementAmount;
|
|
98
98
|
this.cache.set(key, value);
|
|
99
99
|
return value;
|
|
100
100
|
}
|
|
@@ -109,6 +109,10 @@ var Cache = class {
|
|
|
109
109
|
}
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// src/constants.ts
|
|
113
|
+
var DYNAMIC_LIMIT_KEY_SUFFIX = ":dynamic:global";
|
|
114
|
+
var DEFAULT_PREFIX = "@upstash/ratelimit";
|
|
115
|
+
|
|
112
116
|
// src/duration.ts
|
|
113
117
|
function ms(d) {
|
|
114
118
|
const match = d.match(/^(\d+)\s?(ms|s|m|h|d)$/);
|
|
@@ -154,8 +158,19 @@ var safeEval = async (ctx, script, keys, args) => {
|
|
|
154
158
|
// src/lua-scripts/single.ts
|
|
155
159
|
var fixedWindowLimitScript = `
|
|
156
160
|
local key = KEYS[1]
|
|
157
|
-
local
|
|
158
|
-
local
|
|
161
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
162
|
+
local tokens = tonumber(ARGV[1]) -- default limit
|
|
163
|
+
local window = ARGV[2]
|
|
164
|
+
local incrementBy = ARGV[3] -- increment rate per request at a given value, default is 1
|
|
165
|
+
|
|
166
|
+
-- Check for dynamic limit
|
|
167
|
+
local effectiveLimit = tokens
|
|
168
|
+
if dynamicLimitKey ~= "" then
|
|
169
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
170
|
+
if dynamicLimit then
|
|
171
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
159
174
|
|
|
160
175
|
local r = redis.call("INCRBY", key, incrementBy)
|
|
161
176
|
if r == tonumber(incrementBy) then
|
|
@@ -164,25 +179,47 @@ var fixedWindowLimitScript = `
|
|
|
164
179
|
redis.call("PEXPIRE", key, window)
|
|
165
180
|
end
|
|
166
181
|
|
|
167
|
-
return r
|
|
182
|
+
return {r, effectiveLimit}
|
|
168
183
|
`;
|
|
169
184
|
var fixedWindowRemainingTokensScript = `
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
local key = KEYS[1]
|
|
186
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
187
|
+
local tokens = tonumber(ARGV[1]) -- default limit
|
|
172
188
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
197
|
+
|
|
198
|
+
local value = redis.call('GET', key)
|
|
199
|
+
local usedTokens = 0
|
|
200
|
+
if value then
|
|
201
|
+
usedTokens = tonumber(value)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
return {effectiveLimit - usedTokens, effectiveLimit}
|
|
205
|
+
`;
|
|
179
206
|
var slidingWindowLimitScript = `
|
|
180
207
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
181
208
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
182
|
-
local
|
|
209
|
+
local dynamicLimitKey = KEYS[3] -- optional: key for dynamic limit in redis
|
|
210
|
+
local tokens = tonumber(ARGV[1]) -- default tokens per window
|
|
183
211
|
local now = ARGV[2] -- current timestamp in milliseconds
|
|
184
212
|
local window = ARGV[3] -- interval in milliseconds
|
|
185
|
-
local incrementBy = ARGV[4]
|
|
213
|
+
local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
|
|
214
|
+
|
|
215
|
+
-- Check for dynamic limit
|
|
216
|
+
local effectiveLimit = tokens
|
|
217
|
+
if dynamicLimitKey ~= "" then
|
|
218
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
219
|
+
if dynamicLimit then
|
|
220
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
186
223
|
|
|
187
224
|
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
188
225
|
if requestsInCurrentWindow == false then
|
|
@@ -196,23 +233,36 @@ var slidingWindowLimitScript = `
|
|
|
196
233
|
local percentageInCurrent = ( now % window ) / window
|
|
197
234
|
-- weighted requests to consider from the previous window
|
|
198
235
|
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
199
|
-
|
|
200
|
-
|
|
236
|
+
|
|
237
|
+
-- Only check limit if not refunding (negative rate)
|
|
238
|
+
if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= effectiveLimit then
|
|
239
|
+
return {-1, effectiveLimit}
|
|
201
240
|
end
|
|
202
241
|
|
|
203
242
|
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
204
|
-
if newValue ==
|
|
243
|
+
if newValue == incrementBy then
|
|
205
244
|
-- The first time this key is set, the value will be equal to incrementBy.
|
|
206
245
|
-- So we only need the expire command once
|
|
207
246
|
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
208
247
|
end
|
|
209
|
-
return
|
|
248
|
+
return {effectiveLimit - ( newValue + requestsInPreviousWindow ), effectiveLimit}
|
|
210
249
|
`;
|
|
211
250
|
var slidingWindowRemainingTokensScript = `
|
|
212
251
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
213
252
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
214
|
-
local
|
|
215
|
-
local
|
|
253
|
+
local dynamicLimitKey = KEYS[3] -- optional: key for dynamic limit in redis
|
|
254
|
+
local tokens = tonumber(ARGV[1]) -- default tokens per window
|
|
255
|
+
local now = ARGV[2] -- current timestamp in milliseconds
|
|
256
|
+
local window = ARGV[3] -- interval in milliseconds
|
|
257
|
+
|
|
258
|
+
-- Check for dynamic limit
|
|
259
|
+
local effectiveLimit = tokens
|
|
260
|
+
if dynamicLimitKey ~= "" then
|
|
261
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
262
|
+
if dynamicLimit then
|
|
263
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
216
266
|
|
|
217
267
|
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
218
268
|
if requestsInCurrentWindow == false then
|
|
@@ -228,15 +278,26 @@ var slidingWindowRemainingTokensScript = `
|
|
|
228
278
|
-- weighted requests to consider from the previous window
|
|
229
279
|
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
230
280
|
|
|
231
|
-
|
|
281
|
+
local usedTokens = requestsInPreviousWindow + requestsInCurrentWindow
|
|
282
|
+
return {effectiveLimit - usedTokens, effectiveLimit}
|
|
232
283
|
`;
|
|
233
284
|
var tokenBucketLimitScript = `
|
|
234
285
|
local key = KEYS[1] -- identifier including prefixes
|
|
235
|
-
local
|
|
286
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
287
|
+
local maxTokens = tonumber(ARGV[1]) -- default maximum number of tokens
|
|
236
288
|
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
237
289
|
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
238
290
|
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
239
291
|
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
292
|
+
|
|
293
|
+
-- Check for dynamic limit
|
|
294
|
+
local effectiveLimit = maxTokens
|
|
295
|
+
if dynamicLimitKey ~= "" then
|
|
296
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
297
|
+
if dynamicLimit then
|
|
298
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
240
301
|
|
|
241
302
|
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
242
303
|
|
|
@@ -245,7 +306,7 @@ var tokenBucketLimitScript = `
|
|
|
245
306
|
|
|
246
307
|
if bucket[1] == false then
|
|
247
308
|
refilledAt = now
|
|
248
|
-
tokens =
|
|
309
|
+
tokens = effectiveLimit
|
|
249
310
|
else
|
|
250
311
|
refilledAt = tonumber(bucket[1])
|
|
251
312
|
tokens = tonumber(bucket[2])
|
|
@@ -253,34 +314,48 @@ var tokenBucketLimitScript = `
|
|
|
253
314
|
|
|
254
315
|
if now >= refilledAt + interval then
|
|
255
316
|
local numRefills = math.floor((now - refilledAt) / interval)
|
|
256
|
-
tokens = math.min(
|
|
317
|
+
tokens = math.min(effectiveLimit, tokens + numRefills * refillRate)
|
|
257
318
|
|
|
258
319
|
refilledAt = refilledAt + numRefills * interval
|
|
259
320
|
end
|
|
260
321
|
|
|
261
|
-
if tokens
|
|
262
|
-
|
|
322
|
+
-- Only reject if tokens are 0 and we're consuming (not refunding)
|
|
323
|
+
if tokens == 0 and incrementBy > 0 then
|
|
324
|
+
return {-1, refilledAt + interval, effectiveLimit}
|
|
263
325
|
end
|
|
264
326
|
|
|
265
327
|
local remaining = tokens - incrementBy
|
|
266
|
-
local expireAt = math.ceil(((
|
|
328
|
+
local expireAt = math.ceil(((effectiveLimit - remaining) / refillRate)) * interval
|
|
267
329
|
|
|
268
330
|
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
269
|
-
|
|
270
|
-
|
|
331
|
+
|
|
332
|
+
if (expireAt > 0) then
|
|
333
|
+
redis.call("PEXPIRE", key, expireAt)
|
|
334
|
+
end
|
|
335
|
+
return {remaining, refilledAt + interval, effectiveLimit}
|
|
271
336
|
`;
|
|
272
337
|
var tokenBucketIdentifierNotFound = -1;
|
|
273
338
|
var tokenBucketRemainingTokensScript = `
|
|
274
339
|
local key = KEYS[1]
|
|
275
|
-
local
|
|
340
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
341
|
+
local maxTokens = tonumber(ARGV[1]) -- default maximum number of tokens
|
|
342
|
+
|
|
343
|
+
-- Check for dynamic limit
|
|
344
|
+
local effectiveLimit = maxTokens
|
|
345
|
+
if dynamicLimitKey ~= "" then
|
|
346
|
+
local dynamicLimit = redis.call("GET", dynamicLimitKey)
|
|
347
|
+
if dynamicLimit then
|
|
348
|
+
effectiveLimit = tonumber(dynamicLimit)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
276
351
|
|
|
277
352
|
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
278
353
|
|
|
279
354
|
if bucket[1] == false then
|
|
280
|
-
return {
|
|
355
|
+
return {effectiveLimit, ${tokenBucketIdentifierNotFound}, effectiveLimit}
|
|
281
356
|
end
|
|
282
357
|
|
|
283
|
-
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
358
|
+
return {tonumber(bucket[2]), tonumber(bucket[1]), effectiveLimit}
|
|
284
359
|
`;
|
|
285
360
|
var cachedFixedWindowLimitScript = `
|
|
286
361
|
local key = KEYS[1]
|
|
@@ -354,7 +429,9 @@ var slidingWindowLimitScript2 = `
|
|
|
354
429
|
end
|
|
355
430
|
|
|
356
431
|
local percentageInCurrent = ( now % window) / window
|
|
357
|
-
|
|
432
|
+
|
|
433
|
+
-- Only check limit if not refunding (negative rate)
|
|
434
|
+
if incrementBy > 0 and requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow + incrementBy > tokens then
|
|
358
435
|
return {currentFields, previousFields, false}
|
|
359
436
|
end
|
|
360
437
|
|
|
@@ -422,31 +499,31 @@ var SCRIPTS = {
|
|
|
422
499
|
fixedWindow: {
|
|
423
500
|
limit: {
|
|
424
501
|
script: fixedWindowLimitScript,
|
|
425
|
-
hash: "
|
|
502
|
+
hash: "472e55443b62f60d0991028456c57815a387066d"
|
|
426
503
|
},
|
|
427
504
|
getRemaining: {
|
|
428
505
|
script: fixedWindowRemainingTokensScript,
|
|
429
|
-
hash: "
|
|
506
|
+
hash: "40515c9dd0a08f8584f5f9b593935f6a87c1c1c3"
|
|
430
507
|
}
|
|
431
508
|
},
|
|
432
509
|
slidingWindow: {
|
|
433
510
|
limit: {
|
|
434
511
|
script: slidingWindowLimitScript,
|
|
435
|
-
hash: "
|
|
512
|
+
hash: "977fb636fb5ceb7e98a96d1b3a1272ba018efdae"
|
|
436
513
|
},
|
|
437
514
|
getRemaining: {
|
|
438
515
|
script: slidingWindowRemainingTokensScript,
|
|
439
|
-
hash: "
|
|
516
|
+
hash: "ee3a3265fad822f83acad23f8a1e2f5c0b156b03"
|
|
440
517
|
}
|
|
441
518
|
},
|
|
442
519
|
tokenBucket: {
|
|
443
520
|
limit: {
|
|
444
521
|
script: tokenBucketLimitScript,
|
|
445
|
-
hash: "
|
|
522
|
+
hash: "b35c5bc0b7fdae7dd0573d4529911cabaf9d1d89"
|
|
446
523
|
},
|
|
447
524
|
getRemaining: {
|
|
448
525
|
script: tokenBucketRemainingTokensScript,
|
|
449
|
-
hash: "
|
|
526
|
+
hash: "deb03663e8af5a968deee895dd081be553d2611b"
|
|
450
527
|
}
|
|
451
528
|
},
|
|
452
529
|
cachedFixedWindow: {
|
|
@@ -474,7 +551,7 @@ var SCRIPTS = {
|
|
|
474
551
|
slidingWindow: {
|
|
475
552
|
limit: {
|
|
476
553
|
script: slidingWindowLimitScript2,
|
|
477
|
-
hash: "
|
|
554
|
+
hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
|
|
478
555
|
},
|
|
479
556
|
getRemaining: {
|
|
480
557
|
script: slidingWindowRemainingTokensScript2,
|
|
@@ -659,14 +736,20 @@ var Ratelimit = class {
|
|
|
659
736
|
analytics;
|
|
660
737
|
enableProtection;
|
|
661
738
|
denyListThreshold;
|
|
739
|
+
dynamicLimits;
|
|
662
740
|
constructor(config) {
|
|
663
741
|
this.ctx = config.ctx;
|
|
664
742
|
this.limiter = config.limiter;
|
|
665
743
|
this.timeout = config.timeout ?? 5e3;
|
|
666
|
-
this.prefix = config.prefix ??
|
|
744
|
+
this.prefix = config.prefix ?? DEFAULT_PREFIX;
|
|
745
|
+
this.dynamicLimits = config.dynamicLimits ?? false;
|
|
667
746
|
this.enableProtection = config.enableProtection ?? false;
|
|
668
747
|
this.denyListThreshold = config.denyListThreshold ?? 6;
|
|
669
748
|
this.primaryRedis = "redis" in this.ctx ? this.ctx.redis : this.ctx.regionContexts[0].redis;
|
|
749
|
+
if ("redis" in this.ctx) {
|
|
750
|
+
this.ctx.dynamicLimits = this.dynamicLimits;
|
|
751
|
+
this.ctx.prefix = this.prefix;
|
|
752
|
+
}
|
|
670
753
|
this.analytics = config.analytics ? new Analytics({
|
|
671
754
|
redis: this.primaryRedis,
|
|
672
755
|
prefix: this.prefix
|
|
@@ -780,9 +863,9 @@ var Ratelimit = class {
|
|
|
780
863
|
* Returns the remaining token count together with a reset timestamps
|
|
781
864
|
*
|
|
782
865
|
* @param identifier identifir to check
|
|
783
|
-
* @returns object with `remaining` and
|
|
784
|
-
* the remaining tokens
|
|
785
|
-
* tokens reset.
|
|
866
|
+
* @returns object with `remaining`, `reset`, and `limit` fields. `remaining` denotes
|
|
867
|
+
* the remaining tokens, `limit` is the effective limit (considering dynamic
|
|
868
|
+
* limits if enabled), and `reset` denotes the timestamp when the tokens reset.
|
|
786
869
|
*/
|
|
787
870
|
getRemaining = async (identifier) => {
|
|
788
871
|
const pattern = [this.prefix, identifier].join(":");
|
|
@@ -898,6 +981,59 @@ var Ratelimit = class {
|
|
|
898
981
|
const members = [identifier, req?.ip, req?.userAgent, req?.country];
|
|
899
982
|
return members.filter(Boolean);
|
|
900
983
|
};
|
|
984
|
+
/**
|
|
985
|
+
* Set a dynamic rate limit globally.
|
|
986
|
+
*
|
|
987
|
+
* When dynamicLimits is enabled, this limit will override the default limit
|
|
988
|
+
* set in the constructor for all requests.
|
|
989
|
+
*
|
|
990
|
+
* @example
|
|
991
|
+
* ```ts
|
|
992
|
+
* const ratelimit = new Ratelimit({
|
|
993
|
+
* redis: Redis.fromEnv(),
|
|
994
|
+
* limiter: Ratelimit.slidingWindow(10, "10 s"),
|
|
995
|
+
* dynamicLimits: true
|
|
996
|
+
* });
|
|
997
|
+
*
|
|
998
|
+
* // Set global dynamic limit to 120 requests
|
|
999
|
+
* await ratelimit.setDynamicLimit({ limit: 120 });
|
|
1000
|
+
*
|
|
1001
|
+
* // Disable dynamic limit (falls back to default)
|
|
1002
|
+
* await ratelimit.setDynamicLimit({ limit: false });
|
|
1003
|
+
* ```
|
|
1004
|
+
*
|
|
1005
|
+
* @param options.limit - The new rate limit to apply globally, or false to disable
|
|
1006
|
+
*/
|
|
1007
|
+
setDynamicLimit = async (options) => {
|
|
1008
|
+
if (!this.dynamicLimits) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
"dynamicLimits must be enabled in the Ratelimit constructor to use setDynamicLimit()"
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const globalKey = `${this.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`;
|
|
1014
|
+
await (options.limit === false ? this.primaryRedis.del(globalKey) : this.primaryRedis.set(globalKey, options.limit));
|
|
1015
|
+
};
|
|
1016
|
+
/**
|
|
1017
|
+
* Get the current global dynamic rate limit.
|
|
1018
|
+
*
|
|
1019
|
+
* @example
|
|
1020
|
+
* ```ts
|
|
1021
|
+
* const { dynamicLimit } = await ratelimit.getDynamicLimit();
|
|
1022
|
+
* console.log(dynamicLimit); // 120 or null if not set
|
|
1023
|
+
* ```
|
|
1024
|
+
*
|
|
1025
|
+
* @returns Object containing the current global dynamic limit, or null if not set
|
|
1026
|
+
*/
|
|
1027
|
+
getDynamicLimit = async () => {
|
|
1028
|
+
if (!this.dynamicLimits) {
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
"dynamicLimits must be enabled in the Ratelimit constructor to use getDynamicLimit()"
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
const globalKey = `${this.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`;
|
|
1034
|
+
const result = await this.primaryRedis.get(globalKey);
|
|
1035
|
+
return { dynamicLimit: result === null ? null : Number(result) };
|
|
1036
|
+
};
|
|
901
1037
|
};
|
|
902
1038
|
|
|
903
1039
|
// src/multi.ts
|
|
@@ -920,13 +1056,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
920
1056
|
limiter: config.limiter,
|
|
921
1057
|
timeout: config.timeout,
|
|
922
1058
|
analytics: config.analytics,
|
|
1059
|
+
dynamicLimits: config.dynamicLimits,
|
|
923
1060
|
ctx: {
|
|
924
1061
|
regionContexts: config.redis.map((redis) => ({
|
|
925
|
-
redis
|
|
1062
|
+
redis,
|
|
1063
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
926
1064
|
})),
|
|
927
1065
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
928
1066
|
}
|
|
929
1067
|
});
|
|
1068
|
+
if (config.dynamicLimits) {
|
|
1069
|
+
console.warn(
|
|
1070
|
+
"Warning: Dynamic limits are not yet supported for multi-region rate limiters. The dynamicLimits option will be ignored."
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
930
1073
|
}
|
|
931
1074
|
/**
|
|
932
1075
|
* Each request inside a fixed time increases a counter.
|
|
@@ -950,7 +1093,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
950
1093
|
const windowDuration = ms(window);
|
|
951
1094
|
return () => ({
|
|
952
1095
|
async limit(ctx, identifier, rate) {
|
|
953
|
-
|
|
1096
|
+
const requestId = randomId();
|
|
1097
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1098
|
+
const key = [identifier, bucket].join(":");
|
|
1099
|
+
const incrementBy = rate ?? 1;
|
|
1100
|
+
if (ctx.cache && incrementBy > 0) {
|
|
954
1101
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
955
1102
|
if (blocked) {
|
|
956
1103
|
return {
|
|
@@ -963,10 +1110,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
963
1110
|
};
|
|
964
1111
|
}
|
|
965
1112
|
}
|
|
966
|
-
const requestId = randomId();
|
|
967
|
-
const bucket = Math.floor(Date.now() / windowDuration);
|
|
968
|
-
const key = [identifier, bucket].join(":");
|
|
969
|
-
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
970
1113
|
const dbs = ctx.regionContexts.map((regionContext) => ({
|
|
971
1114
|
redis: regionContext.redis,
|
|
972
1115
|
request: safeEval(
|
|
@@ -977,24 +1120,29 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
977
1120
|
)
|
|
978
1121
|
}));
|
|
979
1122
|
const firstResponse = await Promise.any(dbs.map((s) => s.request));
|
|
980
|
-
const usedTokens = firstResponse.reduce(
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1123
|
+
const usedTokens = firstResponse.reduce(
|
|
1124
|
+
(accTokens, usedToken, index) => {
|
|
1125
|
+
let parsedToken = 0;
|
|
1126
|
+
if (index % 2) {
|
|
1127
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1128
|
+
}
|
|
1129
|
+
return accTokens + parsedToken;
|
|
1130
|
+
},
|
|
1131
|
+
0
|
|
1132
|
+
);
|
|
987
1133
|
const remaining = tokens - usedTokens;
|
|
988
1134
|
async function sync() {
|
|
989
1135
|
const individualIDs = await Promise.all(dbs.map((s) => s.request));
|
|
990
|
-
const allIDs = [
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1136
|
+
const allIDs = [
|
|
1137
|
+
...new Set(
|
|
1138
|
+
individualIDs.flat().reduce((acc, curr, index) => {
|
|
1139
|
+
if (index % 2 === 0) {
|
|
1140
|
+
acc.push(curr);
|
|
1141
|
+
}
|
|
1142
|
+
return acc;
|
|
1143
|
+
}, [])
|
|
1144
|
+
).values()
|
|
1145
|
+
];
|
|
998
1146
|
for (const db of dbs) {
|
|
999
1147
|
const usedDbTokensRequest = await db.request;
|
|
1000
1148
|
const usedDbTokens = usedDbTokensRequest.reduce(
|
|
@@ -1008,12 +1156,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1008
1156
|
0
|
|
1009
1157
|
);
|
|
1010
1158
|
const dbIdsRequest = await db.request;
|
|
1011
|
-
const dbIds = dbIdsRequest.reduce(
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1159
|
+
const dbIds = dbIdsRequest.reduce(
|
|
1160
|
+
(ids, currentId, index) => {
|
|
1161
|
+
if (index % 2 === 0) {
|
|
1162
|
+
ids.push(currentId);
|
|
1163
|
+
}
|
|
1164
|
+
return ids;
|
|
1165
|
+
},
|
|
1166
|
+
[]
|
|
1167
|
+
);
|
|
1017
1168
|
if (usedDbTokens >= tokens) {
|
|
1018
1169
|
continue;
|
|
1019
1170
|
}
|
|
@@ -1026,10 +1177,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1026
1177
|
}
|
|
1027
1178
|
}
|
|
1028
1179
|
}
|
|
1029
|
-
const success = remaining
|
|
1180
|
+
const success = remaining >= 0;
|
|
1030
1181
|
const reset = (bucket + 1) * windowDuration;
|
|
1031
|
-
if (ctx.cache
|
|
1032
|
-
|
|
1182
|
+
if (ctx.cache) {
|
|
1183
|
+
if (!success) {
|
|
1184
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1185
|
+
} else if (incrementBy < 0) {
|
|
1186
|
+
ctx.cache.pop(identifier);
|
|
1187
|
+
}
|
|
1033
1188
|
}
|
|
1034
1189
|
return {
|
|
1035
1190
|
success,
|
|
@@ -1052,16 +1207,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1052
1207
|
)
|
|
1053
1208
|
}));
|
|
1054
1209
|
const firstResponse = await Promise.any(dbs.map((s) => s.request));
|
|
1055
|
-
const usedTokens = firstResponse.reduce(
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1210
|
+
const usedTokens = firstResponse.reduce(
|
|
1211
|
+
(accTokens, usedToken, index) => {
|
|
1212
|
+
let parsedToken = 0;
|
|
1213
|
+
if (index % 2) {
|
|
1214
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1215
|
+
}
|
|
1216
|
+
return accTokens + parsedToken;
|
|
1217
|
+
},
|
|
1218
|
+
0
|
|
1219
|
+
);
|
|
1062
1220
|
return {
|
|
1063
1221
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1064
|
-
reset: (bucket + 1) * windowDuration
|
|
1222
|
+
reset: (bucket + 1) * windowDuration,
|
|
1223
|
+
limit: tokens
|
|
1065
1224
|
};
|
|
1066
1225
|
},
|
|
1067
1226
|
async resetTokens(ctx, identifier) {
|
|
@@ -1069,14 +1228,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1069
1228
|
if (ctx.cache) {
|
|
1070
1229
|
ctx.cache.pop(identifier);
|
|
1071
1230
|
}
|
|
1072
|
-
await Promise.all(
|
|
1073
|
-
|
|
1074
|
-
regionContext,
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
[null]
|
|
1078
|
-
);
|
|
1079
|
-
}));
|
|
1231
|
+
await Promise.all(
|
|
1232
|
+
ctx.regionContexts.map((regionContext) => {
|
|
1233
|
+
safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
1080
1236
|
}
|
|
1081
1237
|
});
|
|
1082
1238
|
}
|
|
@@ -1101,7 +1257,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1101
1257
|
const windowDuration = ms(window);
|
|
1102
1258
|
return () => ({
|
|
1103
1259
|
async limit(ctx, identifier, rate) {
|
|
1104
|
-
|
|
1260
|
+
const requestId = randomId();
|
|
1261
|
+
const now = Date.now();
|
|
1262
|
+
const currentWindow = Math.floor(now / windowSize);
|
|
1263
|
+
const currentKey = [identifier, currentWindow].join(":");
|
|
1264
|
+
const previousWindow = currentWindow - 1;
|
|
1265
|
+
const previousKey = [identifier, previousWindow].join(":");
|
|
1266
|
+
const incrementBy = rate ?? 1;
|
|
1267
|
+
if (ctx.cache && incrementBy > 0) {
|
|
1105
1268
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1106
1269
|
if (blocked) {
|
|
1107
1270
|
return {
|
|
@@ -1114,13 +1277,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1114
1277
|
};
|
|
1115
1278
|
}
|
|
1116
1279
|
}
|
|
1117
|
-
const requestId = randomId();
|
|
1118
|
-
const now = Date.now();
|
|
1119
|
-
const currentWindow = Math.floor(now / windowSize);
|
|
1120
|
-
const currentKey = [identifier, currentWindow].join(":");
|
|
1121
|
-
const previousWindow = currentWindow - 1;
|
|
1122
|
-
const previousKey = [identifier, previousWindow].join(":");
|
|
1123
|
-
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1124
1280
|
const dbs = ctx.regionContexts.map((regionContext) => ({
|
|
1125
1281
|
redis: regionContext.redis,
|
|
1126
1282
|
request: safeEval(
|
|
@@ -1132,37 +1288,49 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1132
1288
|
)
|
|
1133
1289
|
}));
|
|
1134
1290
|
const percentageInCurrent = now % windowDuration / windowDuration;
|
|
1135
|
-
const [current, previous, success] = await Promise.any(
|
|
1291
|
+
const [current, previous, success] = await Promise.any(
|
|
1292
|
+
dbs.map((s) => s.request)
|
|
1293
|
+
);
|
|
1136
1294
|
if (success) {
|
|
1137
1295
|
current.push(requestId, incrementBy.toString());
|
|
1138
1296
|
}
|
|
1139
|
-
const previousUsedTokens = previous.reduce(
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1297
|
+
const previousUsedTokens = previous.reduce(
|
|
1298
|
+
(accTokens, usedToken, index) => {
|
|
1299
|
+
let parsedToken = 0;
|
|
1300
|
+
if (index % 2) {
|
|
1301
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1302
|
+
}
|
|
1303
|
+
return accTokens + parsedToken;
|
|
1304
|
+
},
|
|
1305
|
+
0
|
|
1306
|
+
);
|
|
1307
|
+
const currentUsedTokens = current.reduce(
|
|
1308
|
+
(accTokens, usedToken, index) => {
|
|
1309
|
+
let parsedToken = 0;
|
|
1310
|
+
if (index % 2) {
|
|
1311
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1312
|
+
}
|
|
1313
|
+
return accTokens + parsedToken;
|
|
1314
|
+
},
|
|
1315
|
+
0
|
|
1316
|
+
);
|
|
1317
|
+
const previousPartialUsed = Math.ceil(
|
|
1318
|
+
previousUsedTokens * (1 - percentageInCurrent)
|
|
1319
|
+
);
|
|
1154
1320
|
const usedTokens = previousPartialUsed + currentUsedTokens;
|
|
1155
1321
|
const remaining = tokens - usedTokens;
|
|
1156
1322
|
async function sync() {
|
|
1157
1323
|
const res = await Promise.all(dbs.map((s) => s.request));
|
|
1158
|
-
const allCurrentIds = [
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1324
|
+
const allCurrentIds = [
|
|
1325
|
+
...new Set(
|
|
1326
|
+
res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
|
|
1327
|
+
if (index % 2 === 0) {
|
|
1328
|
+
acc.push(curr);
|
|
1329
|
+
}
|
|
1330
|
+
return acc;
|
|
1331
|
+
}, [])
|
|
1332
|
+
).values()
|
|
1333
|
+
];
|
|
1166
1334
|
for (const db of dbs) {
|
|
1167
1335
|
const [current2, _previous, _success] = await db.request;
|
|
1168
1336
|
const dbIds = current2.reduce((ids, currentId, index) => {
|
|
@@ -1171,13 +1339,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1171
1339
|
}
|
|
1172
1340
|
return ids;
|
|
1173
1341
|
}, []);
|
|
1174
|
-
const usedDbTokens = current2.reduce(
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1342
|
+
const usedDbTokens = current2.reduce(
|
|
1343
|
+
(accTokens, usedToken, index) => {
|
|
1344
|
+
let parsedToken = 0;
|
|
1345
|
+
if (index % 2) {
|
|
1346
|
+
parsedToken = Number.parseInt(usedToken);
|
|
1347
|
+
}
|
|
1348
|
+
return accTokens + parsedToken;
|
|
1349
|
+
},
|
|
1350
|
+
0
|
|
1351
|
+
);
|
|
1181
1352
|
if (usedDbTokens >= tokens) {
|
|
1182
1353
|
continue;
|
|
1183
1354
|
}
|
|
@@ -1191,8 +1362,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1191
1362
|
}
|
|
1192
1363
|
}
|
|
1193
1364
|
const reset = (currentWindow + 1) * windowDuration;
|
|
1194
|
-
if (ctx.cache
|
|
1195
|
-
|
|
1365
|
+
if (ctx.cache) {
|
|
1366
|
+
if (!success) {
|
|
1367
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1368
|
+
} else if (incrementBy < 0) {
|
|
1369
|
+
ctx.cache.pop(identifier);
|
|
1370
|
+
}
|
|
1196
1371
|
}
|
|
1197
1372
|
return {
|
|
1198
1373
|
success: Boolean(success),
|
|
@@ -1221,7 +1396,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1221
1396
|
const usedTokens = await Promise.any(dbs.map((s) => s.request));
|
|
1222
1397
|
return {
|
|
1223
1398
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1224
|
-
reset: (currentWindow + 1) * windowSize
|
|
1399
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1400
|
+
limit: tokens
|
|
1225
1401
|
};
|
|
1226
1402
|
},
|
|
1227
1403
|
async resetTokens(ctx, identifier) {
|
|
@@ -1229,14 +1405,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1229
1405
|
if (ctx.cache) {
|
|
1230
1406
|
ctx.cache.pop(identifier);
|
|
1231
1407
|
}
|
|
1232
|
-
await Promise.all(
|
|
1233
|
-
|
|
1234
|
-
regionContext,
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
[null]
|
|
1238
|
-
);
|
|
1239
|
-
}));
|
|
1408
|
+
await Promise.all(
|
|
1409
|
+
ctx.regionContexts.map((regionContext) => {
|
|
1410
|
+
safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
|
|
1411
|
+
})
|
|
1412
|
+
);
|
|
1240
1413
|
}
|
|
1241
1414
|
});
|
|
1242
1415
|
}
|
|
@@ -1254,11 +1427,13 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1254
1427
|
timeout: config.timeout,
|
|
1255
1428
|
analytics: config.analytics,
|
|
1256
1429
|
ctx: {
|
|
1257
|
-
redis: config.redis
|
|
1430
|
+
redis: config.redis,
|
|
1431
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
1258
1432
|
},
|
|
1259
1433
|
ephemeralCache: config.ephemeralCache,
|
|
1260
1434
|
enableProtection: config.enableProtection,
|
|
1261
|
-
denyListThreshold: config.denyListThreshold
|
|
1435
|
+
denyListThreshold: config.denyListThreshold,
|
|
1436
|
+
dynamicLimits: config.dynamicLimits
|
|
1262
1437
|
});
|
|
1263
1438
|
}
|
|
1264
1439
|
/**
|
|
@@ -1285,7 +1460,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1285
1460
|
async limit(ctx, identifier, rate) {
|
|
1286
1461
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1287
1462
|
const key = [identifier, bucket].join(":");
|
|
1288
|
-
|
|
1463
|
+
const incrementBy = rate ?? 1;
|
|
1464
|
+
if (ctx.cache && incrementBy > 0) {
|
|
1289
1465
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1290
1466
|
if (blocked) {
|
|
1291
1467
|
return {
|
|
@@ -1298,22 +1474,26 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1298
1474
|
};
|
|
1299
1475
|
}
|
|
1300
1476
|
}
|
|
1301
|
-
const
|
|
1302
|
-
const usedTokensAfterUpdate = await safeEval(
|
|
1477
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1478
|
+
const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
|
|
1303
1479
|
ctx,
|
|
1304
1480
|
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1305
|
-
[key],
|
|
1306
|
-
[windowDuration, incrementBy]
|
|
1481
|
+
[key, dynamicLimitKey],
|
|
1482
|
+
[tokens, windowDuration, incrementBy]
|
|
1307
1483
|
);
|
|
1308
|
-
const success = usedTokensAfterUpdate <=
|
|
1309
|
-
const remainingTokens = Math.max(0,
|
|
1484
|
+
const success = usedTokensAfterUpdate <= effectiveLimit;
|
|
1485
|
+
const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
|
|
1310
1486
|
const reset = (bucket + 1) * windowDuration;
|
|
1311
|
-
if (ctx.cache
|
|
1312
|
-
|
|
1487
|
+
if (ctx.cache) {
|
|
1488
|
+
if (!success) {
|
|
1489
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1490
|
+
} else if (incrementBy < 0) {
|
|
1491
|
+
ctx.cache.pop(identifier);
|
|
1492
|
+
}
|
|
1313
1493
|
}
|
|
1314
1494
|
return {
|
|
1315
1495
|
success,
|
|
1316
|
-
limit:
|
|
1496
|
+
limit: effectiveLimit,
|
|
1317
1497
|
remaining: remainingTokens,
|
|
1318
1498
|
reset,
|
|
1319
1499
|
pending: Promise.resolve()
|
|
@@ -1322,15 +1502,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1322
1502
|
async getRemaining(ctx, identifier) {
|
|
1323
1503
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1324
1504
|
const key = [identifier, bucket].join(":");
|
|
1325
|
-
const
|
|
1505
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1506
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1326
1507
|
ctx,
|
|
1327
1508
|
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1328
|
-
[key],
|
|
1329
|
-
[
|
|
1509
|
+
[key, dynamicLimitKey],
|
|
1510
|
+
[tokens]
|
|
1330
1511
|
);
|
|
1331
1512
|
return {
|
|
1332
|
-
remaining: Math.max(0,
|
|
1333
|
-
reset: (bucket + 1) * windowDuration
|
|
1513
|
+
remaining: Math.max(0, remaining),
|
|
1514
|
+
reset: (bucket + 1) * windowDuration,
|
|
1515
|
+
limit: effectiveLimit
|
|
1334
1516
|
};
|
|
1335
1517
|
},
|
|
1336
1518
|
async resetTokens(ctx, identifier) {
|
|
@@ -1372,7 +1554,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1372
1554
|
const currentKey = [identifier, currentWindow].join(":");
|
|
1373
1555
|
const previousWindow = currentWindow - 1;
|
|
1374
1556
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1375
|
-
|
|
1557
|
+
const incrementBy = rate ?? 1;
|
|
1558
|
+
if (ctx.cache && incrementBy > 0) {
|
|
1376
1559
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1377
1560
|
if (blocked) {
|
|
1378
1561
|
return {
|
|
@@ -1385,21 +1568,25 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1385
1568
|
};
|
|
1386
1569
|
}
|
|
1387
1570
|
}
|
|
1388
|
-
const
|
|
1389
|
-
const remainingTokens = await safeEval(
|
|
1571
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1572
|
+
const [remainingTokens, effectiveLimit] = await safeEval(
|
|
1390
1573
|
ctx,
|
|
1391
1574
|
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1392
|
-
[currentKey, previousKey],
|
|
1575
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1393
1576
|
[tokens, now, windowSize, incrementBy]
|
|
1394
1577
|
);
|
|
1395
1578
|
const success = remainingTokens >= 0;
|
|
1396
1579
|
const reset = (currentWindow + 1) * windowSize;
|
|
1397
|
-
if (ctx.cache
|
|
1398
|
-
|
|
1580
|
+
if (ctx.cache) {
|
|
1581
|
+
if (!success) {
|
|
1582
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1583
|
+
} else if (incrementBy < 0) {
|
|
1584
|
+
ctx.cache.pop(identifier);
|
|
1585
|
+
}
|
|
1399
1586
|
}
|
|
1400
1587
|
return {
|
|
1401
1588
|
success,
|
|
1402
|
-
limit:
|
|
1589
|
+
limit: effectiveLimit,
|
|
1403
1590
|
remaining: Math.max(0, remainingTokens),
|
|
1404
1591
|
reset,
|
|
1405
1592
|
pending: Promise.resolve()
|
|
@@ -1411,15 +1598,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1411
1598
|
const currentKey = [identifier, currentWindow].join(":");
|
|
1412
1599
|
const previousWindow = currentWindow - 1;
|
|
1413
1600
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1414
|
-
const
|
|
1601
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1602
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1415
1603
|
ctx,
|
|
1416
1604
|
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1417
|
-
[currentKey, previousKey],
|
|
1418
|
-
[now, windowSize]
|
|
1605
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1606
|
+
[tokens, now, windowSize]
|
|
1419
1607
|
);
|
|
1420
1608
|
return {
|
|
1421
|
-
remaining: Math.max(0,
|
|
1422
|
-
reset: (currentWindow + 1) * windowSize
|
|
1609
|
+
remaining: Math.max(0, remaining),
|
|
1610
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1611
|
+
limit: effectiveLimit
|
|
1423
1612
|
};
|
|
1424
1613
|
},
|
|
1425
1614
|
async resetTokens(ctx, identifier) {
|
|
@@ -1453,7 +1642,9 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1453
1642
|
const intervalDuration = ms(interval);
|
|
1454
1643
|
return () => ({
|
|
1455
1644
|
async limit(ctx, identifier, rate) {
|
|
1456
|
-
|
|
1645
|
+
const now = Date.now();
|
|
1646
|
+
const incrementBy = rate ?? 1;
|
|
1647
|
+
if (ctx.cache && incrementBy > 0) {
|
|
1457
1648
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1458
1649
|
if (blocked) {
|
|
1459
1650
|
return {
|
|
@@ -1466,38 +1657,43 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1466
1657
|
};
|
|
1467
1658
|
}
|
|
1468
1659
|
}
|
|
1469
|
-
const
|
|
1470
|
-
const
|
|
1471
|
-
const [remaining, reset] = await safeEval(
|
|
1660
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1661
|
+
const [remaining, reset, effectiveLimit] = await safeEval(
|
|
1472
1662
|
ctx,
|
|
1473
1663
|
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1474
|
-
[identifier],
|
|
1664
|
+
[identifier, dynamicLimitKey],
|
|
1475
1665
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1476
1666
|
);
|
|
1477
1667
|
const success = remaining >= 0;
|
|
1478
|
-
if (ctx.cache
|
|
1479
|
-
|
|
1668
|
+
if (ctx.cache) {
|
|
1669
|
+
if (!success) {
|
|
1670
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1671
|
+
} else if (incrementBy < 0) {
|
|
1672
|
+
ctx.cache.pop(identifier);
|
|
1673
|
+
}
|
|
1480
1674
|
}
|
|
1481
1675
|
return {
|
|
1482
1676
|
success,
|
|
1483
|
-
limit:
|
|
1484
|
-
remaining,
|
|
1677
|
+
limit: effectiveLimit,
|
|
1678
|
+
remaining: Math.max(0, remaining),
|
|
1485
1679
|
reset,
|
|
1486
1680
|
pending: Promise.resolve()
|
|
1487
1681
|
};
|
|
1488
1682
|
},
|
|
1489
1683
|
async getRemaining(ctx, identifier) {
|
|
1490
|
-
const
|
|
1684
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1685
|
+
const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
|
|
1491
1686
|
ctx,
|
|
1492
1687
|
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1493
|
-
[identifier],
|
|
1688
|
+
[identifier, dynamicLimitKey],
|
|
1494
1689
|
[maxTokens]
|
|
1495
1690
|
);
|
|
1496
1691
|
const freshRefillAt = Date.now() + intervalDuration;
|
|
1497
1692
|
const identifierRefillsAt = refilledAt + intervalDuration;
|
|
1498
1693
|
return {
|
|
1499
|
-
remaining: remainingTokens,
|
|
1500
|
-
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
|
|
1694
|
+
remaining: Math.max(0, remainingTokens),
|
|
1695
|
+
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
|
|
1696
|
+
limit: effectiveLimit
|
|
1501
1697
|
};
|
|
1502
1698
|
},
|
|
1503
1699
|
async resetTokens(ctx, identifier) {
|
|
@@ -1545,13 +1741,18 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1545
1741
|
if (!ctx.cache) {
|
|
1546
1742
|
throw new Error("This algorithm requires a cache");
|
|
1547
1743
|
}
|
|
1744
|
+
if (ctx.dynamicLimits) {
|
|
1745
|
+
console.warn(
|
|
1746
|
+
"Warning: Dynamic limits are not yet supported for cachedFixedWindow algorithm. The dynamicLimits option will be ignored."
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1548
1749
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1549
1750
|
const key = [identifier, bucket].join(":");
|
|
1550
1751
|
const reset = (bucket + 1) * windowDuration;
|
|
1551
|
-
const incrementBy = rate
|
|
1752
|
+
const incrementBy = rate ?? 1;
|
|
1552
1753
|
const hit = typeof ctx.cache.get(key) === "number";
|
|
1553
1754
|
if (hit) {
|
|
1554
|
-
const cachedTokensAfterUpdate = ctx.cache.incr(key);
|
|
1755
|
+
const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
|
|
1555
1756
|
const success = cachedTokensAfterUpdate < tokens;
|
|
1556
1757
|
const pending = success ? safeEval(
|
|
1557
1758
|
ctx,
|
|
@@ -1594,7 +1795,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1594
1795
|
const cachedUsedTokens = ctx.cache.get(key) ?? 0;
|
|
1595
1796
|
return {
|
|
1596
1797
|
remaining: Math.max(0, tokens - cachedUsedTokens),
|
|
1597
|
-
reset: (bucket + 1) * windowDuration
|
|
1798
|
+
reset: (bucket + 1) * windowDuration,
|
|
1799
|
+
limit: tokens
|
|
1598
1800
|
};
|
|
1599
1801
|
}
|
|
1600
1802
|
const usedTokens = await safeEval(
|
|
@@ -1605,7 +1807,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1605
1807
|
);
|
|
1606
1808
|
return {
|
|
1607
1809
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1608
|
-
reset: (bucket + 1) * windowDuration
|
|
1810
|
+
reset: (bucket + 1) * windowDuration,
|
|
1811
|
+
limit: tokens
|
|
1609
1812
|
};
|
|
1610
1813
|
},
|
|
1611
1814
|
async resetTokens(ctx, identifier) {
|