@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.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)$/);
@@ -154,8 +158,19 @@ var safeEval = async (ctx, script, keys, args) => {
154
158
  // src/lua-scripts/single.ts
155
159
  var fixedWindowLimitScript = `
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
184
  var fixedWindowRemainingTokensScript = `
170
- local key = KEYS[1]
171
- local tokens = 0
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
- `;
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
+ `;
179
206
  var slidingWindowLimitScript = `
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
250
  var slidingWindowRemainingTokensScript = `
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
284
  var tokenBucketLimitScript = `
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,38 +314,48 @@ 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
338
  var tokenBucketRemainingTokensScript = `
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
360
  var cachedFixedWindowLimitScript = `
292
361
  local key = KEYS[1]
@@ -430,31 +499,31 @@ var SCRIPTS = {
430
499
  fixedWindow: {
431
500
  limit: {
432
501
  script: fixedWindowLimitScript,
433
- hash: "b13943e359636db027ad280f1def143f02158c13"
502
+ hash: "472e55443b62f60d0991028456c57815a387066d"
434
503
  },
435
504
  getRemaining: {
436
505
  script: fixedWindowRemainingTokensScript,
437
- hash: "8c4c341934502aee132643ffbe58ead3450e5208"
506
+ hash: "40515c9dd0a08f8584f5f9b593935f6a87c1c1c3"
438
507
  }
439
508
  },
440
509
  slidingWindow: {
441
510
  limit: {
442
511
  script: slidingWindowLimitScript,
443
- hash: "9b7842963bd73721f1a3011650c23c0010848ee3"
512
+ hash: "977fb636fb5ceb7e98a96d1b3a1272ba018efdae"
444
513
  },
445
514
  getRemaining: {
446
515
  script: slidingWindowRemainingTokensScript,
447
- hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
516
+ hash: "ee3a3265fad822f83acad23f8a1e2f5c0b156b03"
448
517
  }
449
518
  },
450
519
  tokenBucket: {
451
520
  limit: {
452
521
  script: tokenBucketLimitScript,
453
- hash: "d1f857ebbdaeca90ccd2cd4eada61d7c8e5db1ca"
522
+ hash: "b35c5bc0b7fdae7dd0573d4529911cabaf9d1d89"
454
523
  },
455
524
  getRemaining: {
456
525
  script: tokenBucketRemainingTokensScript,
457
- hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
526
+ hash: "deb03663e8af5a968deee895dd081be553d2611b"
458
527
  }
459
528
  },
460
529
  cachedFixedWindow: {
@@ -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,59 @@ 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
+ };
909
1037
  };
910
1038
 
911
1039
  // src/multi.ts
@@ -928,13 +1056,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
928
1056
  limiter: config.limiter,
929
1057
  timeout: config.timeout,
930
1058
  analytics: config.analytics,
1059
+ dynamicLimits: config.dynamicLimits,
931
1060
  ctx: {
932
1061
  regionContexts: config.redis.map((redis) => ({
933
- redis
1062
+ redis,
1063
+ prefix: config.prefix ?? DEFAULT_PREFIX
934
1064
  })),
935
1065
  cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
936
1066
  }
937
1067
  });
1068
+ if (config.dynamicLimits) {
1069
+ console.warn(
1070
+ "Warning: Dynamic limits are not yet supported for multi-region rate limiters. The dynamicLimits option will be ignored."
1071
+ );
1072
+ }
938
1073
  }
939
1074
  /**
940
1075
  * Each request inside a fixed time increases a counter.
@@ -1084,7 +1219,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
1084
1219
  );
1085
1220
  return {
1086
1221
  remaining: Math.max(0, tokens - usedTokens),
1087
- reset: (bucket + 1) * windowDuration
1222
+ reset: (bucket + 1) * windowDuration,
1223
+ limit: tokens
1088
1224
  };
1089
1225
  },
1090
1226
  async resetTokens(ctx, identifier) {
@@ -1260,7 +1396,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
1260
1396
  const usedTokens = await Promise.any(dbs.map((s) => s.request));
1261
1397
  return {
1262
1398
  remaining: Math.max(0, tokens - usedTokens),
1263
- reset: (currentWindow + 1) * windowSize
1399
+ reset: (currentWindow + 1) * windowSize,
1400
+ limit: tokens
1264
1401
  };
1265
1402
  },
1266
1403
  async resetTokens(ctx, identifier) {
@@ -1290,11 +1427,13 @@ var RegionRatelimit = class extends Ratelimit {
1290
1427
  timeout: config.timeout,
1291
1428
  analytics: config.analytics,
1292
1429
  ctx: {
1293
- redis: config.redis
1430
+ redis: config.redis,
1431
+ prefix: config.prefix ?? DEFAULT_PREFIX
1294
1432
  },
1295
1433
  ephemeralCache: config.ephemeralCache,
1296
1434
  enableProtection: config.enableProtection,
1297
- denyListThreshold: config.denyListThreshold
1435
+ denyListThreshold: config.denyListThreshold,
1436
+ dynamicLimits: config.dynamicLimits
1298
1437
  });
1299
1438
  }
1300
1439
  /**
@@ -1335,14 +1474,15 @@ var RegionRatelimit = class extends Ratelimit {
1335
1474
  };
1336
1475
  }
1337
1476
  }
1338
- const usedTokensAfterUpdate = await safeEval(
1477
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1478
+ const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
1339
1479
  ctx,
1340
1480
  SCRIPTS.singleRegion.fixedWindow.limit,
1341
- [key],
1342
- [windowDuration, incrementBy]
1481
+ [key, dynamicLimitKey],
1482
+ [tokens, windowDuration, incrementBy]
1343
1483
  );
1344
- const success = usedTokensAfterUpdate <= tokens;
1345
- const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
1484
+ const success = usedTokensAfterUpdate <= effectiveLimit;
1485
+ const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
1346
1486
  const reset = (bucket + 1) * windowDuration;
1347
1487
  if (ctx.cache) {
1348
1488
  if (!success) {
@@ -1353,7 +1493,7 @@ var RegionRatelimit = class extends Ratelimit {
1353
1493
  }
1354
1494
  return {
1355
1495
  success,
1356
- limit: tokens,
1496
+ limit: effectiveLimit,
1357
1497
  remaining: remainingTokens,
1358
1498
  reset,
1359
1499
  pending: Promise.resolve()
@@ -1362,15 +1502,17 @@ var RegionRatelimit = class extends Ratelimit {
1362
1502
  async getRemaining(ctx, identifier) {
1363
1503
  const bucket = Math.floor(Date.now() / windowDuration);
1364
1504
  const key = [identifier, bucket].join(":");
1365
- const usedTokens = await safeEval(
1505
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1506
+ const [remaining, effectiveLimit] = await safeEval(
1366
1507
  ctx,
1367
1508
  SCRIPTS.singleRegion.fixedWindow.getRemaining,
1368
- [key],
1369
- [null]
1509
+ [key, dynamicLimitKey],
1510
+ [tokens]
1370
1511
  );
1371
1512
  return {
1372
- remaining: Math.max(0, tokens - usedTokens),
1373
- reset: (bucket + 1) * windowDuration
1513
+ remaining: Math.max(0, remaining),
1514
+ reset: (bucket + 1) * windowDuration,
1515
+ limit: effectiveLimit
1374
1516
  };
1375
1517
  },
1376
1518
  async resetTokens(ctx, identifier) {
@@ -1426,10 +1568,11 @@ var RegionRatelimit = class extends Ratelimit {
1426
1568
  };
1427
1569
  }
1428
1570
  }
1429
- const remainingTokens = await safeEval(
1571
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1572
+ const [remainingTokens, effectiveLimit] = await safeEval(
1430
1573
  ctx,
1431
1574
  SCRIPTS.singleRegion.slidingWindow.limit,
1432
- [currentKey, previousKey],
1575
+ [currentKey, previousKey, dynamicLimitKey],
1433
1576
  [tokens, now, windowSize, incrementBy]
1434
1577
  );
1435
1578
  const success = remainingTokens >= 0;
@@ -1443,7 +1586,7 @@ var RegionRatelimit = class extends Ratelimit {
1443
1586
  }
1444
1587
  return {
1445
1588
  success,
1446
- limit: tokens,
1589
+ limit: effectiveLimit,
1447
1590
  remaining: Math.max(0, remainingTokens),
1448
1591
  reset,
1449
1592
  pending: Promise.resolve()
@@ -1455,15 +1598,17 @@ var RegionRatelimit = class extends Ratelimit {
1455
1598
  const currentKey = [identifier, currentWindow].join(":");
1456
1599
  const previousWindow = currentWindow - 1;
1457
1600
  const previousKey = [identifier, previousWindow].join(":");
1458
- const usedTokens = await safeEval(
1601
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1602
+ const [remaining, effectiveLimit] = await safeEval(
1459
1603
  ctx,
1460
1604
  SCRIPTS.singleRegion.slidingWindow.getRemaining,
1461
- [currentKey, previousKey],
1462
- [now, windowSize]
1605
+ [currentKey, previousKey, dynamicLimitKey],
1606
+ [tokens, now, windowSize]
1463
1607
  );
1464
1608
  return {
1465
- remaining: Math.max(0, tokens - usedTokens),
1466
- reset: (currentWindow + 1) * windowSize
1609
+ remaining: Math.max(0, remaining),
1610
+ reset: (currentWindow + 1) * windowSize,
1611
+ limit: effectiveLimit
1467
1612
  };
1468
1613
  },
1469
1614
  async resetTokens(ctx, identifier) {
@@ -1512,10 +1657,11 @@ var RegionRatelimit = class extends Ratelimit {
1512
1657
  };
1513
1658
  }
1514
1659
  }
1515
- const [remaining, reset] = await safeEval(
1660
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1661
+ const [remaining, reset, effectiveLimit] = await safeEval(
1516
1662
  ctx,
1517
1663
  SCRIPTS.singleRegion.tokenBucket.limit,
1518
- [identifier],
1664
+ [identifier, dynamicLimitKey],
1519
1665
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1520
1666
  );
1521
1667
  const success = remaining >= 0;
@@ -1528,24 +1674,26 @@ var RegionRatelimit = class extends Ratelimit {
1528
1674
  }
1529
1675
  return {
1530
1676
  success,
1531
- limit: maxTokens,
1532
- remaining,
1677
+ limit: effectiveLimit,
1678
+ remaining: Math.max(0, remaining),
1533
1679
  reset,
1534
1680
  pending: Promise.resolve()
1535
1681
  };
1536
1682
  },
1537
1683
  async getRemaining(ctx, identifier) {
1538
- const [remainingTokens, refilledAt] = await safeEval(
1684
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1685
+ const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
1539
1686
  ctx,
1540
1687
  SCRIPTS.singleRegion.tokenBucket.getRemaining,
1541
- [identifier],
1688
+ [identifier, dynamicLimitKey],
1542
1689
  [maxTokens]
1543
1690
  );
1544
1691
  const freshRefillAt = Date.now() + intervalDuration;
1545
1692
  const identifierRefillsAt = refilledAt + intervalDuration;
1546
1693
  return {
1547
- remaining: remainingTokens,
1548
- reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
1694
+ remaining: Math.max(0, remainingTokens),
1695
+ reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
1696
+ limit: effectiveLimit
1549
1697
  };
1550
1698
  },
1551
1699
  async resetTokens(ctx, identifier) {
@@ -1593,6 +1741,11 @@ var RegionRatelimit = class extends Ratelimit {
1593
1741
  if (!ctx.cache) {
1594
1742
  throw new Error("This algorithm requires a cache");
1595
1743
  }
1744
+ if (ctx.dynamicLimits) {
1745
+ console.warn(
1746
+ "Warning: Dynamic limits are not yet supported for cachedFixedWindow algorithm. The dynamicLimits option will be ignored."
1747
+ );
1748
+ }
1596
1749
  const bucket = Math.floor(Date.now() / windowDuration);
1597
1750
  const key = [identifier, bucket].join(":");
1598
1751
  const reset = (bucket + 1) * windowDuration;
@@ -1642,7 +1795,8 @@ var RegionRatelimit = class extends Ratelimit {
1642
1795
  const cachedUsedTokens = ctx.cache.get(key) ?? 0;
1643
1796
  return {
1644
1797
  remaining: Math.max(0, tokens - cachedUsedTokens),
1645
- reset: (bucket + 1) * windowDuration
1798
+ reset: (bucket + 1) * windowDuration,
1799
+ limit: tokens
1646
1800
  };
1647
1801
  }
1648
1802
  const usedTokens = await safeEval(
@@ -1653,7 +1807,8 @@ var RegionRatelimit = class extends Ratelimit {
1653
1807
  );
1654
1808
  return {
1655
1809
  remaining: Math.max(0, tokens - usedTokens),
1656
- reset: (bucket + 1) * windowDuration
1810
+ reset: (bucket + 1) * windowDuration,
1811
+ limit: tokens
1657
1812
  };
1658
1813
  },
1659
1814
  async resetTokens(ctx, identifier) {