@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/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 window = ARGV[1]
181
- local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
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
- local key = KEYS[1]
194
- local tokens = 0
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
- local value = redis.call('GET', key)
197
- if value then
198
- tokens = value
199
- end
200
- return tokens
201
- `;
202
- var slidingWindowLimitScript = `
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 tokens = tonumber(ARGV[1]) -- tokens per window
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 >= tokens then
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 tokens - ( newValue + requestsInPreviousWindow )
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 now = ARGV[1] -- current timestamp in milliseconds
240
- local window = ARGV[2] -- interval in milliseconds
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
- return requestsInPreviousWindow + requestsInCurrentWindow
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 maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
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 = maxTokens
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(maxTokens, tokens + numRefills * refillRate)
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(((maxTokens - remaining) / refillRate)) * interval
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 maxTokens = tonumber(ARGV[1])
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 {maxTokens, ${tokenBucketIdentifierNotFound}}
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: "b13943e359636db027ad280f1def143f02158c13"
525
+ hash: "4ef8749b9e927b157546ebad13061fa86b9ffc2e"
457
526
  },
458
527
  getRemaining: {
459
528
  script: fixedWindowRemainingTokensScript,
460
- hash: "8c4c341934502aee132643ffbe58ead3450e5208"
529
+ hash: "e62252cb05b4676b27a5d396da02f0ca0d375d88"
461
530
  }
462
531
  },
463
532
  slidingWindow: {
464
533
  limit: {
465
534
  script: slidingWindowLimitScript,
466
- hash: "9b7842963bd73721f1a3011650c23c0010848ee3"
535
+ hash: "5cd9665be7533e4dfabb3581021737fc69b41ae8"
467
536
  },
468
537
  getRemaining: {
469
538
  script: slidingWindowRemainingTokensScript,
470
- hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
539
+ hash: "1bdbbdb2082bb557084518c64486d7d5a29d1bfe"
471
540
  }
472
541
  },
473
542
  tokenBucket: {
474
543
  limit: {
475
544
  script: tokenBucketLimitScript,
476
- hash: "d1f857ebbdaeca90ccd2cd4eada61d7c8e5db1ca"
545
+ hash: "8f7286b9b0f65d631f760ba615f7b3c598a569a3"
477
546
  },
478
547
  getRemaining: {
479
548
  script: tokenBucketRemainingTokensScript,
480
- hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
549
+ hash: "08d3b3381c507b6f7bfec1bbfa0a6053434b16bd"
481
550
  }
482
551
  },
483
552
  cachedFixedWindow: {
484
553
  limit: {
485
554
  script: cachedFixedWindowLimitScript,
486
- hash: "c26b12703dd137939b9a69a3a9b18e906a2d940f"
555
+ hash: "1861a700bafe96c833a483af6b1c28a8897cfdb0"
487
556
  },
488
557
  getRemaining: {
489
558
  script: cachedFixedWindowRemainingTokenScript,
490
- hash: "8e8f222ccae68b595ee6e3f3bf2199629a62b91a"
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: "a8c14f3835aa87bd70e5e2116081b81664abcf5c"
567
+ hash: "e04b753a75909b7f99aae4c04cf8869a8de02a9e"
499
568
  },
500
569
  getRemaining: {
501
570
  script: fixedWindowRemainingTokensScript2,
502
- hash: "8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd"
571
+ hash: "e066c8ce3eaca142b0894ad1541e0a58c9924819"
503
572
  }
504
573
  },
505
574
  slidingWindow: {
506
575
  limit: {
507
576
  script: slidingWindowLimitScript2,
508
- hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
577
+ hash: "aeb4d8f381e8dafc8e69041baae1324d254ded44"
509
578
  },
510
579
  getRemaining: {
511
580
  script: slidingWindowRemainingTokensScript2,
512
- hash: "558c9306b7ec54abb50747fe0b17e5d44bd24868"
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 ?? "@upstash/ratelimit";
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 reset fields. `remaining` denotes
815
- * the remaining tokens and reset denotes the timestamp when the
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 usedTokensAfterUpdate = await safeEval(
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 <= tokens;
1368
- const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
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: tokens,
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 usedTokens = await safeEval(
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
- [null]
1557
+ [key, dynamicLimitKey],
1558
+ [tokens]
1393
1559
  );
1394
1560
  return {
1395
- remaining: Math.max(0, tokens - usedTokens),
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 remainingTokens = await safeEval(
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: tokens,
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 usedTokens = await safeEval(
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, tokens - usedTokens),
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 [remaining, reset] = await safeEval(
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: maxTokens,
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 [remainingTokens, refilledAt] = await safeEval(
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) {