@upstash/ratelimit 2.0.7 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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)$/);
@@ -177,8 +181,19 @@ var safeEval = async (ctx, script, keys, args) => {
177
181
  // src/lua-scripts/single.ts
178
182
  var fixedWindowLimitScript = `
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
207
  var fixedWindowRemainingTokensScript = `
193
- local key = KEYS[1]
194
- local tokens = 0
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
- `;
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
+ `;
202
229
  var slidingWindowLimitScript = `
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
273
  var slidingWindowRemainingTokensScript = `
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
307
  var tokenBucketLimitScript = `
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,38 +337,48 @@ 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
361
  var tokenBucketRemainingTokensScript = `
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
383
  var cachedFixedWindowLimitScript = `
315
384
  local key = KEYS[1]
@@ -453,31 +522,31 @@ var SCRIPTS = {
453
522
  fixedWindow: {
454
523
  limit: {
455
524
  script: fixedWindowLimitScript,
456
- hash: "b13943e359636db027ad280f1def143f02158c13"
525
+ hash: "472e55443b62f60d0991028456c57815a387066d"
457
526
  },
458
527
  getRemaining: {
459
528
  script: fixedWindowRemainingTokensScript,
460
- hash: "8c4c341934502aee132643ffbe58ead3450e5208"
529
+ hash: "40515c9dd0a08f8584f5f9b593935f6a87c1c1c3"
461
530
  }
462
531
  },
463
532
  slidingWindow: {
464
533
  limit: {
465
534
  script: slidingWindowLimitScript,
466
- hash: "9b7842963bd73721f1a3011650c23c0010848ee3"
535
+ hash: "977fb636fb5ceb7e98a96d1b3a1272ba018efdae"
467
536
  },
468
537
  getRemaining: {
469
538
  script: slidingWindowRemainingTokensScript,
470
- hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
539
+ hash: "ee3a3265fad822f83acad23f8a1e2f5c0b156b03"
471
540
  }
472
541
  },
473
542
  tokenBucket: {
474
543
  limit: {
475
544
  script: tokenBucketLimitScript,
476
- hash: "d1f857ebbdaeca90ccd2cd4eada61d7c8e5db1ca"
545
+ hash: "b35c5bc0b7fdae7dd0573d4529911cabaf9d1d89"
477
546
  },
478
547
  getRemaining: {
479
548
  script: tokenBucketRemainingTokensScript,
480
- hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
549
+ hash: "deb03663e8af5a968deee895dd081be553d2611b"
481
550
  }
482
551
  },
483
552
  cachedFixedWindow: {
@@ -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,59 @@ 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
+ };
932
1060
  };
933
1061
 
934
1062
  // src/multi.ts
@@ -951,13 +1079,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
951
1079
  limiter: config.limiter,
952
1080
  timeout: config.timeout,
953
1081
  analytics: config.analytics,
1082
+ dynamicLimits: config.dynamicLimits,
954
1083
  ctx: {
955
1084
  regionContexts: config.redis.map((redis) => ({
956
- redis
1085
+ redis,
1086
+ prefix: config.prefix ?? DEFAULT_PREFIX
957
1087
  })),
958
1088
  cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
959
1089
  }
960
1090
  });
1091
+ if (config.dynamicLimits) {
1092
+ console.warn(
1093
+ "Warning: Dynamic limits are not yet supported for multi-region rate limiters. The dynamicLimits option will be ignored."
1094
+ );
1095
+ }
961
1096
  }
962
1097
  /**
963
1098
  * Each request inside a fixed time increases a counter.
@@ -1107,7 +1242,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
1107
1242
  );
1108
1243
  return {
1109
1244
  remaining: Math.max(0, tokens - usedTokens),
1110
- reset: (bucket + 1) * windowDuration
1245
+ reset: (bucket + 1) * windowDuration,
1246
+ limit: tokens
1111
1247
  };
1112
1248
  },
1113
1249
  async resetTokens(ctx, identifier) {
@@ -1283,7 +1419,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
1283
1419
  const usedTokens = await Promise.any(dbs.map((s) => s.request));
1284
1420
  return {
1285
1421
  remaining: Math.max(0, tokens - usedTokens),
1286
- reset: (currentWindow + 1) * windowSize
1422
+ reset: (currentWindow + 1) * windowSize,
1423
+ limit: tokens
1287
1424
  };
1288
1425
  },
1289
1426
  async resetTokens(ctx, identifier) {
@@ -1313,11 +1450,13 @@ var RegionRatelimit = class extends Ratelimit {
1313
1450
  timeout: config.timeout,
1314
1451
  analytics: config.analytics,
1315
1452
  ctx: {
1316
- redis: config.redis
1453
+ redis: config.redis,
1454
+ prefix: config.prefix ?? DEFAULT_PREFIX
1317
1455
  },
1318
1456
  ephemeralCache: config.ephemeralCache,
1319
1457
  enableProtection: config.enableProtection,
1320
- denyListThreshold: config.denyListThreshold
1458
+ denyListThreshold: config.denyListThreshold,
1459
+ dynamicLimits: config.dynamicLimits
1321
1460
  });
1322
1461
  }
1323
1462
  /**
@@ -1358,14 +1497,15 @@ var RegionRatelimit = class extends Ratelimit {
1358
1497
  };
1359
1498
  }
1360
1499
  }
1361
- const usedTokensAfterUpdate = await safeEval(
1500
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1501
+ const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
1362
1502
  ctx,
1363
1503
  SCRIPTS.singleRegion.fixedWindow.limit,
1364
- [key],
1365
- [windowDuration, incrementBy]
1504
+ [key, dynamicLimitKey],
1505
+ [tokens, windowDuration, incrementBy]
1366
1506
  );
1367
- const success = usedTokensAfterUpdate <= tokens;
1368
- const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
1507
+ const success = usedTokensAfterUpdate <= effectiveLimit;
1508
+ const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
1369
1509
  const reset = (bucket + 1) * windowDuration;
1370
1510
  if (ctx.cache) {
1371
1511
  if (!success) {
@@ -1376,7 +1516,7 @@ var RegionRatelimit = class extends Ratelimit {
1376
1516
  }
1377
1517
  return {
1378
1518
  success,
1379
- limit: tokens,
1519
+ limit: effectiveLimit,
1380
1520
  remaining: remainingTokens,
1381
1521
  reset,
1382
1522
  pending: Promise.resolve()
@@ -1385,15 +1525,17 @@ var RegionRatelimit = class extends Ratelimit {
1385
1525
  async getRemaining(ctx, identifier) {
1386
1526
  const bucket = Math.floor(Date.now() / windowDuration);
1387
1527
  const key = [identifier, bucket].join(":");
1388
- const usedTokens = await safeEval(
1528
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1529
+ const [remaining, effectiveLimit] = await safeEval(
1389
1530
  ctx,
1390
1531
  SCRIPTS.singleRegion.fixedWindow.getRemaining,
1391
- [key],
1392
- [null]
1532
+ [key, dynamicLimitKey],
1533
+ [tokens]
1393
1534
  );
1394
1535
  return {
1395
- remaining: Math.max(0, tokens - usedTokens),
1396
- reset: (bucket + 1) * windowDuration
1536
+ remaining: Math.max(0, remaining),
1537
+ reset: (bucket + 1) * windowDuration,
1538
+ limit: effectiveLimit
1397
1539
  };
1398
1540
  },
1399
1541
  async resetTokens(ctx, identifier) {
@@ -1449,10 +1591,11 @@ var RegionRatelimit = class extends Ratelimit {
1449
1591
  };
1450
1592
  }
1451
1593
  }
1452
- const remainingTokens = await safeEval(
1594
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1595
+ const [remainingTokens, effectiveLimit] = await safeEval(
1453
1596
  ctx,
1454
1597
  SCRIPTS.singleRegion.slidingWindow.limit,
1455
- [currentKey, previousKey],
1598
+ [currentKey, previousKey, dynamicLimitKey],
1456
1599
  [tokens, now, windowSize, incrementBy]
1457
1600
  );
1458
1601
  const success = remainingTokens >= 0;
@@ -1466,7 +1609,7 @@ var RegionRatelimit = class extends Ratelimit {
1466
1609
  }
1467
1610
  return {
1468
1611
  success,
1469
- limit: tokens,
1612
+ limit: effectiveLimit,
1470
1613
  remaining: Math.max(0, remainingTokens),
1471
1614
  reset,
1472
1615
  pending: Promise.resolve()
@@ -1478,15 +1621,17 @@ var RegionRatelimit = class extends Ratelimit {
1478
1621
  const currentKey = [identifier, currentWindow].join(":");
1479
1622
  const previousWindow = currentWindow - 1;
1480
1623
  const previousKey = [identifier, previousWindow].join(":");
1481
- const usedTokens = await safeEval(
1624
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1625
+ const [remaining, effectiveLimit] = await safeEval(
1482
1626
  ctx,
1483
1627
  SCRIPTS.singleRegion.slidingWindow.getRemaining,
1484
- [currentKey, previousKey],
1485
- [now, windowSize]
1628
+ [currentKey, previousKey, dynamicLimitKey],
1629
+ [tokens, now, windowSize]
1486
1630
  );
1487
1631
  return {
1488
- remaining: Math.max(0, tokens - usedTokens),
1489
- reset: (currentWindow + 1) * windowSize
1632
+ remaining: Math.max(0, remaining),
1633
+ reset: (currentWindow + 1) * windowSize,
1634
+ limit: effectiveLimit
1490
1635
  };
1491
1636
  },
1492
1637
  async resetTokens(ctx, identifier) {
@@ -1535,10 +1680,11 @@ var RegionRatelimit = class extends Ratelimit {
1535
1680
  };
1536
1681
  }
1537
1682
  }
1538
- const [remaining, reset] = await safeEval(
1683
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1684
+ const [remaining, reset, effectiveLimit] = await safeEval(
1539
1685
  ctx,
1540
1686
  SCRIPTS.singleRegion.tokenBucket.limit,
1541
- [identifier],
1687
+ [identifier, dynamicLimitKey],
1542
1688
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1543
1689
  );
1544
1690
  const success = remaining >= 0;
@@ -1551,24 +1697,26 @@ var RegionRatelimit = class extends Ratelimit {
1551
1697
  }
1552
1698
  return {
1553
1699
  success,
1554
- limit: maxTokens,
1555
- remaining,
1700
+ limit: effectiveLimit,
1701
+ remaining: Math.max(0, remaining),
1556
1702
  reset,
1557
1703
  pending: Promise.resolve()
1558
1704
  };
1559
1705
  },
1560
1706
  async getRemaining(ctx, identifier) {
1561
- const [remainingTokens, refilledAt] = await safeEval(
1707
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1708
+ const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
1562
1709
  ctx,
1563
1710
  SCRIPTS.singleRegion.tokenBucket.getRemaining,
1564
- [identifier],
1711
+ [identifier, dynamicLimitKey],
1565
1712
  [maxTokens]
1566
1713
  );
1567
1714
  const freshRefillAt = Date.now() + intervalDuration;
1568
1715
  const identifierRefillsAt = refilledAt + intervalDuration;
1569
1716
  return {
1570
- remaining: remainingTokens,
1571
- reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
1717
+ remaining: Math.max(0, remainingTokens),
1718
+ reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
1719
+ limit: effectiveLimit
1572
1720
  };
1573
1721
  },
1574
1722
  async resetTokens(ctx, identifier) {
@@ -1616,6 +1764,11 @@ var RegionRatelimit = class extends Ratelimit {
1616
1764
  if (!ctx.cache) {
1617
1765
  throw new Error("This algorithm requires a cache");
1618
1766
  }
1767
+ if (ctx.dynamicLimits) {
1768
+ console.warn(
1769
+ "Warning: Dynamic limits are not yet supported for cachedFixedWindow algorithm. The dynamicLimits option will be ignored."
1770
+ );
1771
+ }
1619
1772
  const bucket = Math.floor(Date.now() / windowDuration);
1620
1773
  const key = [identifier, bucket].join(":");
1621
1774
  const reset = (bucket + 1) * windowDuration;
@@ -1665,7 +1818,8 @@ var RegionRatelimit = class extends Ratelimit {
1665
1818
  const cachedUsedTokens = ctx.cache.get(key) ?? 0;
1666
1819
  return {
1667
1820
  remaining: Math.max(0, tokens - cachedUsedTokens),
1668
- reset: (bucket + 1) * windowDuration
1821
+ reset: (bucket + 1) * windowDuration,
1822
+ limit: tokens
1669
1823
  };
1670
1824
  }
1671
1825
  const usedTokens = await safeEval(
@@ -1676,7 +1830,8 @@ var RegionRatelimit = class extends Ratelimit {
1676
1830
  );
1677
1831
  return {
1678
1832
  remaining: Math.max(0, tokens - usedTokens),
1679
- reset: (bucket + 1) * windowDuration
1833
+ reset: (bucket + 1) * windowDuration,
1834
+ limit: tokens
1680
1835
  };
1681
1836
  },
1682
1837
  async resetTokens(ctx, identifier) {