@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.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)$/);
|
|
@@ -175,10 +179,21 @@ var safeEval = async (ctx, script, keys, args) => {
|
|
|
175
179
|
};
|
|
176
180
|
|
|
177
181
|
// src/lua-scripts/single.ts
|
|
178
|
-
var fixedWindowLimitScript =
|
|
182
|
+
var fixedWindowLimitScript = `#!lua flags=allow-key-locking
|
|
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
|
-
var fixedWindowRemainingTokensScript =
|
|
193
|
-
|
|
194
|
-
|
|
207
|
+
var fixedWindowRemainingTokensScript = `#!lua flags=allow-key-locking
|
|
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
|
-
|
|
202
|
-
|
|
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
|
+
`;
|
|
229
|
+
var slidingWindowLimitScript = `#!lua flags=allow-key-locking
|
|
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
|
-
var slidingWindowRemainingTokensScript =
|
|
273
|
+
var slidingWindowRemainingTokensScript = `#!lua flags=allow-key-locking
|
|
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
|
-
var tokenBucketLimitScript =
|
|
307
|
+
var tokenBucketLimitScript = `#!lua flags=allow-key-locking
|
|
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,40 +337,50 @@ 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
|
-
var tokenBucketRemainingTokensScript =
|
|
361
|
+
var tokenBucketRemainingTokensScript = `#!lua flags=allow-key-locking
|
|
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
|
-
var cachedFixedWindowLimitScript =
|
|
383
|
+
var cachedFixedWindowLimitScript = `#!lua flags=allow-key-locking
|
|
315
384
|
local key = KEYS[1]
|
|
316
385
|
local window = ARGV[1]
|
|
317
386
|
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
@@ -325,7 +394,7 @@ var cachedFixedWindowLimitScript = `
|
|
|
325
394
|
|
|
326
395
|
return r
|
|
327
396
|
`;
|
|
328
|
-
var cachedFixedWindowRemainingTokenScript =
|
|
397
|
+
var cachedFixedWindowRemainingTokenScript = `#!lua flags=allow-key-locking
|
|
329
398
|
local key = KEYS[1]
|
|
330
399
|
local tokens = 0
|
|
331
400
|
|
|
@@ -337,7 +406,7 @@ var cachedFixedWindowRemainingTokenScript = `
|
|
|
337
406
|
`;
|
|
338
407
|
|
|
339
408
|
// src/lua-scripts/multi.ts
|
|
340
|
-
var fixedWindowLimitScript2 =
|
|
409
|
+
var fixedWindowLimitScript2 = `#!lua flags=allow-key-locking
|
|
341
410
|
local key = KEYS[1]
|
|
342
411
|
local id = ARGV[1]
|
|
343
412
|
local window = ARGV[2]
|
|
@@ -353,7 +422,7 @@ var fixedWindowLimitScript2 = `
|
|
|
353
422
|
|
|
354
423
|
return fields
|
|
355
424
|
`;
|
|
356
|
-
var fixedWindowRemainingTokensScript2 =
|
|
425
|
+
var fixedWindowRemainingTokensScript2 = `#!lua flags=allow-key-locking
|
|
357
426
|
local key = KEYS[1]
|
|
358
427
|
local tokens = 0
|
|
359
428
|
|
|
@@ -361,7 +430,7 @@ var fixedWindowRemainingTokensScript2 = `
|
|
|
361
430
|
|
|
362
431
|
return fields
|
|
363
432
|
`;
|
|
364
|
-
var slidingWindowLimitScript2 =
|
|
433
|
+
var slidingWindowLimitScript2 = `#!lua flags=allow-key-locking
|
|
365
434
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
366
435
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
367
436
|
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
@@ -398,7 +467,7 @@ var slidingWindowLimitScript2 = `
|
|
|
398
467
|
end
|
|
399
468
|
return {currentFields, previousFields, true}
|
|
400
469
|
`;
|
|
401
|
-
var slidingWindowRemainingTokensScript2 =
|
|
470
|
+
var slidingWindowRemainingTokensScript2 = `#!lua flags=allow-key-locking
|
|
402
471
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
403
472
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
404
473
|
local now = ARGV[1] -- current timestamp in milliseconds
|
|
@@ -453,41 +522,41 @@ var SCRIPTS = {
|
|
|
453
522
|
fixedWindow: {
|
|
454
523
|
limit: {
|
|
455
524
|
script: fixedWindowLimitScript,
|
|
456
|
-
hash: "
|
|
525
|
+
hash: "4ef8749b9e927b157546ebad13061fa86b9ffc2e"
|
|
457
526
|
},
|
|
458
527
|
getRemaining: {
|
|
459
528
|
script: fixedWindowRemainingTokensScript,
|
|
460
|
-
hash: "
|
|
529
|
+
hash: "e62252cb05b4676b27a5d396da02f0ca0d375d88"
|
|
461
530
|
}
|
|
462
531
|
},
|
|
463
532
|
slidingWindow: {
|
|
464
533
|
limit: {
|
|
465
534
|
script: slidingWindowLimitScript,
|
|
466
|
-
hash: "
|
|
535
|
+
hash: "5cd9665be7533e4dfabb3581021737fc69b41ae8"
|
|
467
536
|
},
|
|
468
537
|
getRemaining: {
|
|
469
538
|
script: slidingWindowRemainingTokensScript,
|
|
470
|
-
hash: "
|
|
539
|
+
hash: "1bdbbdb2082bb557084518c64486d7d5a29d1bfe"
|
|
471
540
|
}
|
|
472
541
|
},
|
|
473
542
|
tokenBucket: {
|
|
474
543
|
limit: {
|
|
475
544
|
script: tokenBucketLimitScript,
|
|
476
|
-
hash: "
|
|
545
|
+
hash: "8f7286b9b0f65d631f760ba615f7b3c598a569a3"
|
|
477
546
|
},
|
|
478
547
|
getRemaining: {
|
|
479
548
|
script: tokenBucketRemainingTokensScript,
|
|
480
|
-
hash: "
|
|
549
|
+
hash: "08d3b3381c507b6f7bfec1bbfa0a6053434b16bd"
|
|
481
550
|
}
|
|
482
551
|
},
|
|
483
552
|
cachedFixedWindow: {
|
|
484
553
|
limit: {
|
|
485
554
|
script: cachedFixedWindowLimitScript,
|
|
486
|
-
hash: "
|
|
555
|
+
hash: "1861a700bafe96c833a483af6b1c28a8897cfdb0"
|
|
487
556
|
},
|
|
488
557
|
getRemaining: {
|
|
489
558
|
script: cachedFixedWindowRemainingTokenScript,
|
|
490
|
-
hash: "
|
|
559
|
+
hash: "eb82f0e853d2fc9a236fa9bfbe704fda6ac2fc36"
|
|
491
560
|
}
|
|
492
561
|
}
|
|
493
562
|
},
|
|
@@ -495,21 +564,21 @@ var SCRIPTS = {
|
|
|
495
564
|
fixedWindow: {
|
|
496
565
|
limit: {
|
|
497
566
|
script: fixedWindowLimitScript2,
|
|
498
|
-
hash: "
|
|
567
|
+
hash: "e04b753a75909b7f99aae4c04cf8869a8de02a9e"
|
|
499
568
|
},
|
|
500
569
|
getRemaining: {
|
|
501
570
|
script: fixedWindowRemainingTokensScript2,
|
|
502
|
-
hash: "
|
|
571
|
+
hash: "e066c8ce3eaca142b0894ad1541e0a58c9924819"
|
|
503
572
|
}
|
|
504
573
|
},
|
|
505
574
|
slidingWindow: {
|
|
506
575
|
limit: {
|
|
507
576
|
script: slidingWindowLimitScript2,
|
|
508
|
-
hash: "
|
|
577
|
+
hash: "aeb4d8f381e8dafc8e69041baae1324d254ded44"
|
|
509
578
|
},
|
|
510
579
|
getRemaining: {
|
|
511
580
|
script: slidingWindowRemainingTokensScript2,
|
|
512
|
-
hash: "
|
|
581
|
+
hash: "87828c1088f6a8d1f512a434ea330189982a1c0a"
|
|
513
582
|
}
|
|
514
583
|
}
|
|
515
584
|
}
|
|
@@ -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,80 @@ 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
|
+
};
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
// src/version.ts
|
|
1063
|
+
var VERSION = "2.1.0-rc";
|
|
1064
|
+
|
|
1065
|
+
// src/telemetry.ts
|
|
1066
|
+
var taggedClients = /* @__PURE__ */ new WeakSet();
|
|
1067
|
+
var getSafeEnv = () => typeof process === "object" && process && typeof process.env === "object" ? process.env : {};
|
|
1068
|
+
var addTelemetry = (redis, enableTelemetry = true) => {
|
|
1069
|
+
if (!enableTelemetry || getSafeEnv().UPSTASH_DISABLE_TELEMETRY)
|
|
1070
|
+
return;
|
|
1071
|
+
if (!redis || typeof redis !== "object")
|
|
1072
|
+
return;
|
|
1073
|
+
if (taggedClients.has(redis))
|
|
1074
|
+
return;
|
|
1075
|
+
taggedClients.add(redis);
|
|
1076
|
+
try {
|
|
1077
|
+
const client = redis;
|
|
1078
|
+
client.addTelemetry?.({ sdk: `@upstash/ratelimit@${VERSION}` });
|
|
1079
|
+
} catch {
|
|
1080
|
+
}
|
|
932
1081
|
};
|
|
933
1082
|
|
|
934
1083
|
// src/multi.ts
|
|
@@ -951,13 +1100,23 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
951
1100
|
limiter: config.limiter,
|
|
952
1101
|
timeout: config.timeout,
|
|
953
1102
|
analytics: config.analytics,
|
|
1103
|
+
dynamicLimits: config.dynamicLimits,
|
|
954
1104
|
ctx: {
|
|
955
1105
|
regionContexts: config.redis.map((redis) => ({
|
|
956
|
-
redis
|
|
1106
|
+
redis,
|
|
1107
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
957
1108
|
})),
|
|
958
1109
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
959
1110
|
}
|
|
960
1111
|
});
|
|
1112
|
+
for (const redis of config.redis) {
|
|
1113
|
+
addTelemetry(redis, config.enableTelemetry);
|
|
1114
|
+
}
|
|
1115
|
+
if (config.dynamicLimits) {
|
|
1116
|
+
console.warn(
|
|
1117
|
+
"Warning: Dynamic limits are not yet supported for multi-region rate limiters. The dynamicLimits option will be ignored."
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
961
1120
|
}
|
|
962
1121
|
/**
|
|
963
1122
|
* Each request inside a fixed time increases a counter.
|
|
@@ -1107,7 +1266,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1107
1266
|
);
|
|
1108
1267
|
return {
|
|
1109
1268
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1110
|
-
reset: (bucket + 1) * windowDuration
|
|
1269
|
+
reset: (bucket + 1) * windowDuration,
|
|
1270
|
+
limit: tokens
|
|
1111
1271
|
};
|
|
1112
1272
|
},
|
|
1113
1273
|
async resetTokens(ctx, identifier) {
|
|
@@ -1283,7 +1443,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1283
1443
|
const usedTokens = await Promise.any(dbs.map((s) => s.request));
|
|
1284
1444
|
return {
|
|
1285
1445
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1286
|
-
reset: (currentWindow + 1) * windowSize
|
|
1446
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1447
|
+
limit: tokens
|
|
1287
1448
|
};
|
|
1288
1449
|
},
|
|
1289
1450
|
async resetTokens(ctx, identifier) {
|
|
@@ -1313,12 +1474,15 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1313
1474
|
timeout: config.timeout,
|
|
1314
1475
|
analytics: config.analytics,
|
|
1315
1476
|
ctx: {
|
|
1316
|
-
redis: config.redis
|
|
1477
|
+
redis: config.redis,
|
|
1478
|
+
prefix: config.prefix ?? DEFAULT_PREFIX
|
|
1317
1479
|
},
|
|
1318
1480
|
ephemeralCache: config.ephemeralCache,
|
|
1319
1481
|
enableProtection: config.enableProtection,
|
|
1320
|
-
denyListThreshold: config.denyListThreshold
|
|
1482
|
+
denyListThreshold: config.denyListThreshold,
|
|
1483
|
+
dynamicLimits: config.dynamicLimits
|
|
1321
1484
|
});
|
|
1485
|
+
addTelemetry(config.redis, config.enableTelemetry);
|
|
1322
1486
|
}
|
|
1323
1487
|
/**
|
|
1324
1488
|
* Each request inside a fixed time increases a counter.
|
|
@@ -1358,14 +1522,15 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1358
1522
|
};
|
|
1359
1523
|
}
|
|
1360
1524
|
}
|
|
1361
|
-
const
|
|
1525
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1526
|
+
const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
|
|
1362
1527
|
ctx,
|
|
1363
1528
|
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1364
|
-
[key],
|
|
1365
|
-
[windowDuration, incrementBy]
|
|
1529
|
+
[key, dynamicLimitKey],
|
|
1530
|
+
[tokens, windowDuration, incrementBy]
|
|
1366
1531
|
);
|
|
1367
|
-
const success = usedTokensAfterUpdate <=
|
|
1368
|
-
const remainingTokens = Math.max(0,
|
|
1532
|
+
const success = usedTokensAfterUpdate <= effectiveLimit;
|
|
1533
|
+
const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
|
|
1369
1534
|
const reset = (bucket + 1) * windowDuration;
|
|
1370
1535
|
if (ctx.cache) {
|
|
1371
1536
|
if (!success) {
|
|
@@ -1376,7 +1541,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1376
1541
|
}
|
|
1377
1542
|
return {
|
|
1378
1543
|
success,
|
|
1379
|
-
limit:
|
|
1544
|
+
limit: effectiveLimit,
|
|
1380
1545
|
remaining: remainingTokens,
|
|
1381
1546
|
reset,
|
|
1382
1547
|
pending: Promise.resolve()
|
|
@@ -1385,15 +1550,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1385
1550
|
async getRemaining(ctx, identifier) {
|
|
1386
1551
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1387
1552
|
const key = [identifier, bucket].join(":");
|
|
1388
|
-
const
|
|
1553
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1554
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1389
1555
|
ctx,
|
|
1390
1556
|
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1391
|
-
[key],
|
|
1392
|
-
[
|
|
1557
|
+
[key, dynamicLimitKey],
|
|
1558
|
+
[tokens]
|
|
1393
1559
|
);
|
|
1394
1560
|
return {
|
|
1395
|
-
remaining: Math.max(0,
|
|
1396
|
-
reset: (bucket + 1) * windowDuration
|
|
1561
|
+
remaining: Math.max(0, remaining),
|
|
1562
|
+
reset: (bucket + 1) * windowDuration,
|
|
1563
|
+
limit: effectiveLimit
|
|
1397
1564
|
};
|
|
1398
1565
|
},
|
|
1399
1566
|
async resetTokens(ctx, identifier) {
|
|
@@ -1449,10 +1616,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1449
1616
|
};
|
|
1450
1617
|
}
|
|
1451
1618
|
}
|
|
1452
|
-
const
|
|
1619
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1620
|
+
const [remainingTokens, effectiveLimit] = await safeEval(
|
|
1453
1621
|
ctx,
|
|
1454
1622
|
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1455
|
-
[currentKey, previousKey],
|
|
1623
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1456
1624
|
[tokens, now, windowSize, incrementBy]
|
|
1457
1625
|
);
|
|
1458
1626
|
const success = remainingTokens >= 0;
|
|
@@ -1466,7 +1634,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1466
1634
|
}
|
|
1467
1635
|
return {
|
|
1468
1636
|
success,
|
|
1469
|
-
limit:
|
|
1637
|
+
limit: effectiveLimit,
|
|
1470
1638
|
remaining: Math.max(0, remainingTokens),
|
|
1471
1639
|
reset,
|
|
1472
1640
|
pending: Promise.resolve()
|
|
@@ -1478,15 +1646,17 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1478
1646
|
const currentKey = [identifier, currentWindow].join(":");
|
|
1479
1647
|
const previousWindow = currentWindow - 1;
|
|
1480
1648
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1481
|
-
const
|
|
1649
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1650
|
+
const [remaining, effectiveLimit] = await safeEval(
|
|
1482
1651
|
ctx,
|
|
1483
1652
|
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1484
|
-
[currentKey, previousKey],
|
|
1485
|
-
[now, windowSize]
|
|
1653
|
+
[currentKey, previousKey, dynamicLimitKey],
|
|
1654
|
+
[tokens, now, windowSize]
|
|
1486
1655
|
);
|
|
1487
1656
|
return {
|
|
1488
|
-
remaining: Math.max(0,
|
|
1489
|
-
reset: (currentWindow + 1) * windowSize
|
|
1657
|
+
remaining: Math.max(0, remaining),
|
|
1658
|
+
reset: (currentWindow + 1) * windowSize,
|
|
1659
|
+
limit: effectiveLimit
|
|
1490
1660
|
};
|
|
1491
1661
|
},
|
|
1492
1662
|
async resetTokens(ctx, identifier) {
|
|
@@ -1535,10 +1705,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1535
1705
|
};
|
|
1536
1706
|
}
|
|
1537
1707
|
}
|
|
1538
|
-
const
|
|
1708
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1709
|
+
const [remaining, reset, effectiveLimit] = await safeEval(
|
|
1539
1710
|
ctx,
|
|
1540
1711
|
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1541
|
-
[identifier],
|
|
1712
|
+
[identifier, dynamicLimitKey],
|
|
1542
1713
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1543
1714
|
);
|
|
1544
1715
|
const success = remaining >= 0;
|
|
@@ -1551,24 +1722,26 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1551
1722
|
}
|
|
1552
1723
|
return {
|
|
1553
1724
|
success,
|
|
1554
|
-
limit:
|
|
1555
|
-
remaining,
|
|
1725
|
+
limit: effectiveLimit,
|
|
1726
|
+
remaining: Math.max(0, remaining),
|
|
1556
1727
|
reset,
|
|
1557
1728
|
pending: Promise.resolve()
|
|
1558
1729
|
};
|
|
1559
1730
|
},
|
|
1560
1731
|
async getRemaining(ctx, identifier) {
|
|
1561
|
-
const
|
|
1732
|
+
const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
|
|
1733
|
+
const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
|
|
1562
1734
|
ctx,
|
|
1563
1735
|
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1564
|
-
[identifier],
|
|
1736
|
+
[identifier, dynamicLimitKey],
|
|
1565
1737
|
[maxTokens]
|
|
1566
1738
|
);
|
|
1567
1739
|
const freshRefillAt = Date.now() + intervalDuration;
|
|
1568
1740
|
const identifierRefillsAt = refilledAt + intervalDuration;
|
|
1569
1741
|
return {
|
|
1570
|
-
remaining: remainingTokens,
|
|
1571
|
-
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
|
|
1742
|
+
remaining: Math.max(0, remainingTokens),
|
|
1743
|
+
reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
|
|
1744
|
+
limit: effectiveLimit
|
|
1572
1745
|
};
|
|
1573
1746
|
},
|
|
1574
1747
|
async resetTokens(ctx, identifier) {
|
|
@@ -1616,6 +1789,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1616
1789
|
if (!ctx.cache) {
|
|
1617
1790
|
throw new Error("This algorithm requires a cache");
|
|
1618
1791
|
}
|
|
1792
|
+
if (ctx.dynamicLimits) {
|
|
1793
|
+
console.warn(
|
|
1794
|
+
"Warning: Dynamic limits are not yet supported for cachedFixedWindow algorithm. The dynamicLimits option will be ignored."
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1619
1797
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1620
1798
|
const key = [identifier, bucket].join(":");
|
|
1621
1799
|
const reset = (bucket + 1) * windowDuration;
|
|
@@ -1665,7 +1843,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1665
1843
|
const cachedUsedTokens = ctx.cache.get(key) ?? 0;
|
|
1666
1844
|
return {
|
|
1667
1845
|
remaining: Math.max(0, tokens - cachedUsedTokens),
|
|
1668
|
-
reset: (bucket + 1) * windowDuration
|
|
1846
|
+
reset: (bucket + 1) * windowDuration,
|
|
1847
|
+
limit: tokens
|
|
1669
1848
|
};
|
|
1670
1849
|
}
|
|
1671
1850
|
const usedTokens = await safeEval(
|
|
@@ -1676,7 +1855,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1676
1855
|
);
|
|
1677
1856
|
return {
|
|
1678
1857
|
remaining: Math.max(0, tokens - usedTokens),
|
|
1679
|
-
reset: (bucket + 1) * windowDuration
|
|
1858
|
+
reset: (bucket + 1) * windowDuration,
|
|
1859
|
+
limit: tokens
|
|
1680
1860
|
};
|
|
1681
1861
|
},
|
|
1682
1862
|
async resetTokens(ctx, identifier) {
|