@upstash/ratelimit 2.0.7 → 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 +86 -3
- package/dist/index.d.ts +86 -3
- package/dist/index.js +226 -71
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +226 -71
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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,26 +202,48 @@ 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
236
|
local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
|
|
209
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
|
|
246
|
+
|
|
210
247
|
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
211
248
|
if requestsInCurrentWindow == false then
|
|
212
249
|
requestsInCurrentWindow = 0
|
|
@@ -221,8 +258,8 @@ var slidingWindowLimitScript = `
|
|
|
221
258
|
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
222
259
|
|
|
223
260
|
-- Only check limit if not refunding (negative rate)
|
|
224
|
-
if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >=
|
|
225
|
-
return -1
|
|
261
|
+
if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= effectiveLimit then
|
|
262
|
+
return {-1, effectiveLimit}
|
|
226
263
|
end
|
|
227
264
|
|
|
228
265
|
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
@@ -231,13 +268,24 @@ var slidingWindowLimitScript = `
|
|
|
231
268
|
-- So we only need the expire command once
|
|
232
269
|
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
233
270
|
end
|
|
234
|
-
return
|
|
271
|
+
return {effectiveLimit - ( newValue + requestsInPreviousWindow ), effectiveLimit}
|
|
235
272
|
`;
|
|
236
273
|
var slidingWindowRemainingTokensScript = `
|
|
237
274
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
238
275
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
239
|
-
local
|
|
240
|
-
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
|
|
241
289
|
|
|
242
290
|
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
243
291
|
if requestsInCurrentWindow == false then
|
|
@@ -253,15 +301,26 @@ var slidingWindowRemainingTokensScript = `
|
|
|
253
301
|
-- weighted requests to consider from the previous window
|
|
254
302
|
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
255
303
|
|
|
256
|
-
|
|
304
|
+
local usedTokens = requestsInPreviousWindow + requestsInCurrentWindow
|
|
305
|
+
return {effectiveLimit - usedTokens, effectiveLimit}
|
|
257
306
|
`;
|
|
258
307
|
var tokenBucketLimitScript = `
|
|
259
308
|
local key = KEYS[1] -- identifier including prefixes
|
|
260
|
-
local
|
|
309
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
310
|
+
local maxTokens = tonumber(ARGV[1]) -- default maximum number of tokens
|
|
261
311
|
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
262
312
|
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
263
313
|
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
264
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
|
|
265
324
|
|
|
266
325
|
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
267
326
|
|
|
@@ -270,7 +329,7 @@ var tokenBucketLimitScript = `
|
|
|
270
329
|
|
|
271
330
|
if bucket[1] == false then
|
|
272
331
|
refilledAt = now
|
|
273
|
-
tokens =
|
|
332
|
+
tokens = effectiveLimit
|
|
274
333
|
else
|
|
275
334
|
refilledAt = tonumber(bucket[1])
|
|
276
335
|
tokens = tonumber(bucket[2])
|
|
@@ -278,38 +337,48 @@ var tokenBucketLimitScript = `
|
|
|
278
337
|
|
|
279
338
|
if now >= refilledAt + interval then
|
|
280
339
|
local numRefills = math.floor((now - refilledAt) / interval)
|
|
281
|
-
tokens = math.min(
|
|
340
|
+
tokens = math.min(effectiveLimit, tokens + numRefills * refillRate)
|
|
282
341
|
|
|
283
342
|
refilledAt = refilledAt + numRefills * interval
|
|
284
343
|
end
|
|
285
344
|
|
|
286
345
|
-- Only reject if tokens are 0 and we're consuming (not refunding)
|
|
287
346
|
if tokens == 0 and incrementBy > 0 then
|
|
288
|
-
return {-1, refilledAt + interval}
|
|
347
|
+
return {-1, refilledAt + interval, effectiveLimit}
|
|
289
348
|
end
|
|
290
349
|
|
|
291
350
|
local remaining = tokens - incrementBy
|
|
292
|
-
local expireAt = math.ceil(((
|
|
351
|
+
local expireAt = math.ceil(((effectiveLimit - remaining) / refillRate)) * interval
|
|
293
352
|
|
|
294
353
|
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
295
354
|
|
|
296
355
|
if (expireAt > 0) then
|
|
297
356
|
redis.call("PEXPIRE", key, expireAt)
|
|
298
357
|
end
|
|
299
|
-
return {remaining, refilledAt + interval}
|
|
358
|
+
return {remaining, refilledAt + interval, effectiveLimit}
|
|
300
359
|
`;
|
|
301
360
|
var tokenBucketIdentifierNotFound = -1;
|
|
302
361
|
var tokenBucketRemainingTokensScript = `
|
|
303
362
|
local key = KEYS[1]
|
|
304
|
-
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
|
|
305
374
|
|
|
306
375
|
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
307
376
|
|
|
308
377
|
if bucket[1] == false then
|
|
309
|
-
return {
|
|
378
|
+
return {effectiveLimit, ${tokenBucketIdentifierNotFound}, effectiveLimit}
|
|
310
379
|
end
|
|
311
380
|
|
|
312
|
-
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
381
|
+
return {tonumber(bucket[2]), tonumber(bucket[1]), effectiveLimit}
|
|
313
382
|
`;
|
|
314
383
|
var cachedFixedWindowLimitScript = `
|
|
315
384
|
local key = KEYS[1]
|
|
@@ -453,31 +522,31 @@ var SCRIPTS = {
|
|
|
453
522
|
fixedWindow: {
|
|
454
523
|
limit: {
|
|
455
524
|
script: fixedWindowLimitScript,
|
|
456
|
-
hash: "
|
|
525
|
+
hash: "472e55443b62f60d0991028456c57815a387066d"
|
|
457
526
|
},
|
|
458
527
|
getRemaining: {
|
|
459
528
|
script: fixedWindowRemainingTokensScript,
|
|
460
|
-
hash: "
|
|
529
|
+
hash: "40515c9dd0a08f8584f5f9b593935f6a87c1c1c3"
|
|
461
530
|
}
|
|
462
531
|
},
|
|
463
532
|
slidingWindow: {
|
|
464
533
|
limit: {
|
|
465
534
|
script: slidingWindowLimitScript,
|
|
466
|
-
hash: "
|
|
535
|
+
hash: "977fb636fb5ceb7e98a96d1b3a1272ba018efdae"
|
|
467
536
|
},
|
|
468
537
|
getRemaining: {
|
|
469
538
|
script: slidingWindowRemainingTokensScript,
|
|
470
|
-
hash: "
|
|
539
|
+
hash: "ee3a3265fad822f83acad23f8a1e2f5c0b156b03"
|
|
471
540
|
}
|
|
472
541
|
},
|
|
473
542
|
tokenBucket: {
|
|
474
543
|
limit: {
|
|
475
544
|
script: tokenBucketLimitScript,
|
|
476
|
-
hash: "
|
|
545
|
+
hash: "b35c5bc0b7fdae7dd0573d4529911cabaf9d1d89"
|
|
477
546
|
},
|
|
478
547
|
getRemaining: {
|
|
479
548
|
script: tokenBucketRemainingTokensScript,
|
|
480
|
-
hash: "
|
|
549
|
+
hash: "deb03663e8af5a968deee895dd081be553d2611b"
|
|
481
550
|
}
|
|
482
551
|
},
|
|
483
552
|
cachedFixedWindow: {
|
|
@@ -690,14 +759,20 @@ var Ratelimit = class {
|
|
|
690
759
|
analytics;
|
|
691
760
|
enableProtection;
|
|
692
761
|
denyListThreshold;
|
|
762
|
+
dynamicLimits;
|
|
693
763
|
constructor(config) {
|
|
694
764
|
this.ctx = config.ctx;
|
|
695
765
|
this.limiter = config.limiter;
|
|
696
766
|
this.timeout = config.timeout ?? 5e3;
|
|
697
|
-
this.prefix = config.prefix ??
|
|
767
|
+
this.prefix = config.prefix ?? DEFAULT_PREFIX;
|
|
768
|
+
this.dynamicLimits = config.dynamicLimits ?? false;
|
|
698
769
|
this.enableProtection = config.enableProtection ?? false;
|
|
699
770
|
this.denyListThreshold = config.denyListThreshold ?? 6;
|
|
700
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
|
+
}
|
|
701
776
|
this.analytics = config.analytics ? new Analytics({
|
|
702
777
|
redis: this.primaryRedis,
|
|
703
778
|
prefix: this.prefix
|
|
@@ -811,9 +886,9 @@ var Ratelimit = class {
|
|
|
811
886
|
* Returns the remaining token count together with a reset timestamps
|
|
812
887
|
*
|
|
813
888
|
* @param identifier identifir to check
|
|
814
|
-
* @returns object with `remaining` and
|
|
815
|
-
* the remaining tokens
|
|
816
|
-
* 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.
|
|
817
892
|
*/
|
|
818
893
|
getRemaining = async (identifier) => {
|
|
819
894
|
const pattern = [this.prefix, identifier].join(":");
|
|
@@ -929,6 +1004,59 @@ var Ratelimit = class {
|
|
|
929
1004
|
const members = [identifier, req?.ip, req?.userAgent, req?.country];
|
|
930
1005
|
return members.filter(Boolean);
|
|
931
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
|
+
};
|
|
932
1060
|
};
|
|
933
1061
|
|
|
934
1062
|
// src/multi.ts
|
|
@@ -951,13 +1079,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
951
1079
|
limiter: config.limiter,
|
|
952
1080
|
timeout: config.timeout,
|
|
953
1081
|
analytics: config.analytics,
|
|
1082
|
+
dynamicLimits: config.dynamicLimits,
|
|
954
1083
|
ctx: {
|
|
955
1084
|
regionContexts: config.redis.map((redis) => ({
|
|
956
|
-
redis
|
|
1085
|
+
redis,
|
|
1086
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
957
1087
|
})),
|
|
958
1088
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
959
1089
|
}
|
|
960
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
|
+
}
|
|
961
1096
|
}
|
|
962
1097
|
/**
|
|
963
1098
|
* Each request inside a fixed time increases a counter.
|
|
@@ -1107,7 +1242,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1107
1242
|
);
|
|
1108
1243
|
return {
|
|
1109
1244
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1110
|
-
reset: (bucket + 1) * windowDuration
|
|
1245
|
+
reset: (bucket + 1) * windowDuration,
|
|
1246
|
+
limit: tokens
|
|
1111
1247
|
};
|
|
1112
1248
|
},
|
|
1113
1249
|
async resetTokens(ctx, identifier) {
|
|
@@ -1283,7 +1419,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1283
1419
|
const usedTokens = await Promise.any(dbs.map((s) => s.request));
|
|
1284
1420
|
return {
|
|
1285
1421
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1286
|
-
reset: (currentWindow + 1) * windowSize
|
|
1422
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1423
|
+
limit: tokens
|
|
1287
1424
|
};
|
|
1288
1425
|
},
|
|
1289
1426
|
async resetTokens(ctx, identifier) {
|
|
@@ -1313,11 +1450,13 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1313
1450
|
timeout: config.timeout,
|
|
1314
1451
|
analytics: config.analytics,
|
|
1315
1452
|
ctx: {
|
|
1316
|
-
redis: config.redis
|
|
1453
|
+
redis: config.redis,
|
|
1454
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
1317
1455
|
},
|
|
1318
1456
|
ephemeralCache: config.ephemeralCache,
|
|
1319
1457
|
enableProtection: config.enableProtection,
|
|
1320
|
-
denyListThreshold: config.denyListThreshold
|
|
1458
|
+
denyListThreshold: config.denyListThreshold,
|
|
1459
|
+
dynamicLimits: config.dynamicLimits
|
|
1321
1460
|
});
|
|
1322
1461
|
}
|
|
1323
1462
|
/**
|
|
@@ -1358,14 +1497,15 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1358
1497
|
};
|
|
1359
1498
|
}
|
|
1360
1499
|
}
|
|
1361
|
-
const
|
|
1500
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1501
|
+
const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
|
|
1362
1502
|
ctx,
|
|
1363
1503
|
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1364
|
-
[key],
|
|
1365
|
-
[windowDuration, incrementBy]
|
|
1504
|
+
[key, dynamicLimitKey],
|
|
1505
|
+
[tokens, windowDuration, incrementBy]
|
|
1366
1506
|
);
|
|
1367
|
-
const success = usedTokensAfterUpdate <=
|
|
1368
|
-
const remainingTokens = Math.max(0,
|
|
1507
|
+
const success = usedTokensAfterUpdate <= effectiveLimit;
|
|
1508
|
+
const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
|
|
1369
1509
|
const reset = (bucket + 1) * windowDuration;
|
|
1370
1510
|
if (ctx.cache) {
|
|
1371
1511
|
if (!success) {
|
|
@@ -1376,7 +1516,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1376
1516
|
}
|
|
1377
1517
|
return {
|
|
1378
1518
|
success,
|
|
1379
|
-
limit:
|
|
1519
|
+
limit: effectiveLimit,
|
|
1380
1520
|
remaining: remainingTokens,
|
|
1381
1521
|
reset,
|
|
1382
1522
|
pending: Promise.resolve()
|
|
@@ -1385,15 +1525,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1385
1525
|
async getRemaining(ctx, identifier) {
|
|
1386
1526
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1387
1527
|
const key = [identifier, bucket].join(":");
|
|
1388
|
-
const
|
|
1528
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1529
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1389
1530
|
ctx,
|
|
1390
1531
|
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1391
|
-
[key],
|
|
1392
|
-
[
|
|
1532
|
+
[key, dynamicLimitKey],
|
|
1533
|
+
[tokens]
|
|
1393
1534
|
);
|
|
1394
1535
|
return {
|
|
1395
|
-
remaining: Math.max(0,
|
|
1396
|
-
reset: (bucket + 1) * windowDuration
|
|
1536
|
+
remaining: Math.max(0, remaining),
|
|
1537
|
+
reset: (bucket + 1) * windowDuration,
|
|
1538
|
+
limit: effectiveLimit
|
|
1397
1539
|
};
|
|
1398
1540
|
},
|
|
1399
1541
|
async resetTokens(ctx, identifier) {
|
|
@@ -1449,10 +1591,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1449
1591
|
};
|
|
1450
1592
|
}
|
|
1451
1593
|
}
|
|
1452
|
-
const
|
|
1594
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1595
|
+
const [remainingTokens, effectiveLimit] = await safeEval(
|
|
1453
1596
|
ctx,
|
|
1454
1597
|
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1455
|
-
[currentKey, previousKey],
|
|
1598
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1456
1599
|
[tokens, now, windowSize, incrementBy]
|
|
1457
1600
|
);
|
|
1458
1601
|
const success = remainingTokens >= 0;
|
|
@@ -1466,7 +1609,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1466
1609
|
}
|
|
1467
1610
|
return {
|
|
1468
1611
|
success,
|
|
1469
|
-
limit:
|
|
1612
|
+
limit: effectiveLimit,
|
|
1470
1613
|
remaining: Math.max(0, remainingTokens),
|
|
1471
1614
|
reset,
|
|
1472
1615
|
pending: Promise.resolve()
|
|
@@ -1478,15 +1621,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1478
1621
|
const currentKey = [identifier, currentWindow].join(":");
|
|
1479
1622
|
const previousWindow = currentWindow - 1;
|
|
1480
1623
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1481
|
-
const
|
|
1624
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1625
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1482
1626
|
ctx,
|
|
1483
1627
|
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1484
|
-
[currentKey, previousKey],
|
|
1485
|
-
[now, windowSize]
|
|
1628
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1629
|
+
[tokens, now, windowSize]
|
|
1486
1630
|
);
|
|
1487
1631
|
return {
|
|
1488
|
-
remaining: Math.max(0,
|
|
1489
|
-
reset: (currentWindow + 1) * windowSize
|
|
1632
|
+
remaining: Math.max(0, remaining),
|
|
1633
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1634
|
+
limit: effectiveLimit
|
|
1490
1635
|
};
|
|
1491
1636
|
},
|
|
1492
1637
|
async resetTokens(ctx, identifier) {
|
|
@@ -1535,10 +1680,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1535
1680
|
};
|
|
1536
1681
|
}
|
|
1537
1682
|
}
|
|
1538
|
-
const
|
|
1683
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1684
|
+
const [remaining, reset, effectiveLimit] = await safeEval(
|
|
1539
1685
|
ctx,
|
|
1540
1686
|
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1541
|
-
[identifier],
|
|
1687
|
+
[identifier, dynamicLimitKey],
|
|
1542
1688
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1543
1689
|
);
|
|
1544
1690
|
const success = remaining >= 0;
|
|
@@ -1551,24 +1697,26 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1551
1697
|
}
|
|
1552
1698
|
return {
|
|
1553
1699
|
success,
|
|
1554
|
-
limit:
|
|
1555
|
-
remaining,
|
|
1700
|
+
limit: effectiveLimit,
|
|
1701
|
+
remaining: Math.max(0, remaining),
|
|
1556
1702
|
reset,
|
|
1557
1703
|
pending: Promise.resolve()
|
|
1558
1704
|
};
|
|
1559
1705
|
},
|
|
1560
1706
|
async getRemaining(ctx, identifier) {
|
|
1561
|
-
const
|
|
1707
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1708
|
+
const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
|
|
1562
1709
|
ctx,
|
|
1563
1710
|
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1564
|
-
[identifier],
|
|
1711
|
+
[identifier, dynamicLimitKey],
|
|
1565
1712
|
[maxTokens]
|
|
1566
1713
|
);
|
|
1567
1714
|
const freshRefillAt = Date.now() + intervalDuration;
|
|
1568
1715
|
const identifierRefillsAt = refilledAt + intervalDuration;
|
|
1569
1716
|
return {
|
|
1570
|
-
remaining: remainingTokens,
|
|
1571
|
-
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
|
|
1717
|
+
remaining: Math.max(0, remainingTokens),
|
|
1718
|
+
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
|
|
1719
|
+
limit: effectiveLimit
|
|
1572
1720
|
};
|
|
1573
1721
|
},
|
|
1574
1722
|
async resetTokens(ctx, identifier) {
|
|
@@ -1616,6 +1764,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1616
1764
|
if (!ctx.cache) {
|
|
1617
1765
|
throw new Error("This algorithm requires a cache");
|
|
1618
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
|
+
}
|
|
1619
1772
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1620
1773
|
const key = [identifier, bucket].join(":");
|
|
1621
1774
|
const reset = (bucket + 1) * windowDuration;
|
|
@@ -1665,7 +1818,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1665
1818
|
const cachedUsedTokens = ctx.cache.get(key) ?? 0;
|
|
1666
1819
|
return {
|
|
1667
1820
|
remaining: Math.max(0, tokens - cachedUsedTokens),
|
|
1668
|
-
reset: (bucket + 1) * windowDuration
|
|
1821
|
+
reset: (bucket + 1) * windowDuration,
|
|
1822
|
+
limit: tokens
|
|
1669
1823
|
};
|
|
1670
1824
|
}
|
|
1671
1825
|
const usedTokens = await safeEval(
|
|
@@ -1676,7 +1830,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1676
1830
|
);
|
|
1677
1831
|
return {
|
|
1678
1832
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1679
|
-
reset: (bucket + 1) * windowDuration
|
|
1833
|
+
reset: (bucket + 1) * windowDuration,
|
|
1834
|
+
limit: tokens
|
|
1680
1835
|
};
|
|
1681
1836
|
},
|
|
1682
1837
|
async resetTokens(ctx, identifier) {
|