@upstash/ratelimit 2.0.6 → 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
@@ -115,9 +115,9 @@ var Cache = class {
115
115
  get(key) {
116
116
  return this.cache.get(key) || null;
117
117
  }
118
- incr(key) {
118
+ incr(key, incrementAmount = 1) {
119
119
  let value = this.cache.get(key) ?? 0;
120
- value += 1;
120
+ value += incrementAmount;
121
121
  this.cache.set(key, value);
122
122
  return value;
123
123
  }
@@ -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,25 +202,47 @@ 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
- local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
236
+ local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
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
209
246
 
210
247
  local requestsInCurrentWindow = redis.call("GET", currentKey)
211
248
  if requestsInCurrentWindow == false then
@@ -219,23 +256,36 @@ var slidingWindowLimitScript = `
219
256
  local percentageInCurrent = ( now % window ) / window
220
257
  -- weighted requests to consider from the previous window
221
258
  requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
222
- if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
223
- return -1
259
+
260
+ -- Only check limit if not refunding (negative rate)
261
+ if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= effectiveLimit then
262
+ return {-1, effectiveLimit}
224
263
  end
225
264
 
226
265
  local newValue = redis.call("INCRBY", currentKey, incrementBy)
227
- if newValue == tonumber(incrementBy) then
266
+ if newValue == incrementBy then
228
267
  -- The first time this key is set, the value will be equal to incrementBy.
229
268
  -- So we only need the expire command once
230
269
  redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
231
270
  end
232
- return tokens - ( newValue + requestsInPreviousWindow )
271
+ return {effectiveLimit - ( newValue + requestsInPreviousWindow ), effectiveLimit}
233
272
  `;
234
273
  var slidingWindowRemainingTokensScript = `
235
274
  local currentKey = KEYS[1] -- identifier including prefixes
236
275
  local previousKey = KEYS[2] -- key of the previous bucket
237
- local now = ARGV[1] -- current timestamp in milliseconds
238
- 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
239
289
 
240
290
  local requestsInCurrentWindow = redis.call("GET", currentKey)
241
291
  if requestsInCurrentWindow == false then
@@ -251,15 +301,26 @@ var slidingWindowRemainingTokensScript = `
251
301
  -- weighted requests to consider from the previous window
252
302
  requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
253
303
 
254
- return requestsInPreviousWindow + requestsInCurrentWindow
304
+ local usedTokens = requestsInPreviousWindow + requestsInCurrentWindow
305
+ return {effectiveLimit - usedTokens, effectiveLimit}
255
306
  `;
256
307
  var tokenBucketLimitScript = `
257
308
  local key = KEYS[1] -- identifier including prefixes
258
- 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
259
311
  local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
260
312
  local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
261
313
  local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
262
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
263
324
 
264
325
  local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
265
326
 
@@ -268,7 +329,7 @@ var tokenBucketLimitScript = `
268
329
 
269
330
  if bucket[1] == false then
270
331
  refilledAt = now
271
- tokens = maxTokens
332
+ tokens = effectiveLimit
272
333
  else
273
334
  refilledAt = tonumber(bucket[1])
274
335
  tokens = tonumber(bucket[2])
@@ -276,34 +337,48 @@ var tokenBucketLimitScript = `
276
337
 
277
338
  if now >= refilledAt + interval then
278
339
  local numRefills = math.floor((now - refilledAt) / interval)
279
- tokens = math.min(maxTokens, tokens + numRefills * refillRate)
340
+ tokens = math.min(effectiveLimit, tokens + numRefills * refillRate)
280
341
 
281
342
  refilledAt = refilledAt + numRefills * interval
282
343
  end
283
344
 
284
- if tokens == 0 then
285
- return {-1, refilledAt + interval}
345
+ -- Only reject if tokens are 0 and we're consuming (not refunding)
346
+ if tokens == 0 and incrementBy > 0 then
347
+ return {-1, refilledAt + interval, effectiveLimit}
286
348
  end
287
349
 
288
350
  local remaining = tokens - incrementBy
289
- local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
351
+ local expireAt = math.ceil(((effectiveLimit - remaining) / refillRate)) * interval
290
352
 
291
353
  redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
292
- redis.call("PEXPIRE", key, expireAt)
293
- return {remaining, refilledAt + interval}
354
+
355
+ if (expireAt > 0) then
356
+ redis.call("PEXPIRE", key, expireAt)
357
+ end
358
+ return {remaining, refilledAt + interval, effectiveLimit}
294
359
  `;
295
360
  var tokenBucketIdentifierNotFound = -1;
296
361
  var tokenBucketRemainingTokensScript = `
297
362
  local key = KEYS[1]
298
- 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
299
374
 
300
375
  local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
301
376
 
302
377
  if bucket[1] == false then
303
- return {maxTokens, ${tokenBucketIdentifierNotFound}}
378
+ return {effectiveLimit, ${tokenBucketIdentifierNotFound}, effectiveLimit}
304
379
  end
305
380
 
306
- return {tonumber(bucket[2]), tonumber(bucket[1])}
381
+ return {tonumber(bucket[2]), tonumber(bucket[1]), effectiveLimit}
307
382
  `;
308
383
  var cachedFixedWindowLimitScript = `
309
384
  local key = KEYS[1]
@@ -377,7 +452,9 @@ var slidingWindowLimitScript2 = `
377
452
  end
378
453
 
379
454
  local percentageInCurrent = ( now % window) / window
380
- if requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
455
+
456
+ -- Only check limit if not refunding (negative rate)
457
+ if incrementBy > 0 and requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow + incrementBy > tokens then
381
458
  return {currentFields, previousFields, false}
382
459
  end
383
460
 
@@ -445,31 +522,31 @@ var SCRIPTS = {
445
522
  fixedWindow: {
446
523
  limit: {
447
524
  script: fixedWindowLimitScript,
448
- hash: "b13943e359636db027ad280f1def143f02158c13"
525
+ hash: "472e55443b62f60d0991028456c57815a387066d"
449
526
  },
450
527
  getRemaining: {
451
528
  script: fixedWindowRemainingTokensScript,
452
- hash: "8c4c341934502aee132643ffbe58ead3450e5208"
529
+ hash: "40515c9dd0a08f8584f5f9b593935f6a87c1c1c3"
453
530
  }
454
531
  },
455
532
  slidingWindow: {
456
533
  limit: {
457
534
  script: slidingWindowLimitScript,
458
- hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
535
+ hash: "977fb636fb5ceb7e98a96d1b3a1272ba018efdae"
459
536
  },
460
537
  getRemaining: {
461
538
  script: slidingWindowRemainingTokensScript,
462
- hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
539
+ hash: "ee3a3265fad822f83acad23f8a1e2f5c0b156b03"
463
540
  }
464
541
  },
465
542
  tokenBucket: {
466
543
  limit: {
467
544
  script: tokenBucketLimitScript,
468
- hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
545
+ hash: "b35c5bc0b7fdae7dd0573d4529911cabaf9d1d89"
469
546
  },
470
547
  getRemaining: {
471
548
  script: tokenBucketRemainingTokensScript,
472
- hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
549
+ hash: "deb03663e8af5a968deee895dd081be553d2611b"
473
550
  }
474
551
  },
475
552
  cachedFixedWindow: {
@@ -497,7 +574,7 @@ var SCRIPTS = {
497
574
  slidingWindow: {
498
575
  limit: {
499
576
  script: slidingWindowLimitScript2,
500
- hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
577
+ hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
501
578
  },
502
579
  getRemaining: {
503
580
  script: slidingWindowRemainingTokensScript2,
@@ -682,14 +759,20 @@ var Ratelimit = class {
682
759
  analytics;
683
760
  enableProtection;
684
761
  denyListThreshold;
762
+ dynamicLimits;
685
763
  constructor(config) {
686
764
  this.ctx = config.ctx;
687
765
  this.limiter = config.limiter;
688
766
  this.timeout = config.timeout ?? 5e3;
689
- this.prefix = config.prefix ?? "@upstash/ratelimit";
767
+ this.prefix = config.prefix ?? DEFAULT_PREFIX;
768
+ this.dynamicLimits = config.dynamicLimits ?? false;
690
769
  this.enableProtection = config.enableProtection ?? false;
691
770
  this.denyListThreshold = config.denyListThreshold ?? 6;
692
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
+ }
693
776
  this.analytics = config.analytics ? new Analytics({
694
777
  redis: this.primaryRedis,
695
778
  prefix: this.prefix
@@ -803,9 +886,9 @@ var Ratelimit = class {
803
886
  * Returns the remaining token count together with a reset timestamps
804
887
  *
805
888
  * @param identifier identifir to check
806
- * @returns object with `remaining` and reset fields. `remaining` denotes
807
- * the remaining tokens and reset denotes the timestamp when the
808
- * 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.
809
892
  */
810
893
  getRemaining = async (identifier) => {
811
894
  const pattern = [this.prefix, identifier].join(":");
@@ -921,6 +1004,59 @@ var Ratelimit = class {
921
1004
  const members = [identifier, req?.ip, req?.userAgent, req?.country];
922
1005
  return members.filter(Boolean);
923
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
+ };
924
1060
  };
925
1061
 
926
1062
  // src/multi.ts
@@ -943,13 +1079,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
943
1079
  limiter: config.limiter,
944
1080
  timeout: config.timeout,
945
1081
  analytics: config.analytics,
1082
+ dynamicLimits: config.dynamicLimits,
946
1083
  ctx: {
947
1084
  regionContexts: config.redis.map((redis) => ({
948
- redis
1085
+ redis,
1086
+ prefix: config.prefix ?? DEFAULT_PREFIX
949
1087
  })),
950
1088
  cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
951
1089
  }
952
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
+ }
953
1096
  }
954
1097
  /**
955
1098
  * Each request inside a fixed time increases a counter.
@@ -973,7 +1116,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
973
1116
  const windowDuration = ms(window);
974
1117
  return () => ({
975
1118
  async limit(ctx, identifier, rate) {
976
- if (ctx.cache) {
1119
+ const requestId = randomId();
1120
+ const bucket = Math.floor(Date.now() / windowDuration);
1121
+ const key = [identifier, bucket].join(":");
1122
+ const incrementBy = rate ?? 1;
1123
+ if (ctx.cache && incrementBy > 0) {
977
1124
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
978
1125
  if (blocked) {
979
1126
  return {
@@ -986,10 +1133,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
986
1133
  };
987
1134
  }
988
1135
  }
989
- const requestId = randomId();
990
- const bucket = Math.floor(Date.now() / windowDuration);
991
- const key = [identifier, bucket].join(":");
992
- const incrementBy = rate ? Math.max(1, rate) : 1;
993
1136
  const dbs = ctx.regionContexts.map((regionContext) => ({
994
1137
  redis: regionContext.redis,
995
1138
  request: safeEval(
@@ -1000,24 +1143,29 @@ var MultiRegionRatelimit = class extends Ratelimit {
1000
1143
  )
1001
1144
  }));
1002
1145
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
1003
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
1004
- let parsedToken = 0;
1005
- if (index % 2) {
1006
- parsedToken = Number.parseInt(usedToken);
1007
- }
1008
- return accTokens + parsedToken;
1009
- }, 0);
1146
+ const usedTokens = firstResponse.reduce(
1147
+ (accTokens, usedToken, index) => {
1148
+ let parsedToken = 0;
1149
+ if (index % 2) {
1150
+ parsedToken = Number.parseInt(usedToken);
1151
+ }
1152
+ return accTokens + parsedToken;
1153
+ },
1154
+ 0
1155
+ );
1010
1156
  const remaining = tokens - usedTokens;
1011
1157
  async function sync() {
1012
1158
  const individualIDs = await Promise.all(dbs.map((s) => s.request));
1013
- const allIDs = [...new Set(
1014
- individualIDs.flat().reduce((acc, curr, index) => {
1015
- if (index % 2 === 0) {
1016
- acc.push(curr);
1017
- }
1018
- return acc;
1019
- }, [])
1020
- ).values()];
1159
+ const allIDs = [
1160
+ ...new Set(
1161
+ individualIDs.flat().reduce((acc, curr, index) => {
1162
+ if (index % 2 === 0) {
1163
+ acc.push(curr);
1164
+ }
1165
+ return acc;
1166
+ }, [])
1167
+ ).values()
1168
+ ];
1021
1169
  for (const db of dbs) {
1022
1170
  const usedDbTokensRequest = await db.request;
1023
1171
  const usedDbTokens = usedDbTokensRequest.reduce(
@@ -1031,12 +1179,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
1031
1179
  0
1032
1180
  );
1033
1181
  const dbIdsRequest = await db.request;
1034
- const dbIds = dbIdsRequest.reduce((ids, currentId, index) => {
1035
- if (index % 2 === 0) {
1036
- ids.push(currentId);
1037
- }
1038
- return ids;
1039
- }, []);
1182
+ const dbIds = dbIdsRequest.reduce(
1183
+ (ids, currentId, index) => {
1184
+ if (index % 2 === 0) {
1185
+ ids.push(currentId);
1186
+ }
1187
+ return ids;
1188
+ },
1189
+ []
1190
+ );
1040
1191
  if (usedDbTokens >= tokens) {
1041
1192
  continue;
1042
1193
  }
@@ -1049,10 +1200,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1049
1200
  }
1050
1201
  }
1051
1202
  }
1052
- const success = remaining > 0;
1203
+ const success = remaining >= 0;
1053
1204
  const reset = (bucket + 1) * windowDuration;
1054
- if (ctx.cache && !success) {
1055
- ctx.cache.blockUntil(identifier, reset);
1205
+ if (ctx.cache) {
1206
+ if (!success) {
1207
+ ctx.cache.blockUntil(identifier, reset);
1208
+ } else if (incrementBy < 0) {
1209
+ ctx.cache.pop(identifier);
1210
+ }
1056
1211
  }
1057
1212
  return {
1058
1213
  success,
@@ -1075,16 +1230,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
1075
1230
  )
1076
1231
  }));
1077
1232
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
1078
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
1079
- let parsedToken = 0;
1080
- if (index % 2) {
1081
- parsedToken = Number.parseInt(usedToken);
1082
- }
1083
- return accTokens + parsedToken;
1084
- }, 0);
1233
+ const usedTokens = firstResponse.reduce(
1234
+ (accTokens, usedToken, index) => {
1235
+ let parsedToken = 0;
1236
+ if (index % 2) {
1237
+ parsedToken = Number.parseInt(usedToken);
1238
+ }
1239
+ return accTokens + parsedToken;
1240
+ },
1241
+ 0
1242
+ );
1085
1243
  return {
1086
1244
  remaining: Math.max(0, tokens - usedTokens),
1087
- reset: (bucket + 1) * windowDuration
1245
+ reset: (bucket + 1) * windowDuration,
1246
+ limit: tokens
1088
1247
  };
1089
1248
  },
1090
1249
  async resetTokens(ctx, identifier) {
@@ -1092,14 +1251,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1092
1251
  if (ctx.cache) {
1093
1252
  ctx.cache.pop(identifier);
1094
1253
  }
1095
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1096
- safeEval(
1097
- regionContext,
1098
- RESET_SCRIPT,
1099
- [pattern],
1100
- [null]
1101
- );
1102
- }));
1254
+ await Promise.all(
1255
+ ctx.regionContexts.map((regionContext) => {
1256
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1257
+ })
1258
+ );
1103
1259
  }
1104
1260
  });
1105
1261
  }
@@ -1124,7 +1280,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1124
1280
  const windowDuration = ms(window);
1125
1281
  return () => ({
1126
1282
  async limit(ctx, identifier, rate) {
1127
- if (ctx.cache) {
1283
+ const requestId = randomId();
1284
+ const now = Date.now();
1285
+ const currentWindow = Math.floor(now / windowSize);
1286
+ const currentKey = [identifier, currentWindow].join(":");
1287
+ const previousWindow = currentWindow - 1;
1288
+ const previousKey = [identifier, previousWindow].join(":");
1289
+ const incrementBy = rate ?? 1;
1290
+ if (ctx.cache && incrementBy > 0) {
1128
1291
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1129
1292
  if (blocked) {
1130
1293
  return {
@@ -1137,13 +1300,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
1137
1300
  };
1138
1301
  }
1139
1302
  }
1140
- const requestId = randomId();
1141
- const now = Date.now();
1142
- const currentWindow = Math.floor(now / windowSize);
1143
- const currentKey = [identifier, currentWindow].join(":");
1144
- const previousWindow = currentWindow - 1;
1145
- const previousKey = [identifier, previousWindow].join(":");
1146
- const incrementBy = rate ? Math.max(1, rate) : 1;
1147
1303
  const dbs = ctx.regionContexts.map((regionContext) => ({
1148
1304
  redis: regionContext.redis,
1149
1305
  request: safeEval(
@@ -1155,37 +1311,49 @@ var MultiRegionRatelimit = class extends Ratelimit {
1155
1311
  )
1156
1312
  }));
1157
1313
  const percentageInCurrent = now % windowDuration / windowDuration;
1158
- const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
1314
+ const [current, previous, success] = await Promise.any(
1315
+ dbs.map((s) => s.request)
1316
+ );
1159
1317
  if (success) {
1160
1318
  current.push(requestId, incrementBy.toString());
1161
1319
  }
1162
- const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
1163
- let parsedToken = 0;
1164
- if (index % 2) {
1165
- parsedToken = Number.parseInt(usedToken);
1166
- }
1167
- return accTokens + parsedToken;
1168
- }, 0);
1169
- const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
1170
- let parsedToken = 0;
1171
- if (index % 2) {
1172
- parsedToken = Number.parseInt(usedToken);
1173
- }
1174
- return accTokens + parsedToken;
1175
- }, 0);
1176
- const previousPartialUsed = Math.ceil(previousUsedTokens * (1 - percentageInCurrent));
1320
+ const previousUsedTokens = previous.reduce(
1321
+ (accTokens, usedToken, index) => {
1322
+ let parsedToken = 0;
1323
+ if (index % 2) {
1324
+ parsedToken = Number.parseInt(usedToken);
1325
+ }
1326
+ return accTokens + parsedToken;
1327
+ },
1328
+ 0
1329
+ );
1330
+ const currentUsedTokens = current.reduce(
1331
+ (accTokens, usedToken, index) => {
1332
+ let parsedToken = 0;
1333
+ if (index % 2) {
1334
+ parsedToken = Number.parseInt(usedToken);
1335
+ }
1336
+ return accTokens + parsedToken;
1337
+ },
1338
+ 0
1339
+ );
1340
+ const previousPartialUsed = Math.ceil(
1341
+ previousUsedTokens * (1 - percentageInCurrent)
1342
+ );
1177
1343
  const usedTokens = previousPartialUsed + currentUsedTokens;
1178
1344
  const remaining = tokens - usedTokens;
1179
1345
  async function sync() {
1180
1346
  const res = await Promise.all(dbs.map((s) => s.request));
1181
- const allCurrentIds = [...new Set(
1182
- res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1183
- if (index % 2 === 0) {
1184
- acc.push(curr);
1185
- }
1186
- return acc;
1187
- }, [])
1188
- ).values()];
1347
+ const allCurrentIds = [
1348
+ ...new Set(
1349
+ res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1350
+ if (index % 2 === 0) {
1351
+ acc.push(curr);
1352
+ }
1353
+ return acc;
1354
+ }, [])
1355
+ ).values()
1356
+ ];
1189
1357
  for (const db of dbs) {
1190
1358
  const [current2, _previous, _success] = await db.request;
1191
1359
  const dbIds = current2.reduce((ids, currentId, index) => {
@@ -1194,13 +1362,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
1194
1362
  }
1195
1363
  return ids;
1196
1364
  }, []);
1197
- const usedDbTokens = current2.reduce((accTokens, usedToken, index) => {
1198
- let parsedToken = 0;
1199
- if (index % 2) {
1200
- parsedToken = Number.parseInt(usedToken);
1201
- }
1202
- return accTokens + parsedToken;
1203
- }, 0);
1365
+ const usedDbTokens = current2.reduce(
1366
+ (accTokens, usedToken, index) => {
1367
+ let parsedToken = 0;
1368
+ if (index % 2) {
1369
+ parsedToken = Number.parseInt(usedToken);
1370
+ }
1371
+ return accTokens + parsedToken;
1372
+ },
1373
+ 0
1374
+ );
1204
1375
  if (usedDbTokens >= tokens) {
1205
1376
  continue;
1206
1377
  }
@@ -1214,8 +1385,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
1214
1385
  }
1215
1386
  }
1216
1387
  const reset = (currentWindow + 1) * windowDuration;
1217
- if (ctx.cache && !success) {
1218
- ctx.cache.blockUntil(identifier, reset);
1388
+ if (ctx.cache) {
1389
+ if (!success) {
1390
+ ctx.cache.blockUntil(identifier, reset);
1391
+ } else if (incrementBy < 0) {
1392
+ ctx.cache.pop(identifier);
1393
+ }
1219
1394
  }
1220
1395
  return {
1221
1396
  success: Boolean(success),
@@ -1244,7 +1419,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
1244
1419
  const usedTokens = await Promise.any(dbs.map((s) => s.request));
1245
1420
  return {
1246
1421
  remaining: Math.max(0, tokens - usedTokens),
1247
- reset: (currentWindow + 1) * windowSize
1422
+ reset: (currentWindow + 1) * windowSize,
1423
+ limit: tokens
1248
1424
  };
1249
1425
  },
1250
1426
  async resetTokens(ctx, identifier) {
@@ -1252,14 +1428,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1252
1428
  if (ctx.cache) {
1253
1429
  ctx.cache.pop(identifier);
1254
1430
  }
1255
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1256
- safeEval(
1257
- regionContext,
1258
- RESET_SCRIPT,
1259
- [pattern],
1260
- [null]
1261
- );
1262
- }));
1431
+ await Promise.all(
1432
+ ctx.regionContexts.map((regionContext) => {
1433
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1434
+ })
1435
+ );
1263
1436
  }
1264
1437
  });
1265
1438
  }
@@ -1277,11 +1450,13 @@ var RegionRatelimit = class extends Ratelimit {
1277
1450
  timeout: config.timeout,
1278
1451
  analytics: config.analytics,
1279
1452
  ctx: {
1280
- redis: config.redis
1453
+ redis: config.redis,
1454
+ prefix: config.prefix ?? DEFAULT_PREFIX
1281
1455
  },
1282
1456
  ephemeralCache: config.ephemeralCache,
1283
1457
  enableProtection: config.enableProtection,
1284
- denyListThreshold: config.denyListThreshold
1458
+ denyListThreshold: config.denyListThreshold,
1459
+ dynamicLimits: config.dynamicLimits
1285
1460
  });
1286
1461
  }
1287
1462
  /**
@@ -1308,7 +1483,8 @@ var RegionRatelimit = class extends Ratelimit {
1308
1483
  async limit(ctx, identifier, rate) {
1309
1484
  const bucket = Math.floor(Date.now() / windowDuration);
1310
1485
  const key = [identifier, bucket].join(":");
1311
- if (ctx.cache) {
1486
+ const incrementBy = rate ?? 1;
1487
+ if (ctx.cache && incrementBy > 0) {
1312
1488
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1313
1489
  if (blocked) {
1314
1490
  return {
@@ -1321,22 +1497,26 @@ var RegionRatelimit = class extends Ratelimit {
1321
1497
  };
1322
1498
  }
1323
1499
  }
1324
- const incrementBy = rate ? Math.max(1, rate) : 1;
1325
- const usedTokensAfterUpdate = await safeEval(
1500
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1501
+ const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
1326
1502
  ctx,
1327
1503
  SCRIPTS.singleRegion.fixedWindow.limit,
1328
- [key],
1329
- [windowDuration, incrementBy]
1504
+ [key, dynamicLimitKey],
1505
+ [tokens, windowDuration, incrementBy]
1330
1506
  );
1331
- const success = usedTokensAfterUpdate <= tokens;
1332
- const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
1507
+ const success = usedTokensAfterUpdate <= effectiveLimit;
1508
+ const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
1333
1509
  const reset = (bucket + 1) * windowDuration;
1334
- if (ctx.cache && !success) {
1335
- ctx.cache.blockUntil(identifier, reset);
1510
+ if (ctx.cache) {
1511
+ if (!success) {
1512
+ ctx.cache.blockUntil(identifier, reset);
1513
+ } else if (incrementBy < 0) {
1514
+ ctx.cache.pop(identifier);
1515
+ }
1336
1516
  }
1337
1517
  return {
1338
1518
  success,
1339
- limit: tokens,
1519
+ limit: effectiveLimit,
1340
1520
  remaining: remainingTokens,
1341
1521
  reset,
1342
1522
  pending: Promise.resolve()
@@ -1345,15 +1525,17 @@ var RegionRatelimit = class extends Ratelimit {
1345
1525
  async getRemaining(ctx, identifier) {
1346
1526
  const bucket = Math.floor(Date.now() / windowDuration);
1347
1527
  const key = [identifier, bucket].join(":");
1348
- const usedTokens = await safeEval(
1528
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1529
+ const [remaining, effectiveLimit] = await safeEval(
1349
1530
  ctx,
1350
1531
  SCRIPTS.singleRegion.fixedWindow.getRemaining,
1351
- [key],
1352
- [null]
1532
+ [key, dynamicLimitKey],
1533
+ [tokens]
1353
1534
  );
1354
1535
  return {
1355
- remaining: Math.max(0, tokens - usedTokens),
1356
- reset: (bucket + 1) * windowDuration
1536
+ remaining: Math.max(0, remaining),
1537
+ reset: (bucket + 1) * windowDuration,
1538
+ limit: effectiveLimit
1357
1539
  };
1358
1540
  },
1359
1541
  async resetTokens(ctx, identifier) {
@@ -1395,7 +1577,8 @@ var RegionRatelimit = class extends Ratelimit {
1395
1577
  const currentKey = [identifier, currentWindow].join(":");
1396
1578
  const previousWindow = currentWindow - 1;
1397
1579
  const previousKey = [identifier, previousWindow].join(":");
1398
- if (ctx.cache) {
1580
+ const incrementBy = rate ?? 1;
1581
+ if (ctx.cache && incrementBy > 0) {
1399
1582
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1400
1583
  if (blocked) {
1401
1584
  return {
@@ -1408,21 +1591,25 @@ var RegionRatelimit = class extends Ratelimit {
1408
1591
  };
1409
1592
  }
1410
1593
  }
1411
- const incrementBy = rate ? Math.max(1, rate) : 1;
1412
- const remainingTokens = await safeEval(
1594
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1595
+ const [remainingTokens, effectiveLimit] = await safeEval(
1413
1596
  ctx,
1414
1597
  SCRIPTS.singleRegion.slidingWindow.limit,
1415
- [currentKey, previousKey],
1598
+ [currentKey, previousKey, dynamicLimitKey],
1416
1599
  [tokens, now, windowSize, incrementBy]
1417
1600
  );
1418
1601
  const success = remainingTokens >= 0;
1419
1602
  const reset = (currentWindow + 1) * windowSize;
1420
- if (ctx.cache && !success) {
1421
- ctx.cache.blockUntil(identifier, reset);
1603
+ if (ctx.cache) {
1604
+ if (!success) {
1605
+ ctx.cache.blockUntil(identifier, reset);
1606
+ } else if (incrementBy < 0) {
1607
+ ctx.cache.pop(identifier);
1608
+ }
1422
1609
  }
1423
1610
  return {
1424
1611
  success,
1425
- limit: tokens,
1612
+ limit: effectiveLimit,
1426
1613
  remaining: Math.max(0, remainingTokens),
1427
1614
  reset,
1428
1615
  pending: Promise.resolve()
@@ -1434,15 +1621,17 @@ var RegionRatelimit = class extends Ratelimit {
1434
1621
  const currentKey = [identifier, currentWindow].join(":");
1435
1622
  const previousWindow = currentWindow - 1;
1436
1623
  const previousKey = [identifier, previousWindow].join(":");
1437
- const usedTokens = await safeEval(
1624
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1625
+ const [remaining, effectiveLimit] = await safeEval(
1438
1626
  ctx,
1439
1627
  SCRIPTS.singleRegion.slidingWindow.getRemaining,
1440
- [currentKey, previousKey],
1441
- [now, windowSize]
1628
+ [currentKey, previousKey, dynamicLimitKey],
1629
+ [tokens, now, windowSize]
1442
1630
  );
1443
1631
  return {
1444
- remaining: Math.max(0, tokens - usedTokens),
1445
- reset: (currentWindow + 1) * windowSize
1632
+ remaining: Math.max(0, remaining),
1633
+ reset: (currentWindow + 1) * windowSize,
1634
+ limit: effectiveLimit
1446
1635
  };
1447
1636
  },
1448
1637
  async resetTokens(ctx, identifier) {
@@ -1476,7 +1665,9 @@ var RegionRatelimit = class extends Ratelimit {
1476
1665
  const intervalDuration = ms(interval);
1477
1666
  return () => ({
1478
1667
  async limit(ctx, identifier, rate) {
1479
- if (ctx.cache) {
1668
+ const now = Date.now();
1669
+ const incrementBy = rate ?? 1;
1670
+ if (ctx.cache && incrementBy > 0) {
1480
1671
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1481
1672
  if (blocked) {
1482
1673
  return {
@@ -1489,38 +1680,43 @@ var RegionRatelimit = class extends Ratelimit {
1489
1680
  };
1490
1681
  }
1491
1682
  }
1492
- const now = Date.now();
1493
- const incrementBy = rate ? Math.max(1, rate) : 1;
1494
- const [remaining, reset] = await safeEval(
1683
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1684
+ const [remaining, reset, effectiveLimit] = await safeEval(
1495
1685
  ctx,
1496
1686
  SCRIPTS.singleRegion.tokenBucket.limit,
1497
- [identifier],
1687
+ [identifier, dynamicLimitKey],
1498
1688
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1499
1689
  );
1500
1690
  const success = remaining >= 0;
1501
- if (ctx.cache && !success) {
1502
- ctx.cache.blockUntil(identifier, reset);
1691
+ if (ctx.cache) {
1692
+ if (!success) {
1693
+ ctx.cache.blockUntil(identifier, reset);
1694
+ } else if (incrementBy < 0) {
1695
+ ctx.cache.pop(identifier);
1696
+ }
1503
1697
  }
1504
1698
  return {
1505
1699
  success,
1506
- limit: maxTokens,
1507
- remaining,
1700
+ limit: effectiveLimit,
1701
+ remaining: Math.max(0, remaining),
1508
1702
  reset,
1509
1703
  pending: Promise.resolve()
1510
1704
  };
1511
1705
  },
1512
1706
  async getRemaining(ctx, identifier) {
1513
- const [remainingTokens, refilledAt] = await safeEval(
1707
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1708
+ const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
1514
1709
  ctx,
1515
1710
  SCRIPTS.singleRegion.tokenBucket.getRemaining,
1516
- [identifier],
1711
+ [identifier, dynamicLimitKey],
1517
1712
  [maxTokens]
1518
1713
  );
1519
1714
  const freshRefillAt = Date.now() + intervalDuration;
1520
1715
  const identifierRefillsAt = refilledAt + intervalDuration;
1521
1716
  return {
1522
- remaining: remainingTokens,
1523
- reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
1717
+ remaining: Math.max(0, remainingTokens),
1718
+ reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
1719
+ limit: effectiveLimit
1524
1720
  };
1525
1721
  },
1526
1722
  async resetTokens(ctx, identifier) {
@@ -1568,13 +1764,18 @@ var RegionRatelimit = class extends Ratelimit {
1568
1764
  if (!ctx.cache) {
1569
1765
  throw new Error("This algorithm requires a cache");
1570
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
+ }
1571
1772
  const bucket = Math.floor(Date.now() / windowDuration);
1572
1773
  const key = [identifier, bucket].join(":");
1573
1774
  const reset = (bucket + 1) * windowDuration;
1574
- const incrementBy = rate ? Math.max(1, rate) : 1;
1775
+ const incrementBy = rate ?? 1;
1575
1776
  const hit = typeof ctx.cache.get(key) === "number";
1576
1777
  if (hit) {
1577
- const cachedTokensAfterUpdate = ctx.cache.incr(key);
1778
+ const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
1578
1779
  const success = cachedTokensAfterUpdate < tokens;
1579
1780
  const pending = success ? safeEval(
1580
1781
  ctx,
@@ -1617,7 +1818,8 @@ var RegionRatelimit = class extends Ratelimit {
1617
1818
  const cachedUsedTokens = ctx.cache.get(key) ?? 0;
1618
1819
  return {
1619
1820
  remaining: Math.max(0, tokens - cachedUsedTokens),
1620
- reset: (bucket + 1) * windowDuration
1821
+ reset: (bucket + 1) * windowDuration,
1822
+ limit: tokens
1621
1823
  };
1622
1824
  }
1623
1825
  const usedTokens = await safeEval(
@@ -1628,7 +1830,8 @@ var RegionRatelimit = class extends Ratelimit {
1628
1830
  );
1629
1831
  return {
1630
1832
  remaining: Math.max(0, tokens - usedTokens),
1631
- reset: (bucket + 1) * windowDuration
1833
+ reset: (bucket + 1) * windowDuration,
1834
+ limit: tokens
1632
1835
  };
1633
1836
  },
1634
1837
  async resetTokens(ctx, identifier) {