@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.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 window = ARGV[1]
158
- local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
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
- local key = KEYS[1]
171
- local tokens = 0
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
- local value = redis.call('GET', key)
174
- if value then
175
- tokens = value
176
- end
177
- return tokens
178
- `;
179
- var slidingWindowLimitScript = `
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 tokens = tonumber(ARGV[1]) -- tokens per window
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 >= tokens then
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 tokens - ( newValue + requestsInPreviousWindow )
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 now = ARGV[1] -- current timestamp in milliseconds
217
- local window = ARGV[2] -- interval in milliseconds
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
- return requestsInPreviousWindow + requestsInCurrentWindow
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 maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
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 = maxTokens
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(maxTokens, tokens + numRefills * refillRate)
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(((maxTokens - remaining) / refillRate)) * interval
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 maxTokens = tonumber(ARGV[1])
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 {maxTokens, ${tokenBucketIdentifierNotFound}}
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: "b13943e359636db027ad280f1def143f02158c13"
502
+ hash: "4ef8749b9e927b157546ebad13061fa86b9ffc2e"
434
503
  },
435
504
  getRemaining: {
436
505
  script: fixedWindowRemainingTokensScript,
437
- hash: "8c4c341934502aee132643ffbe58ead3450e5208"
506
+ hash: "e62252cb05b4676b27a5d396da02f0ca0d375d88"
438
507
  }
439
508
  },
440
509
  slidingWindow: {
441
510
  limit: {
442
511
  script: slidingWindowLimitScript,
443
- hash: "9b7842963bd73721f1a3011650c23c0010848ee3"
512
+ hash: "5cd9665be7533e4dfabb3581021737fc69b41ae8"
444
513
  },
445
514
  getRemaining: {
446
515
  script: slidingWindowRemainingTokensScript,
447
- hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
516
+ hash: "1bdbbdb2082bb557084518c64486d7d5a29d1bfe"
448
517
  }
449
518
  },
450
519
  tokenBucket: {
451
520
  limit: {
452
521
  script: tokenBucketLimitScript,
453
- hash: "d1f857ebbdaeca90ccd2cd4eada61d7c8e5db1ca"
522
+ hash: "8f7286b9b0f65d631f760ba615f7b3c598a569a3"
454
523
  },
455
524
  getRemaining: {
456
525
  script: tokenBucketRemainingTokensScript,
457
- hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
526
+ hash: "08d3b3381c507b6f7bfec1bbfa0a6053434b16bd"
458
527
  }
459
528
  },
460
529
  cachedFixedWindow: {
461
530
  limit: {
462
531
  script: cachedFixedWindowLimitScript,
463
- hash: "c26b12703dd137939b9a69a3a9b18e906a2d940f"
532
+ hash: "1861a700bafe96c833a483af6b1c28a8897cfdb0"
464
533
  },
465
534
  getRemaining: {
466
535
  script: cachedFixedWindowRemainingTokenScript,
467
- hash: "8e8f222ccae68b595ee6e3f3bf2199629a62b91a"
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: "a8c14f3835aa87bd70e5e2116081b81664abcf5c"
544
+ hash: "e04b753a75909b7f99aae4c04cf8869a8de02a9e"
476
545
  },
477
546
  getRemaining: {
478
547
  script: fixedWindowRemainingTokensScript2,
479
- hash: "8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd"
548
+ hash: "e066c8ce3eaca142b0894ad1541e0a58c9924819"
480
549
  }
481
550
  },
482
551
  slidingWindow: {
483
552
  limit: {
484
553
  script: slidingWindowLimitScript2,
485
- hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
554
+ hash: "aeb4d8f381e8dafc8e69041baae1324d254ded44"
486
555
  },
487
556
  getRemaining: {
488
557
  script: slidingWindowRemainingTokensScript2,
489
- hash: "558c9306b7ec54abb50747fe0b17e5d44bd24868"
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 ?? "@upstash/ratelimit";
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 reset fields. `remaining` denotes
792
- * the remaining tokens and reset denotes the timestamp when the
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 usedTokensAfterUpdate = await safeEval(
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 <= tokens;
1345
- const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
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: tokens,
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 usedTokens = await safeEval(
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
- [null]
1534
+ [key, dynamicLimitKey],
1535
+ [tokens]
1370
1536
  );
1371
1537
  return {
1372
- remaining: Math.max(0, tokens - usedTokens),
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 remainingTokens = await safeEval(
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: tokens,
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 usedTokens = await safeEval(
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, tokens - usedTokens),
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 [remaining, reset] = await safeEval(
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: maxTokens,
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 [remainingTokens, refilledAt] = await safeEval(
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) {