@upstash/ratelimit 2.0.7 → 2.1.0-rc
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/README.md +19 -0
- package/dist/index.d.mts +108 -3
- package/dist/index.d.ts +108 -3
- package/dist/index.js +269 -89
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +269 -89
- 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)$/);
|
|
@@ -152,10 +156,21 @@ var safeEval = async (ctx, script, keys, args) => {
|
|
|
152
156
|
};
|
|
153
157
|
|
|
154
158
|
// src/lua-scripts/single.ts
|
|
155
|
-
var fixedWindowLimitScript =
|
|
159
|
+
var fixedWindowLimitScript = `#!lua flags=allow-key-locking
|
|
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
|
-
var fixedWindowRemainingTokensScript =
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
var fixedWindowRemainingTokensScript = `#!lua flags=allow-key-locking
|
|
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
|
-
|
|
179
|
-
|
|
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
|
+
`;
|
|
206
|
+
var slidingWindowLimitScript = `#!lua flags=allow-key-locking
|
|
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
|
-
var slidingWindowRemainingTokensScript =
|
|
250
|
+
var slidingWindowRemainingTokensScript = `#!lua flags=allow-key-locking
|
|
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
|
-
var tokenBucketLimitScript =
|
|
284
|
+
var tokenBucketLimitScript = `#!lua flags=allow-key-locking
|
|
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,40 +314,50 @@ 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
|
-
var tokenBucketRemainingTokensScript =
|
|
338
|
+
var tokenBucketRemainingTokensScript = `#!lua flags=allow-key-locking
|
|
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
|
-
var cachedFixedWindowLimitScript =
|
|
360
|
+
var cachedFixedWindowLimitScript = `#!lua flags=allow-key-locking
|
|
292
361
|
local key = KEYS[1]
|
|
293
362
|
local window = ARGV[1]
|
|
294
363
|
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
@@ -302,7 +371,7 @@ var cachedFixedWindowLimitScript = `
|
|
|
302
371
|
|
|
303
372
|
return r
|
|
304
373
|
`;
|
|
305
|
-
var cachedFixedWindowRemainingTokenScript =
|
|
374
|
+
var cachedFixedWindowRemainingTokenScript = `#!lua flags=allow-key-locking
|
|
306
375
|
local key = KEYS[1]
|
|
307
376
|
local tokens = 0
|
|
308
377
|
|
|
@@ -314,7 +383,7 @@ var cachedFixedWindowRemainingTokenScript = `
|
|
|
314
383
|
`;
|
|
315
384
|
|
|
316
385
|
// src/lua-scripts/multi.ts
|
|
317
|
-
var fixedWindowLimitScript2 =
|
|
386
|
+
var fixedWindowLimitScript2 = `#!lua flags=allow-key-locking
|
|
318
387
|
local key = KEYS[1]
|
|
319
388
|
local id = ARGV[1]
|
|
320
389
|
local window = ARGV[2]
|
|
@@ -330,7 +399,7 @@ var fixedWindowLimitScript2 = `
|
|
|
330
399
|
|
|
331
400
|
return fields
|
|
332
401
|
`;
|
|
333
|
-
var fixedWindowRemainingTokensScript2 =
|
|
402
|
+
var fixedWindowRemainingTokensScript2 = `#!lua flags=allow-key-locking
|
|
334
403
|
local key = KEYS[1]
|
|
335
404
|
local tokens = 0
|
|
336
405
|
|
|
@@ -338,7 +407,7 @@ var fixedWindowRemainingTokensScript2 = `
|
|
|
338
407
|
|
|
339
408
|
return fields
|
|
340
409
|
`;
|
|
341
|
-
var slidingWindowLimitScript2 =
|
|
410
|
+
var slidingWindowLimitScript2 = `#!lua flags=allow-key-locking
|
|
342
411
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
343
412
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
344
413
|
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
@@ -375,7 +444,7 @@ var slidingWindowLimitScript2 = `
|
|
|
375
444
|
end
|
|
376
445
|
return {currentFields, previousFields, true}
|
|
377
446
|
`;
|
|
378
|
-
var slidingWindowRemainingTokensScript2 =
|
|
447
|
+
var slidingWindowRemainingTokensScript2 = `#!lua flags=allow-key-locking
|
|
379
448
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
380
449
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
381
450
|
local now = ARGV[1] -- current timestamp in milliseconds
|
|
@@ -430,41 +499,41 @@ var SCRIPTS = {
|
|
|
430
499
|
fixedWindow: {
|
|
431
500
|
limit: {
|
|
432
501
|
script: fixedWindowLimitScript,
|
|
433
|
-
hash: "
|
|
502
|
+
hash: "4ef8749b9e927b157546ebad13061fa86b9ffc2e"
|
|
434
503
|
},
|
|
435
504
|
getRemaining: {
|
|
436
505
|
script: fixedWindowRemainingTokensScript,
|
|
437
|
-
hash: "
|
|
506
|
+
hash: "e62252cb05b4676b27a5d396da02f0ca0d375d88"
|
|
438
507
|
}
|
|
439
508
|
},
|
|
440
509
|
slidingWindow: {
|
|
441
510
|
limit: {
|
|
442
511
|
script: slidingWindowLimitScript,
|
|
443
|
-
hash: "
|
|
512
|
+
hash: "5cd9665be7533e4dfabb3581021737fc69b41ae8"
|
|
444
513
|
},
|
|
445
514
|
getRemaining: {
|
|
446
515
|
script: slidingWindowRemainingTokensScript,
|
|
447
|
-
hash: "
|
|
516
|
+
hash: "1bdbbdb2082bb557084518c64486d7d5a29d1bfe"
|
|
448
517
|
}
|
|
449
518
|
},
|
|
450
519
|
tokenBucket: {
|
|
451
520
|
limit: {
|
|
452
521
|
script: tokenBucketLimitScript,
|
|
453
|
-
hash: "
|
|
522
|
+
hash: "8f7286b9b0f65d631f760ba615f7b3c598a569a3"
|
|
454
523
|
},
|
|
455
524
|
getRemaining: {
|
|
456
525
|
script: tokenBucketRemainingTokensScript,
|
|
457
|
-
hash: "
|
|
526
|
+
hash: "08d3b3381c507b6f7bfec1bbfa0a6053434b16bd"
|
|
458
527
|
}
|
|
459
528
|
},
|
|
460
529
|
cachedFixedWindow: {
|
|
461
530
|
limit: {
|
|
462
531
|
script: cachedFixedWindowLimitScript,
|
|
463
|
-
hash: "
|
|
532
|
+
hash: "1861a700bafe96c833a483af6b1c28a8897cfdb0"
|
|
464
533
|
},
|
|
465
534
|
getRemaining: {
|
|
466
535
|
script: cachedFixedWindowRemainingTokenScript,
|
|
467
|
-
hash: "
|
|
536
|
+
hash: "eb82f0e853d2fc9a236fa9bfbe704fda6ac2fc36"
|
|
468
537
|
}
|
|
469
538
|
}
|
|
470
539
|
},
|
|
@@ -472,21 +541,21 @@ var SCRIPTS = {
|
|
|
472
541
|
fixedWindow: {
|
|
473
542
|
limit: {
|
|
474
543
|
script: fixedWindowLimitScript2,
|
|
475
|
-
hash: "
|
|
544
|
+
hash: "e04b753a75909b7f99aae4c04cf8869a8de02a9e"
|
|
476
545
|
},
|
|
477
546
|
getRemaining: {
|
|
478
547
|
script: fixedWindowRemainingTokensScript2,
|
|
479
|
-
hash: "
|
|
548
|
+
hash: "e066c8ce3eaca142b0894ad1541e0a58c9924819"
|
|
480
549
|
}
|
|
481
550
|
},
|
|
482
551
|
slidingWindow: {
|
|
483
552
|
limit: {
|
|
484
553
|
script: slidingWindowLimitScript2,
|
|
485
|
-
hash: "
|
|
554
|
+
hash: "aeb4d8f381e8dafc8e69041baae1324d254ded44"
|
|
486
555
|
},
|
|
487
556
|
getRemaining: {
|
|
488
557
|
script: slidingWindowRemainingTokensScript2,
|
|
489
|
-
hash: "
|
|
558
|
+
hash: "87828c1088f6a8d1f512a434ea330189982a1c0a"
|
|
490
559
|
}
|
|
491
560
|
}
|
|
492
561
|
}
|
|
@@ -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,80 @@ 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
|
+
};
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
// src/version.ts
|
|
1040
|
+
var VERSION = "2.1.0-rc";
|
|
1041
|
+
|
|
1042
|
+
// src/telemetry.ts
|
|
1043
|
+
var taggedClients = /* @__PURE__ */ new WeakSet();
|
|
1044
|
+
var getSafeEnv = () => typeof process === "object" && process && typeof process.env === "object" ? process.env : {};
|
|
1045
|
+
var addTelemetry = (redis, enableTelemetry = true) => {
|
|
1046
|
+
if (!enableTelemetry || getSafeEnv().UPSTASH_DISABLE_TELEMETRY)
|
|
1047
|
+
return;
|
|
1048
|
+
if (!redis || typeof redis !== "object")
|
|
1049
|
+
return;
|
|
1050
|
+
if (taggedClients.has(redis))
|
|
1051
|
+
return;
|
|
1052
|
+
taggedClients.add(redis);
|
|
1053
|
+
try {
|
|
1054
|
+
const client = redis;
|
|
1055
|
+
client.addTelemetry?.({ sdk: `@upstash/ratelimit@${VERSION}` });
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
909
1058
|
};
|
|
910
1059
|
|
|
911
1060
|
// src/multi.ts
|
|
@@ -928,13 +1077,23 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
928
1077
|
limiter: config.limiter,
|
|
929
1078
|
timeout: config.timeout,
|
|
930
1079
|
analytics: config.analytics,
|
|
1080
|
+
dynamicLimits: config.dynamicLimits,
|
|
931
1081
|
ctx: {
|
|
932
1082
|
regionContexts: config.redis.map((redis) => ({
|
|
933
|
-
redis
|
|
1083
|
+
redis,
|
|
1084
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
934
1085
|
})),
|
|
935
1086
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
936
1087
|
}
|
|
937
1088
|
});
|
|
1089
|
+
for (const redis of config.redis) {
|
|
1090
|
+
addTelemetry(redis, config.enableTelemetry);
|
|
1091
|
+
}
|
|
1092
|
+
if (config.dynamicLimits) {
|
|
1093
|
+
console.warn(
|
|
1094
|
+
"Warning: Dynamic limits are not yet supported for multi-region rate limiters. The dynamicLimits option will be ignored."
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
938
1097
|
}
|
|
939
1098
|
/**
|
|
940
1099
|
* Each request inside a fixed time increases a counter.
|
|
@@ -1084,7 +1243,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1084
1243
|
);
|
|
1085
1244
|
return {
|
|
1086
1245
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1087
|
-
reset: (bucket + 1) * windowDuration
|
|
1246
|
+
reset: (bucket + 1) * windowDuration,
|
|
1247
|
+
limit: tokens
|
|
1088
1248
|
};
|
|
1089
1249
|
},
|
|
1090
1250
|
async resetTokens(ctx, identifier) {
|
|
@@ -1260,7 +1420,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1260
1420
|
const usedTokens = await Promise.any(dbs.map((s) => s.request));
|
|
1261
1421
|
return {
|
|
1262
1422
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1263
|
-
reset: (currentWindow + 1) * windowSize
|
|
1423
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1424
|
+
limit: tokens
|
|
1264
1425
|
};
|
|
1265
1426
|
},
|
|
1266
1427
|
async resetTokens(ctx, identifier) {
|
|
@@ -1290,12 +1451,15 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1290
1451
|
timeout: config.timeout,
|
|
1291
1452
|
analytics: config.analytics,
|
|
1292
1453
|
ctx: {
|
|
1293
|
-
redis: config.redis
|
|
1454
|
+
redis: config.redis,
|
|
1455
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
1294
1456
|
},
|
|
1295
1457
|
ephemeralCache: config.ephemeralCache,
|
|
1296
1458
|
enableProtection: config.enableProtection,
|
|
1297
|
-
denyListThreshold: config.denyListThreshold
|
|
1459
|
+
denyListThreshold: config.denyListThreshold,
|
|
1460
|
+
dynamicLimits: config.dynamicLimits
|
|
1298
1461
|
});
|
|
1462
|
+
addTelemetry(config.redis, config.enableTelemetry);
|
|
1299
1463
|
}
|
|
1300
1464
|
/**
|
|
1301
1465
|
* Each request inside a fixed time increases a counter.
|
|
@@ -1335,14 +1499,15 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1335
1499
|
};
|
|
1336
1500
|
}
|
|
1337
1501
|
}
|
|
1338
|
-
const
|
|
1502
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1503
|
+
const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
|
|
1339
1504
|
ctx,
|
|
1340
1505
|
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1341
|
-
[key],
|
|
1342
|
-
[windowDuration, incrementBy]
|
|
1506
|
+
[key, dynamicLimitKey],
|
|
1507
|
+
[tokens, windowDuration, incrementBy]
|
|
1343
1508
|
);
|
|
1344
|
-
const success = usedTokensAfterUpdate <=
|
|
1345
|
-
const remainingTokens = Math.max(0,
|
|
1509
|
+
const success = usedTokensAfterUpdate <= effectiveLimit;
|
|
1510
|
+
const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
|
|
1346
1511
|
const reset = (bucket + 1) * windowDuration;
|
|
1347
1512
|
if (ctx.cache) {
|
|
1348
1513
|
if (!success) {
|
|
@@ -1353,7 +1518,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1353
1518
|
}
|
|
1354
1519
|
return {
|
|
1355
1520
|
success,
|
|
1356
|
-
limit:
|
|
1521
|
+
limit: effectiveLimit,
|
|
1357
1522
|
remaining: remainingTokens,
|
|
1358
1523
|
reset,
|
|
1359
1524
|
pending: Promise.resolve()
|
|
@@ -1362,15 +1527,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1362
1527
|
async getRemaining(ctx, identifier) {
|
|
1363
1528
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1364
1529
|
const key = [identifier, bucket].join(":");
|
|
1365
|
-
const
|
|
1530
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1531
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1366
1532
|
ctx,
|
|
1367
1533
|
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1368
|
-
[key],
|
|
1369
|
-
[
|
|
1534
|
+
[key, dynamicLimitKey],
|
|
1535
|
+
[tokens]
|
|
1370
1536
|
);
|
|
1371
1537
|
return {
|
|
1372
|
-
remaining: Math.max(0,
|
|
1373
|
-
reset: (bucket + 1) * windowDuration
|
|
1538
|
+
remaining: Math.max(0, remaining),
|
|
1539
|
+
reset: (bucket + 1) * windowDuration,
|
|
1540
|
+
limit: effectiveLimit
|
|
1374
1541
|
};
|
|
1375
1542
|
},
|
|
1376
1543
|
async resetTokens(ctx, identifier) {
|
|
@@ -1426,10 +1593,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1426
1593
|
};
|
|
1427
1594
|
}
|
|
1428
1595
|
}
|
|
1429
|
-
const
|
|
1596
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1597
|
+
const [remainingTokens, effectiveLimit] = await safeEval(
|
|
1430
1598
|
ctx,
|
|
1431
1599
|
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1432
|
-
[currentKey, previousKey],
|
|
1600
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1433
1601
|
[tokens, now, windowSize, incrementBy]
|
|
1434
1602
|
);
|
|
1435
1603
|
const success = remainingTokens >= 0;
|
|
@@ -1443,7 +1611,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1443
1611
|
}
|
|
1444
1612
|
return {
|
|
1445
1613
|
success,
|
|
1446
|
-
limit:
|
|
1614
|
+
limit: effectiveLimit,
|
|
1447
1615
|
remaining: Math.max(0, remainingTokens),
|
|
1448
1616
|
reset,
|
|
1449
1617
|
pending: Promise.resolve()
|
|
@@ -1455,15 +1623,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1455
1623
|
const currentKey = [identifier, currentWindow].join(":");
|
|
1456
1624
|
const previousWindow = currentWindow - 1;
|
|
1457
1625
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1458
|
-
const
|
|
1626
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1627
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1459
1628
|
ctx,
|
|
1460
1629
|
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1461
|
-
[currentKey, previousKey],
|
|
1462
|
-
[now, windowSize]
|
|
1630
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1631
|
+
[tokens, now, windowSize]
|
|
1463
1632
|
);
|
|
1464
1633
|
return {
|
|
1465
|
-
remaining: Math.max(0,
|
|
1466
|
-
reset: (currentWindow + 1) * windowSize
|
|
1634
|
+
remaining: Math.max(0, remaining),
|
|
1635
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1636
|
+
limit: effectiveLimit
|
|
1467
1637
|
};
|
|
1468
1638
|
},
|
|
1469
1639
|
async resetTokens(ctx, identifier) {
|
|
@@ -1512,10 +1682,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1512
1682
|
};
|
|
1513
1683
|
}
|
|
1514
1684
|
}
|
|
1515
|
-
const
|
|
1685
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1686
|
+
const [remaining, reset, effectiveLimit] = await safeEval(
|
|
1516
1687
|
ctx,
|
|
1517
1688
|
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1518
|
-
[identifier],
|
|
1689
|
+
[identifier, dynamicLimitKey],
|
|
1519
1690
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1520
1691
|
);
|
|
1521
1692
|
const success = remaining >= 0;
|
|
@@ -1528,24 +1699,26 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1528
1699
|
}
|
|
1529
1700
|
return {
|
|
1530
1701
|
success,
|
|
1531
|
-
limit:
|
|
1532
|
-
remaining,
|
|
1702
|
+
limit: effectiveLimit,
|
|
1703
|
+
remaining: Math.max(0, remaining),
|
|
1533
1704
|
reset,
|
|
1534
1705
|
pending: Promise.resolve()
|
|
1535
1706
|
};
|
|
1536
1707
|
},
|
|
1537
1708
|
async getRemaining(ctx, identifier) {
|
|
1538
|
-
const
|
|
1709
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1710
|
+
const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
|
|
1539
1711
|
ctx,
|
|
1540
1712
|
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1541
|
-
[identifier],
|
|
1713
|
+
[identifier, dynamicLimitKey],
|
|
1542
1714
|
[maxTokens]
|
|
1543
1715
|
);
|
|
1544
1716
|
const freshRefillAt = Date.now() + intervalDuration;
|
|
1545
1717
|
const identifierRefillsAt = refilledAt + intervalDuration;
|
|
1546
1718
|
return {
|
|
1547
|
-
remaining: remainingTokens,
|
|
1548
|
-
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
|
|
1719
|
+
remaining: Math.max(0, remainingTokens),
|
|
1720
|
+
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
|
|
1721
|
+
limit: effectiveLimit
|
|
1549
1722
|
};
|
|
1550
1723
|
},
|
|
1551
1724
|
async resetTokens(ctx, identifier) {
|
|
@@ -1593,6 +1766,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1593
1766
|
if (!ctx.cache) {
|
|
1594
1767
|
throw new Error("This algorithm requires a cache");
|
|
1595
1768
|
}
|
|
1769
|
+
if (ctx.dynamicLimits) {
|
|
1770
|
+
console.warn(
|
|
1771
|
+
"Warning: Dynamic limits are not yet supported for cachedFixedWindow algorithm. The dynamicLimits option will be ignored."
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1596
1774
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1597
1775
|
const key = [identifier, bucket].join(":");
|
|
1598
1776
|
const reset = (bucket + 1) * windowDuration;
|
|
@@ -1642,7 +1820,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1642
1820
|
const cachedUsedTokens = ctx.cache.get(key) ?? 0;
|
|
1643
1821
|
return {
|
|
1644
1822
|
remaining: Math.max(0, tokens - cachedUsedTokens),
|
|
1645
|
-
reset: (bucket + 1) * windowDuration
|
|
1823
|
+
reset: (bucket + 1) * windowDuration,
|
|
1824
|
+
limit: tokens
|
|
1646
1825
|
};
|
|
1647
1826
|
}
|
|
1648
1827
|
const usedTokens = await safeEval(
|
|
@@ -1653,7 +1832,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1653
1832
|
);
|
|
1654
1833
|
return {
|
|
1655
1834
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1656
|
-
reset: (bucket + 1) * windowDuration
|
|
1835
|
+
reset: (bucket + 1) * windowDuration,
|
|
1836
|
+
limit: tokens
|
|
1657
1837
|
};
|
|
1658
1838
|
},
|
|
1659
1839
|
async resetTokens(ctx, identifier) {
|