@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.mjs
CHANGED
|
@@ -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,26 +179,48 @@ 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
213
|
local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
|
|
186
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
|
|
223
|
+
|
|
187
224
|
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
188
225
|
if requestsInCurrentWindow == false then
|
|
189
226
|
requestsInCurrentWindow = 0
|
|
@@ -198,8 +235,8 @@ var slidingWindowLimitScript = `
|
|
|
198
235
|
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
199
236
|
|
|
200
237
|
-- Only check limit if not refunding (negative rate)
|
|
201
|
-
if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >=
|
|
202
|
-
return -1
|
|
238
|
+
if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= effectiveLimit then
|
|
239
|
+
return {-1, effectiveLimit}
|
|
203
240
|
end
|
|
204
241
|
|
|
205
242
|
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
@@ -208,13 +245,24 @@ var slidingWindowLimitScript = `
|
|
|
208
245
|
-- So we only need the expire command once
|
|
209
246
|
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
210
247
|
end
|
|
211
|
-
return
|
|
248
|
+
return {effectiveLimit - ( newValue + requestsInPreviousWindow ), effectiveLimit}
|
|
212
249
|
`;
|
|
213
250
|
var slidingWindowRemainingTokensScript = `
|
|
214
251
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
215
252
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
216
|
-
local
|
|
217
|
-
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
|
|
218
266
|
|
|
219
267
|
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
220
268
|
if requestsInCurrentWindow == false then
|
|
@@ -230,15 +278,26 @@ var slidingWindowRemainingTokensScript = `
|
|
|
230
278
|
-- weighted requests to consider from the previous window
|
|
231
279
|
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
232
280
|
|
|
233
|
-
|
|
281
|
+
local usedTokens = requestsInPreviousWindow + requestsInCurrentWindow
|
|
282
|
+
return {effectiveLimit - usedTokens, effectiveLimit}
|
|
234
283
|
`;
|
|
235
284
|
var tokenBucketLimitScript = `
|
|
236
285
|
local key = KEYS[1] -- identifier including prefixes
|
|
237
|
-
local
|
|
286
|
+
local dynamicLimitKey = KEYS[2] -- optional: key for dynamic limit in redis
|
|
287
|
+
local maxTokens = tonumber(ARGV[1]) -- default maximum number of tokens
|
|
238
288
|
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
239
289
|
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
240
290
|
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
241
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
|
|
242
301
|
|
|
243
302
|
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
244
303
|
|
|
@@ -247,7 +306,7 @@ var tokenBucketLimitScript = `
|
|
|
247
306
|
|
|
248
307
|
if bucket[1] == false then
|
|
249
308
|
refilledAt = now
|
|
250
|
-
tokens =
|
|
309
|
+
tokens = effectiveLimit
|
|
251
310
|
else
|
|
252
311
|
refilledAt = tonumber(bucket[1])
|
|
253
312
|
tokens = tonumber(bucket[2])
|
|
@@ -255,38 +314,48 @@ var tokenBucketLimitScript = `
|
|
|
255
314
|
|
|
256
315
|
if now >= refilledAt + interval then
|
|
257
316
|
local numRefills = math.floor((now - refilledAt) / interval)
|
|
258
|
-
tokens = math.min(
|
|
317
|
+
tokens = math.min(effectiveLimit, tokens + numRefills * refillRate)
|
|
259
318
|
|
|
260
319
|
refilledAt = refilledAt + numRefills * interval
|
|
261
320
|
end
|
|
262
321
|
|
|
263
322
|
-- Only reject if tokens are 0 and we're consuming (not refunding)
|
|
264
323
|
if tokens == 0 and incrementBy > 0 then
|
|
265
|
-
return {-1, refilledAt + interval}
|
|
324
|
+
return {-1, refilledAt + interval, effectiveLimit}
|
|
266
325
|
end
|
|
267
326
|
|
|
268
327
|
local remaining = tokens - incrementBy
|
|
269
|
-
local expireAt = math.ceil(((
|
|
328
|
+
local expireAt = math.ceil(((effectiveLimit - remaining) / refillRate)) * interval
|
|
270
329
|
|
|
271
330
|
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
272
331
|
|
|
273
332
|
if (expireAt > 0) then
|
|
274
333
|
redis.call("PEXPIRE", key, expireAt)
|
|
275
334
|
end
|
|
276
|
-
return {remaining, refilledAt + interval}
|
|
335
|
+
return {remaining, refilledAt + interval, effectiveLimit}
|
|
277
336
|
`;
|
|
278
337
|
var tokenBucketIdentifierNotFound = -1;
|
|
279
338
|
var tokenBucketRemainingTokensScript = `
|
|
280
339
|
local key = KEYS[1]
|
|
281
|
-
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
|
|
282
351
|
|
|
283
352
|
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
284
353
|
|
|
285
354
|
if bucket[1] == false then
|
|
286
|
-
return {
|
|
355
|
+
return {effectiveLimit, ${tokenBucketIdentifierNotFound}, effectiveLimit}
|
|
287
356
|
end
|
|
288
357
|
|
|
289
|
-
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
358
|
+
return {tonumber(bucket[2]), tonumber(bucket[1]), effectiveLimit}
|
|
290
359
|
`;
|
|
291
360
|
var cachedFixedWindowLimitScript = `
|
|
292
361
|
local key = KEYS[1]
|
|
@@ -430,31 +499,31 @@ var SCRIPTS = {
|
|
|
430
499
|
fixedWindow: {
|
|
431
500
|
limit: {
|
|
432
501
|
script: fixedWindowLimitScript,
|
|
433
|
-
hash: "
|
|
502
|
+
hash: "472e55443b62f60d0991028456c57815a387066d"
|
|
434
503
|
},
|
|
435
504
|
getRemaining: {
|
|
436
505
|
script: fixedWindowRemainingTokensScript,
|
|
437
|
-
hash: "
|
|
506
|
+
hash: "40515c9dd0a08f8584f5f9b593935f6a87c1c1c3"
|
|
438
507
|
}
|
|
439
508
|
},
|
|
440
509
|
slidingWindow: {
|
|
441
510
|
limit: {
|
|
442
511
|
script: slidingWindowLimitScript,
|
|
443
|
-
hash: "
|
|
512
|
+
hash: "977fb636fb5ceb7e98a96d1b3a1272ba018efdae"
|
|
444
513
|
},
|
|
445
514
|
getRemaining: {
|
|
446
515
|
script: slidingWindowRemainingTokensScript,
|
|
447
|
-
hash: "
|
|
516
|
+
hash: "ee3a3265fad822f83acad23f8a1e2f5c0b156b03"
|
|
448
517
|
}
|
|
449
518
|
},
|
|
450
519
|
tokenBucket: {
|
|
451
520
|
limit: {
|
|
452
521
|
script: tokenBucketLimitScript,
|
|
453
|
-
hash: "
|
|
522
|
+
hash: "b35c5bc0b7fdae7dd0573d4529911cabaf9d1d89"
|
|
454
523
|
},
|
|
455
524
|
getRemaining: {
|
|
456
525
|
script: tokenBucketRemainingTokensScript,
|
|
457
|
-
hash: "
|
|
526
|
+
hash: "deb03663e8af5a968deee895dd081be553d2611b"
|
|
458
527
|
}
|
|
459
528
|
},
|
|
460
529
|
cachedFixedWindow: {
|
|
@@ -667,14 +736,20 @@ var Ratelimit = class {
|
|
|
667
736
|
analytics;
|
|
668
737
|
enableProtection;
|
|
669
738
|
denyListThreshold;
|
|
739
|
+
dynamicLimits;
|
|
670
740
|
constructor(config) {
|
|
671
741
|
this.ctx = config.ctx;
|
|
672
742
|
this.limiter = config.limiter;
|
|
673
743
|
this.timeout = config.timeout ?? 5e3;
|
|
674
|
-
this.prefix = config.prefix ??
|
|
744
|
+
this.prefix = config.prefix ?? DEFAULT_PREFIX;
|
|
745
|
+
this.dynamicLimits = config.dynamicLimits ?? false;
|
|
675
746
|
this.enableProtection = config.enableProtection ?? false;
|
|
676
747
|
this.denyListThreshold = config.denyListThreshold ?? 6;
|
|
677
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
|
+
}
|
|
678
753
|
this.analytics = config.analytics ? new Analytics({
|
|
679
754
|
redis: this.primaryRedis,
|
|
680
755
|
prefix: this.prefix
|
|
@@ -788,9 +863,9 @@ var Ratelimit = class {
|
|
|
788
863
|
* Returns the remaining token count together with a reset timestamps
|
|
789
864
|
*
|
|
790
865
|
* @param identifier identifir to check
|
|
791
|
-
* @returns object with `remaining` and
|
|
792
|
-
* the remaining tokens
|
|
793
|
-
* 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.
|
|
794
869
|
*/
|
|
795
870
|
getRemaining = async (identifier) => {
|
|
796
871
|
const pattern = [this.prefix, identifier].join(":");
|
|
@@ -906,6 +981,59 @@ var Ratelimit = class {
|
|
|
906
981
|
const members = [identifier, req?.ip, req?.userAgent, req?.country];
|
|
907
982
|
return members.filter(Boolean);
|
|
908
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
|
+
};
|
|
909
1037
|
};
|
|
910
1038
|
|
|
911
1039
|
// src/multi.ts
|
|
@@ -928,13 +1056,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
928
1056
|
limiter: config.limiter,
|
|
929
1057
|
timeout: config.timeout,
|
|
930
1058
|
analytics: config.analytics,
|
|
1059
|
+
dynamicLimits: config.dynamicLimits,
|
|
931
1060
|
ctx: {
|
|
932
1061
|
regionContexts: config.redis.map((redis) => ({
|
|
933
|
-
redis
|
|
1062
|
+
redis,
|
|
1063
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
934
1064
|
})),
|
|
935
1065
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
936
1066
|
}
|
|
937
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
|
+
}
|
|
938
1073
|
}
|
|
939
1074
|
/**
|
|
940
1075
|
* Each request inside a fixed time increases a counter.
|
|
@@ -1084,7 +1219,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1084
1219
|
);
|
|
1085
1220
|
return {
|
|
1086
1221
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1087
|
-
reset: (bucket + 1) * windowDuration
|
|
1222
|
+
reset: (bucket + 1) * windowDuration,
|
|
1223
|
+
limit: tokens
|
|
1088
1224
|
};
|
|
1089
1225
|
},
|
|
1090
1226
|
async resetTokens(ctx, identifier) {
|
|
@@ -1260,7 +1396,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1260
1396
|
const usedTokens = await Promise.any(dbs.map((s) => s.request));
|
|
1261
1397
|
return {
|
|
1262
1398
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1263
|
-
reset: (currentWindow + 1) * windowSize
|
|
1399
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1400
|
+
limit: tokens
|
|
1264
1401
|
};
|
|
1265
1402
|
},
|
|
1266
1403
|
async resetTokens(ctx, identifier) {
|
|
@@ -1290,11 +1427,13 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1290
1427
|
timeout: config.timeout,
|
|
1291
1428
|
analytics: config.analytics,
|
|
1292
1429
|
ctx: {
|
|
1293
|
-
redis: config.redis
|
|
1430
|
+
redis: config.redis,
|
|
1431
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
1294
1432
|
},
|
|
1295
1433
|
ephemeralCache: config.ephemeralCache,
|
|
1296
1434
|
enableProtection: config.enableProtection,
|
|
1297
|
-
denyListThreshold: config.denyListThreshold
|
|
1435
|
+
denyListThreshold: config.denyListThreshold,
|
|
1436
|
+
dynamicLimits: config.dynamicLimits
|
|
1298
1437
|
});
|
|
1299
1438
|
}
|
|
1300
1439
|
/**
|
|
@@ -1335,14 +1474,15 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1335
1474
|
};
|
|
1336
1475
|
}
|
|
1337
1476
|
}
|
|
1338
|
-
const
|
|
1477
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1478
|
+
const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
|
|
1339
1479
|
ctx,
|
|
1340
1480
|
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1341
|
-
[key],
|
|
1342
|
-
[windowDuration, incrementBy]
|
|
1481
|
+
[key, dynamicLimitKey],
|
|
1482
|
+
[tokens, windowDuration, incrementBy]
|
|
1343
1483
|
);
|
|
1344
|
-
const success = usedTokensAfterUpdate <=
|
|
1345
|
-
const remainingTokens = Math.max(0,
|
|
1484
|
+
const success = usedTokensAfterUpdate <= effectiveLimit;
|
|
1485
|
+
const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
|
|
1346
1486
|
const reset = (bucket + 1) * windowDuration;
|
|
1347
1487
|
if (ctx.cache) {
|
|
1348
1488
|
if (!success) {
|
|
@@ -1353,7 +1493,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1353
1493
|
}
|
|
1354
1494
|
return {
|
|
1355
1495
|
success,
|
|
1356
|
-
limit:
|
|
1496
|
+
limit: effectiveLimit,
|
|
1357
1497
|
remaining: remainingTokens,
|
|
1358
1498
|
reset,
|
|
1359
1499
|
pending: Promise.resolve()
|
|
@@ -1362,15 +1502,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1362
1502
|
async getRemaining(ctx, identifier) {
|
|
1363
1503
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1364
1504
|
const key = [identifier, bucket].join(":");
|
|
1365
|
-
const
|
|
1505
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1506
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1366
1507
|
ctx,
|
|
1367
1508
|
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1368
|
-
[key],
|
|
1369
|
-
[
|
|
1509
|
+
[key, dynamicLimitKey],
|
|
1510
|
+
[tokens]
|
|
1370
1511
|
);
|
|
1371
1512
|
return {
|
|
1372
|
-
remaining: Math.max(0,
|
|
1373
|
-
reset: (bucket + 1) * windowDuration
|
|
1513
|
+
remaining: Math.max(0, remaining),
|
|
1514
|
+
reset: (bucket + 1) * windowDuration,
|
|
1515
|
+
limit: effectiveLimit
|
|
1374
1516
|
};
|
|
1375
1517
|
},
|
|
1376
1518
|
async resetTokens(ctx, identifier) {
|
|
@@ -1426,10 +1568,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1426
1568
|
};
|
|
1427
1569
|
}
|
|
1428
1570
|
}
|
|
1429
|
-
const
|
|
1571
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1572
|
+
const [remainingTokens, effectiveLimit] = await safeEval(
|
|
1430
1573
|
ctx,
|
|
1431
1574
|
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1432
|
-
[currentKey, previousKey],
|
|
1575
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1433
1576
|
[tokens, now, windowSize, incrementBy]
|
|
1434
1577
|
);
|
|
1435
1578
|
const success = remainingTokens >= 0;
|
|
@@ -1443,7 +1586,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1443
1586
|
}
|
|
1444
1587
|
return {
|
|
1445
1588
|
success,
|
|
1446
|
-
limit:
|
|
1589
|
+
limit: effectiveLimit,
|
|
1447
1590
|
remaining: Math.max(0, remainingTokens),
|
|
1448
1591
|
reset,
|
|
1449
1592
|
pending: Promise.resolve()
|
|
@@ -1455,15 +1598,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1455
1598
|
const currentKey = [identifier, currentWindow].join(":");
|
|
1456
1599
|
const previousWindow = currentWindow - 1;
|
|
1457
1600
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1458
|
-
const
|
|
1601
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1602
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1459
1603
|
ctx,
|
|
1460
1604
|
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1461
|
-
[currentKey, previousKey],
|
|
1462
|
-
[now, windowSize]
|
|
1605
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1606
|
+
[tokens, now, windowSize]
|
|
1463
1607
|
);
|
|
1464
1608
|
return {
|
|
1465
|
-
remaining: Math.max(0,
|
|
1466
|
-
reset: (currentWindow + 1) * windowSize
|
|
1609
|
+
remaining: Math.max(0, remaining),
|
|
1610
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1611
|
+
limit: effectiveLimit
|
|
1467
1612
|
};
|
|
1468
1613
|
},
|
|
1469
1614
|
async resetTokens(ctx, identifier) {
|
|
@@ -1512,10 +1657,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1512
1657
|
};
|
|
1513
1658
|
}
|
|
1514
1659
|
}
|
|
1515
|
-
const
|
|
1660
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1661
|
+
const [remaining, reset, effectiveLimit] = await safeEval(
|
|
1516
1662
|
ctx,
|
|
1517
1663
|
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1518
|
-
[identifier],
|
|
1664
|
+
[identifier, dynamicLimitKey],
|
|
1519
1665
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1520
1666
|
);
|
|
1521
1667
|
const success = remaining >= 0;
|
|
@@ -1528,24 +1674,26 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1528
1674
|
}
|
|
1529
1675
|
return {
|
|
1530
1676
|
success,
|
|
1531
|
-
limit:
|
|
1532
|
-
remaining,
|
|
1677
|
+
limit: effectiveLimit,
|
|
1678
|
+
remaining: Math.max(0, remaining),
|
|
1533
1679
|
reset,
|
|
1534
1680
|
pending: Promise.resolve()
|
|
1535
1681
|
};
|
|
1536
1682
|
},
|
|
1537
1683
|
async getRemaining(ctx, identifier) {
|
|
1538
|
-
const
|
|
1684
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1685
|
+
const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
|
|
1539
1686
|
ctx,
|
|
1540
1687
|
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1541
|
-
[identifier],
|
|
1688
|
+
[identifier, dynamicLimitKey],
|
|
1542
1689
|
[maxTokens]
|
|
1543
1690
|
);
|
|
1544
1691
|
const freshRefillAt = Date.now() + intervalDuration;
|
|
1545
1692
|
const identifierRefillsAt = refilledAt + intervalDuration;
|
|
1546
1693
|
return {
|
|
1547
|
-
remaining: remainingTokens,
|
|
1548
|
-
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
|
|
1694
|
+
remaining: Math.max(0, remainingTokens),
|
|
1695
|
+
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
|
|
1696
|
+
limit: effectiveLimit
|
|
1549
1697
|
};
|
|
1550
1698
|
},
|
|
1551
1699
|
async resetTokens(ctx, identifier) {
|
|
@@ -1593,6 +1741,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1593
1741
|
if (!ctx.cache) {
|
|
1594
1742
|
throw new Error("This algorithm requires a cache");
|
|
1595
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
|
+
}
|
|
1596
1749
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1597
1750
|
const key = [identifier, bucket].join(":");
|
|
1598
1751
|
const reset = (bucket + 1) * windowDuration;
|
|
@@ -1642,7 +1795,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1642
1795
|
const cachedUsedTokens = ctx.cache.get(key) ?? 0;
|
|
1643
1796
|
return {
|
|
1644
1797
|
remaining: Math.max(0, tokens - cachedUsedTokens),
|
|
1645
|
-
reset: (bucket + 1) * windowDuration
|
|
1798
|
+
reset: (bucket + 1) * windowDuration,
|
|
1799
|
+
limit: tokens
|
|
1646
1800
|
};
|
|
1647
1801
|
}
|
|
1648
1802
|
const usedTokens = await safeEval(
|
|
@@ -1653,7 +1807,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1653
1807
|
);
|
|
1654
1808
|
return {
|
|
1655
1809
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1656
|
-
reset: (bucket + 1) * windowDuration
|
|
1810
|
+
reset: (bucket + 1) * windowDuration,
|
|
1811
|
+
limit: tokens
|
|
1657
1812
|
};
|
|
1658
1813
|
},
|
|
1659
1814
|
async resetTokens(ctx, identifier) {
|