@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.mjs CHANGED
@@ -92,9 +92,9 @@ var Cache = class {
92
92
  get(key) {
93
93
  return this.cache.get(key) || null;
94
94
  }
95
- incr(key) {
95
+ incr(key, incrementAmount = 1) {
96
96
  let value = this.cache.get(key) ?? 0;
97
- value += 1;
97
+ value += incrementAmount;
98
98
  this.cache.set(key, value);
99
99
  return value;
100
100
  }
@@ -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,25 +179,47 @@ 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
- local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
213
+ local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
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
186
223
 
187
224
  local requestsInCurrentWindow = redis.call("GET", currentKey)
188
225
  if requestsInCurrentWindow == false then
@@ -196,23 +233,36 @@ var slidingWindowLimitScript = `
196
233
  local percentageInCurrent = ( now % window ) / window
197
234
  -- weighted requests to consider from the previous window
198
235
  requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
199
- if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
200
- return -1
236
+
237
+ -- Only check limit if not refunding (negative rate)
238
+ if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= effectiveLimit then
239
+ return {-1, effectiveLimit}
201
240
  end
202
241
 
203
242
  local newValue = redis.call("INCRBY", currentKey, incrementBy)
204
- if newValue == tonumber(incrementBy) then
243
+ if newValue == incrementBy then
205
244
  -- The first time this key is set, the value will be equal to incrementBy.
206
245
  -- So we only need the expire command once
207
246
  redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
208
247
  end
209
- return tokens - ( newValue + requestsInPreviousWindow )
248
+ return {effectiveLimit - ( newValue + requestsInPreviousWindow ), effectiveLimit}
210
249
  `;
211
250
  var slidingWindowRemainingTokensScript = `
212
251
  local currentKey = KEYS[1] -- identifier including prefixes
213
252
  local previousKey = KEYS[2] -- key of the previous bucket
214
- local now = ARGV[1] -- current timestamp in milliseconds
215
- 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
216
266
 
217
267
  local requestsInCurrentWindow = redis.call("GET", currentKey)
218
268
  if requestsInCurrentWindow == false then
@@ -228,15 +278,26 @@ var slidingWindowRemainingTokensScript = `
228
278
  -- weighted requests to consider from the previous window
229
279
  requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
230
280
 
231
- return requestsInPreviousWindow + requestsInCurrentWindow
281
+ local usedTokens = requestsInPreviousWindow + requestsInCurrentWindow
282
+ return {effectiveLimit - usedTokens, effectiveLimit}
232
283
  `;
233
284
  var tokenBucketLimitScript = `
234
285
  local key = KEYS[1] -- identifier including prefixes
235
- 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
236
288
  local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
237
289
  local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
238
290
  local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
239
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
240
301
 
241
302
  local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
242
303
 
@@ -245,7 +306,7 @@ var tokenBucketLimitScript = `
245
306
 
246
307
  if bucket[1] == false then
247
308
  refilledAt = now
248
- tokens = maxTokens
309
+ tokens = effectiveLimit
249
310
  else
250
311
  refilledAt = tonumber(bucket[1])
251
312
  tokens = tonumber(bucket[2])
@@ -253,34 +314,48 @@ var tokenBucketLimitScript = `
253
314
 
254
315
  if now >= refilledAt + interval then
255
316
  local numRefills = math.floor((now - refilledAt) / interval)
256
- tokens = math.min(maxTokens, tokens + numRefills * refillRate)
317
+ tokens = math.min(effectiveLimit, tokens + numRefills * refillRate)
257
318
 
258
319
  refilledAt = refilledAt + numRefills * interval
259
320
  end
260
321
 
261
- if tokens == 0 then
262
- return {-1, refilledAt + interval}
322
+ -- Only reject if tokens are 0 and we're consuming (not refunding)
323
+ if tokens == 0 and incrementBy > 0 then
324
+ return {-1, refilledAt + interval, effectiveLimit}
263
325
  end
264
326
 
265
327
  local remaining = tokens - incrementBy
266
- local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
328
+ local expireAt = math.ceil(((effectiveLimit - remaining) / refillRate)) * interval
267
329
 
268
330
  redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
269
- redis.call("PEXPIRE", key, expireAt)
270
- return {remaining, refilledAt + interval}
331
+
332
+ if (expireAt > 0) then
333
+ redis.call("PEXPIRE", key, expireAt)
334
+ end
335
+ return {remaining, refilledAt + interval, effectiveLimit}
271
336
  `;
272
337
  var tokenBucketIdentifierNotFound = -1;
273
338
  var tokenBucketRemainingTokensScript = `
274
339
  local key = KEYS[1]
275
- 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
276
351
 
277
352
  local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
278
353
 
279
354
  if bucket[1] == false then
280
- return {maxTokens, ${tokenBucketIdentifierNotFound}}
355
+ return {effectiveLimit, ${tokenBucketIdentifierNotFound}, effectiveLimit}
281
356
  end
282
357
 
283
- return {tonumber(bucket[2]), tonumber(bucket[1])}
358
+ return {tonumber(bucket[2]), tonumber(bucket[1]), effectiveLimit}
284
359
  `;
285
360
  var cachedFixedWindowLimitScript = `
286
361
  local key = KEYS[1]
@@ -354,7 +429,9 @@ var slidingWindowLimitScript2 = `
354
429
  end
355
430
 
356
431
  local percentageInCurrent = ( now % window) / window
357
- if requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
432
+
433
+ -- Only check limit if not refunding (negative rate)
434
+ if incrementBy > 0 and requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow + incrementBy > tokens then
358
435
  return {currentFields, previousFields, false}
359
436
  end
360
437
 
@@ -422,31 +499,31 @@ var SCRIPTS = {
422
499
  fixedWindow: {
423
500
  limit: {
424
501
  script: fixedWindowLimitScript,
425
- hash: "b13943e359636db027ad280f1def143f02158c13"
502
+ hash: "472e55443b62f60d0991028456c57815a387066d"
426
503
  },
427
504
  getRemaining: {
428
505
  script: fixedWindowRemainingTokensScript,
429
- hash: "8c4c341934502aee132643ffbe58ead3450e5208"
506
+ hash: "40515c9dd0a08f8584f5f9b593935f6a87c1c1c3"
430
507
  }
431
508
  },
432
509
  slidingWindow: {
433
510
  limit: {
434
511
  script: slidingWindowLimitScript,
435
- hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
512
+ hash: "977fb636fb5ceb7e98a96d1b3a1272ba018efdae"
436
513
  },
437
514
  getRemaining: {
438
515
  script: slidingWindowRemainingTokensScript,
439
- hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
516
+ hash: "ee3a3265fad822f83acad23f8a1e2f5c0b156b03"
440
517
  }
441
518
  },
442
519
  tokenBucket: {
443
520
  limit: {
444
521
  script: tokenBucketLimitScript,
445
- hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
522
+ hash: "b35c5bc0b7fdae7dd0573d4529911cabaf9d1d89"
446
523
  },
447
524
  getRemaining: {
448
525
  script: tokenBucketRemainingTokensScript,
449
- hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
526
+ hash: "deb03663e8af5a968deee895dd081be553d2611b"
450
527
  }
451
528
  },
452
529
  cachedFixedWindow: {
@@ -474,7 +551,7 @@ var SCRIPTS = {
474
551
  slidingWindow: {
475
552
  limit: {
476
553
  script: slidingWindowLimitScript2,
477
- hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
554
+ hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
478
555
  },
479
556
  getRemaining: {
480
557
  script: slidingWindowRemainingTokensScript2,
@@ -659,14 +736,20 @@ var Ratelimit = class {
659
736
  analytics;
660
737
  enableProtection;
661
738
  denyListThreshold;
739
+ dynamicLimits;
662
740
  constructor(config) {
663
741
  this.ctx = config.ctx;
664
742
  this.limiter = config.limiter;
665
743
  this.timeout = config.timeout ?? 5e3;
666
- this.prefix = config.prefix ?? "@upstash/ratelimit";
744
+ this.prefix = config.prefix ?? DEFAULT_PREFIX;
745
+ this.dynamicLimits = config.dynamicLimits ?? false;
667
746
  this.enableProtection = config.enableProtection ?? false;
668
747
  this.denyListThreshold = config.denyListThreshold ?? 6;
669
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
+ }
670
753
  this.analytics = config.analytics ? new Analytics({
671
754
  redis: this.primaryRedis,
672
755
  prefix: this.prefix
@@ -780,9 +863,9 @@ var Ratelimit = class {
780
863
  * Returns the remaining token count together with a reset timestamps
781
864
  *
782
865
  * @param identifier identifir to check
783
- * @returns object with `remaining` and reset fields. `remaining` denotes
784
- * the remaining tokens and reset denotes the timestamp when the
785
- * 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.
786
869
  */
787
870
  getRemaining = async (identifier) => {
788
871
  const pattern = [this.prefix, identifier].join(":");
@@ -898,6 +981,59 @@ var Ratelimit = class {
898
981
  const members = [identifier, req?.ip, req?.userAgent, req?.country];
899
982
  return members.filter(Boolean);
900
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
+ };
901
1037
  };
902
1038
 
903
1039
  // src/multi.ts
@@ -920,13 +1056,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
920
1056
  limiter: config.limiter,
921
1057
  timeout: config.timeout,
922
1058
  analytics: config.analytics,
1059
+ dynamicLimits: config.dynamicLimits,
923
1060
  ctx: {
924
1061
  regionContexts: config.redis.map((redis) => ({
925
- redis
1062
+ redis,
1063
+ prefix: config.prefix ?? DEFAULT_PREFIX
926
1064
  })),
927
1065
  cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
928
1066
  }
929
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
+ }
930
1073
  }
931
1074
  /**
932
1075
  * Each request inside a fixed time increases a counter.
@@ -950,7 +1093,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
950
1093
  const windowDuration = ms(window);
951
1094
  return () => ({
952
1095
  async limit(ctx, identifier, rate) {
953
- if (ctx.cache) {
1096
+ const requestId = randomId();
1097
+ const bucket = Math.floor(Date.now() / windowDuration);
1098
+ const key = [identifier, bucket].join(":");
1099
+ const incrementBy = rate ?? 1;
1100
+ if (ctx.cache && incrementBy > 0) {
954
1101
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
955
1102
  if (blocked) {
956
1103
  return {
@@ -963,10 +1110,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
963
1110
  };
964
1111
  }
965
1112
  }
966
- const requestId = randomId();
967
- const bucket = Math.floor(Date.now() / windowDuration);
968
- const key = [identifier, bucket].join(":");
969
- const incrementBy = rate ? Math.max(1, rate) : 1;
970
1113
  const dbs = ctx.regionContexts.map((regionContext) => ({
971
1114
  redis: regionContext.redis,
972
1115
  request: safeEval(
@@ -977,24 +1120,29 @@ var MultiRegionRatelimit = class extends Ratelimit {
977
1120
  )
978
1121
  }));
979
1122
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
980
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
981
- let parsedToken = 0;
982
- if (index % 2) {
983
- parsedToken = Number.parseInt(usedToken);
984
- }
985
- return accTokens + parsedToken;
986
- }, 0);
1123
+ const usedTokens = firstResponse.reduce(
1124
+ (accTokens, usedToken, index) => {
1125
+ let parsedToken = 0;
1126
+ if (index % 2) {
1127
+ parsedToken = Number.parseInt(usedToken);
1128
+ }
1129
+ return accTokens + parsedToken;
1130
+ },
1131
+ 0
1132
+ );
987
1133
  const remaining = tokens - usedTokens;
988
1134
  async function sync() {
989
1135
  const individualIDs = await Promise.all(dbs.map((s) => s.request));
990
- const allIDs = [...new Set(
991
- individualIDs.flat().reduce((acc, curr, index) => {
992
- if (index % 2 === 0) {
993
- acc.push(curr);
994
- }
995
- return acc;
996
- }, [])
997
- ).values()];
1136
+ const allIDs = [
1137
+ ...new Set(
1138
+ individualIDs.flat().reduce((acc, curr, index) => {
1139
+ if (index % 2 === 0) {
1140
+ acc.push(curr);
1141
+ }
1142
+ return acc;
1143
+ }, [])
1144
+ ).values()
1145
+ ];
998
1146
  for (const db of dbs) {
999
1147
  const usedDbTokensRequest = await db.request;
1000
1148
  const usedDbTokens = usedDbTokensRequest.reduce(
@@ -1008,12 +1156,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
1008
1156
  0
1009
1157
  );
1010
1158
  const dbIdsRequest = await db.request;
1011
- const dbIds = dbIdsRequest.reduce((ids, currentId, index) => {
1012
- if (index % 2 === 0) {
1013
- ids.push(currentId);
1014
- }
1015
- return ids;
1016
- }, []);
1159
+ const dbIds = dbIdsRequest.reduce(
1160
+ (ids, currentId, index) => {
1161
+ if (index % 2 === 0) {
1162
+ ids.push(currentId);
1163
+ }
1164
+ return ids;
1165
+ },
1166
+ []
1167
+ );
1017
1168
  if (usedDbTokens >= tokens) {
1018
1169
  continue;
1019
1170
  }
@@ -1026,10 +1177,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1026
1177
  }
1027
1178
  }
1028
1179
  }
1029
- const success = remaining > 0;
1180
+ const success = remaining >= 0;
1030
1181
  const reset = (bucket + 1) * windowDuration;
1031
- if (ctx.cache && !success) {
1032
- ctx.cache.blockUntil(identifier, reset);
1182
+ if (ctx.cache) {
1183
+ if (!success) {
1184
+ ctx.cache.blockUntil(identifier, reset);
1185
+ } else if (incrementBy < 0) {
1186
+ ctx.cache.pop(identifier);
1187
+ }
1033
1188
  }
1034
1189
  return {
1035
1190
  success,
@@ -1052,16 +1207,20 @@ var MultiRegionRatelimit = class extends Ratelimit {
1052
1207
  )
1053
1208
  }));
1054
1209
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
1055
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
1056
- let parsedToken = 0;
1057
- if (index % 2) {
1058
- parsedToken = Number.parseInt(usedToken);
1059
- }
1060
- return accTokens + parsedToken;
1061
- }, 0);
1210
+ const usedTokens = firstResponse.reduce(
1211
+ (accTokens, usedToken, index) => {
1212
+ let parsedToken = 0;
1213
+ if (index % 2) {
1214
+ parsedToken = Number.parseInt(usedToken);
1215
+ }
1216
+ return accTokens + parsedToken;
1217
+ },
1218
+ 0
1219
+ );
1062
1220
  return {
1063
1221
  remaining: Math.max(0, tokens - usedTokens),
1064
- reset: (bucket + 1) * windowDuration
1222
+ reset: (bucket + 1) * windowDuration,
1223
+ limit: tokens
1065
1224
  };
1066
1225
  },
1067
1226
  async resetTokens(ctx, identifier) {
@@ -1069,14 +1228,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1069
1228
  if (ctx.cache) {
1070
1229
  ctx.cache.pop(identifier);
1071
1230
  }
1072
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1073
- safeEval(
1074
- regionContext,
1075
- RESET_SCRIPT,
1076
- [pattern],
1077
- [null]
1078
- );
1079
- }));
1231
+ await Promise.all(
1232
+ ctx.regionContexts.map((regionContext) => {
1233
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1234
+ })
1235
+ );
1080
1236
  }
1081
1237
  });
1082
1238
  }
@@ -1101,7 +1257,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1101
1257
  const windowDuration = ms(window);
1102
1258
  return () => ({
1103
1259
  async limit(ctx, identifier, rate) {
1104
- if (ctx.cache) {
1260
+ const requestId = randomId();
1261
+ const now = Date.now();
1262
+ const currentWindow = Math.floor(now / windowSize);
1263
+ const currentKey = [identifier, currentWindow].join(":");
1264
+ const previousWindow = currentWindow - 1;
1265
+ const previousKey = [identifier, previousWindow].join(":");
1266
+ const incrementBy = rate ?? 1;
1267
+ if (ctx.cache && incrementBy > 0) {
1105
1268
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1106
1269
  if (blocked) {
1107
1270
  return {
@@ -1114,13 +1277,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
1114
1277
  };
1115
1278
  }
1116
1279
  }
1117
- const requestId = randomId();
1118
- const now = Date.now();
1119
- const currentWindow = Math.floor(now / windowSize);
1120
- const currentKey = [identifier, currentWindow].join(":");
1121
- const previousWindow = currentWindow - 1;
1122
- const previousKey = [identifier, previousWindow].join(":");
1123
- const incrementBy = rate ? Math.max(1, rate) : 1;
1124
1280
  const dbs = ctx.regionContexts.map((regionContext) => ({
1125
1281
  redis: regionContext.redis,
1126
1282
  request: safeEval(
@@ -1132,37 +1288,49 @@ var MultiRegionRatelimit = class extends Ratelimit {
1132
1288
  )
1133
1289
  }));
1134
1290
  const percentageInCurrent = now % windowDuration / windowDuration;
1135
- const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
1291
+ const [current, previous, success] = await Promise.any(
1292
+ dbs.map((s) => s.request)
1293
+ );
1136
1294
  if (success) {
1137
1295
  current.push(requestId, incrementBy.toString());
1138
1296
  }
1139
- const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
1140
- let parsedToken = 0;
1141
- if (index % 2) {
1142
- parsedToken = Number.parseInt(usedToken);
1143
- }
1144
- return accTokens + parsedToken;
1145
- }, 0);
1146
- const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
1147
- let parsedToken = 0;
1148
- if (index % 2) {
1149
- parsedToken = Number.parseInt(usedToken);
1150
- }
1151
- return accTokens + parsedToken;
1152
- }, 0);
1153
- const previousPartialUsed = Math.ceil(previousUsedTokens * (1 - percentageInCurrent));
1297
+ const previousUsedTokens = previous.reduce(
1298
+ (accTokens, usedToken, index) => {
1299
+ let parsedToken = 0;
1300
+ if (index % 2) {
1301
+ parsedToken = Number.parseInt(usedToken);
1302
+ }
1303
+ return accTokens + parsedToken;
1304
+ },
1305
+ 0
1306
+ );
1307
+ const currentUsedTokens = current.reduce(
1308
+ (accTokens, usedToken, index) => {
1309
+ let parsedToken = 0;
1310
+ if (index % 2) {
1311
+ parsedToken = Number.parseInt(usedToken);
1312
+ }
1313
+ return accTokens + parsedToken;
1314
+ },
1315
+ 0
1316
+ );
1317
+ const previousPartialUsed = Math.ceil(
1318
+ previousUsedTokens * (1 - percentageInCurrent)
1319
+ );
1154
1320
  const usedTokens = previousPartialUsed + currentUsedTokens;
1155
1321
  const remaining = tokens - usedTokens;
1156
1322
  async function sync() {
1157
1323
  const res = await Promise.all(dbs.map((s) => s.request));
1158
- const allCurrentIds = [...new Set(
1159
- res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1160
- if (index % 2 === 0) {
1161
- acc.push(curr);
1162
- }
1163
- return acc;
1164
- }, [])
1165
- ).values()];
1324
+ const allCurrentIds = [
1325
+ ...new Set(
1326
+ res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1327
+ if (index % 2 === 0) {
1328
+ acc.push(curr);
1329
+ }
1330
+ return acc;
1331
+ }, [])
1332
+ ).values()
1333
+ ];
1166
1334
  for (const db of dbs) {
1167
1335
  const [current2, _previous, _success] = await db.request;
1168
1336
  const dbIds = current2.reduce((ids, currentId, index) => {
@@ -1171,13 +1339,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
1171
1339
  }
1172
1340
  return ids;
1173
1341
  }, []);
1174
- const usedDbTokens = current2.reduce((accTokens, usedToken, index) => {
1175
- let parsedToken = 0;
1176
- if (index % 2) {
1177
- parsedToken = Number.parseInt(usedToken);
1178
- }
1179
- return accTokens + parsedToken;
1180
- }, 0);
1342
+ const usedDbTokens = current2.reduce(
1343
+ (accTokens, usedToken, index) => {
1344
+ let parsedToken = 0;
1345
+ if (index % 2) {
1346
+ parsedToken = Number.parseInt(usedToken);
1347
+ }
1348
+ return accTokens + parsedToken;
1349
+ },
1350
+ 0
1351
+ );
1181
1352
  if (usedDbTokens >= tokens) {
1182
1353
  continue;
1183
1354
  }
@@ -1191,8 +1362,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
1191
1362
  }
1192
1363
  }
1193
1364
  const reset = (currentWindow + 1) * windowDuration;
1194
- if (ctx.cache && !success) {
1195
- ctx.cache.blockUntil(identifier, reset);
1365
+ if (ctx.cache) {
1366
+ if (!success) {
1367
+ ctx.cache.blockUntil(identifier, reset);
1368
+ } else if (incrementBy < 0) {
1369
+ ctx.cache.pop(identifier);
1370
+ }
1196
1371
  }
1197
1372
  return {
1198
1373
  success: Boolean(success),
@@ -1221,7 +1396,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
1221
1396
  const usedTokens = await Promise.any(dbs.map((s) => s.request));
1222
1397
  return {
1223
1398
  remaining: Math.max(0, tokens - usedTokens),
1224
- reset: (currentWindow + 1) * windowSize
1399
+ reset: (currentWindow + 1) * windowSize,
1400
+ limit: tokens
1225
1401
  };
1226
1402
  },
1227
1403
  async resetTokens(ctx, identifier) {
@@ -1229,14 +1405,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1229
1405
  if (ctx.cache) {
1230
1406
  ctx.cache.pop(identifier);
1231
1407
  }
1232
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1233
- safeEval(
1234
- regionContext,
1235
- RESET_SCRIPT,
1236
- [pattern],
1237
- [null]
1238
- );
1239
- }));
1408
+ await Promise.all(
1409
+ ctx.regionContexts.map((regionContext) => {
1410
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1411
+ })
1412
+ );
1240
1413
  }
1241
1414
  });
1242
1415
  }
@@ -1254,11 +1427,13 @@ var RegionRatelimit = class extends Ratelimit {
1254
1427
  timeout: config.timeout,
1255
1428
  analytics: config.analytics,
1256
1429
  ctx: {
1257
- redis: config.redis
1430
+ redis: config.redis,
1431
+ prefix: config.prefix ?? DEFAULT_PREFIX
1258
1432
  },
1259
1433
  ephemeralCache: config.ephemeralCache,
1260
1434
  enableProtection: config.enableProtection,
1261
- denyListThreshold: config.denyListThreshold
1435
+ denyListThreshold: config.denyListThreshold,
1436
+ dynamicLimits: config.dynamicLimits
1262
1437
  });
1263
1438
  }
1264
1439
  /**
@@ -1285,7 +1460,8 @@ var RegionRatelimit = class extends Ratelimit {
1285
1460
  async limit(ctx, identifier, rate) {
1286
1461
  const bucket = Math.floor(Date.now() / windowDuration);
1287
1462
  const key = [identifier, bucket].join(":");
1288
- if (ctx.cache) {
1463
+ const incrementBy = rate ?? 1;
1464
+ if (ctx.cache && incrementBy > 0) {
1289
1465
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1290
1466
  if (blocked) {
1291
1467
  return {
@@ -1298,22 +1474,26 @@ var RegionRatelimit = class extends Ratelimit {
1298
1474
  };
1299
1475
  }
1300
1476
  }
1301
- const incrementBy = rate ? Math.max(1, rate) : 1;
1302
- const usedTokensAfterUpdate = await safeEval(
1477
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1478
+ const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
1303
1479
  ctx,
1304
1480
  SCRIPTS.singleRegion.fixedWindow.limit,
1305
- [key],
1306
- [windowDuration, incrementBy]
1481
+ [key, dynamicLimitKey],
1482
+ [tokens, windowDuration, incrementBy]
1307
1483
  );
1308
- const success = usedTokensAfterUpdate <= tokens;
1309
- const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
1484
+ const success = usedTokensAfterUpdate <= effectiveLimit;
1485
+ const remainingTokens = Math.max(0, effectiveLimit - usedTokensAfterUpdate);
1310
1486
  const reset = (bucket + 1) * windowDuration;
1311
- if (ctx.cache && !success) {
1312
- ctx.cache.blockUntil(identifier, reset);
1487
+ if (ctx.cache) {
1488
+ if (!success) {
1489
+ ctx.cache.blockUntil(identifier, reset);
1490
+ } else if (incrementBy < 0) {
1491
+ ctx.cache.pop(identifier);
1492
+ }
1313
1493
  }
1314
1494
  return {
1315
1495
  success,
1316
- limit: tokens,
1496
+ limit: effectiveLimit,
1317
1497
  remaining: remainingTokens,
1318
1498
  reset,
1319
1499
  pending: Promise.resolve()
@@ -1322,15 +1502,17 @@ var RegionRatelimit = class extends Ratelimit {
1322
1502
  async getRemaining(ctx, identifier) {
1323
1503
  const bucket = Math.floor(Date.now() / windowDuration);
1324
1504
  const key = [identifier, bucket].join(":");
1325
- const usedTokens = await safeEval(
1505
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1506
+ const [remaining, effectiveLimit] = await safeEval(
1326
1507
  ctx,
1327
1508
  SCRIPTS.singleRegion.fixedWindow.getRemaining,
1328
- [key],
1329
- [null]
1509
+ [key, dynamicLimitKey],
1510
+ [tokens]
1330
1511
  );
1331
1512
  return {
1332
- remaining: Math.max(0, tokens - usedTokens),
1333
- reset: (bucket + 1) * windowDuration
1513
+ remaining: Math.max(0, remaining),
1514
+ reset: (bucket + 1) * windowDuration,
1515
+ limit: effectiveLimit
1334
1516
  };
1335
1517
  },
1336
1518
  async resetTokens(ctx, identifier) {
@@ -1372,7 +1554,8 @@ var RegionRatelimit = class extends Ratelimit {
1372
1554
  const currentKey = [identifier, currentWindow].join(":");
1373
1555
  const previousWindow = currentWindow - 1;
1374
1556
  const previousKey = [identifier, previousWindow].join(":");
1375
- if (ctx.cache) {
1557
+ const incrementBy = rate ?? 1;
1558
+ if (ctx.cache && incrementBy > 0) {
1376
1559
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1377
1560
  if (blocked) {
1378
1561
  return {
@@ -1385,21 +1568,25 @@ var RegionRatelimit = class extends Ratelimit {
1385
1568
  };
1386
1569
  }
1387
1570
  }
1388
- const incrementBy = rate ? Math.max(1, rate) : 1;
1389
- const remainingTokens = await safeEval(
1571
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1572
+ const [remainingTokens, effectiveLimit] = await safeEval(
1390
1573
  ctx,
1391
1574
  SCRIPTS.singleRegion.slidingWindow.limit,
1392
- [currentKey, previousKey],
1575
+ [currentKey, previousKey, dynamicLimitKey],
1393
1576
  [tokens, now, windowSize, incrementBy]
1394
1577
  );
1395
1578
  const success = remainingTokens >= 0;
1396
1579
  const reset = (currentWindow + 1) * windowSize;
1397
- if (ctx.cache && !success) {
1398
- ctx.cache.blockUntil(identifier, reset);
1580
+ if (ctx.cache) {
1581
+ if (!success) {
1582
+ ctx.cache.blockUntil(identifier, reset);
1583
+ } else if (incrementBy < 0) {
1584
+ ctx.cache.pop(identifier);
1585
+ }
1399
1586
  }
1400
1587
  return {
1401
1588
  success,
1402
- limit: tokens,
1589
+ limit: effectiveLimit,
1403
1590
  remaining: Math.max(0, remainingTokens),
1404
1591
  reset,
1405
1592
  pending: Promise.resolve()
@@ -1411,15 +1598,17 @@ var RegionRatelimit = class extends Ratelimit {
1411
1598
  const currentKey = [identifier, currentWindow].join(":");
1412
1599
  const previousWindow = currentWindow - 1;
1413
1600
  const previousKey = [identifier, previousWindow].join(":");
1414
- const usedTokens = await safeEval(
1601
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1602
+ const [remaining, effectiveLimit] = await safeEval(
1415
1603
  ctx,
1416
1604
  SCRIPTS.singleRegion.slidingWindow.getRemaining,
1417
- [currentKey, previousKey],
1418
- [now, windowSize]
1605
+ [currentKey, previousKey, dynamicLimitKey],
1606
+ [tokens, now, windowSize]
1419
1607
  );
1420
1608
  return {
1421
- remaining: Math.max(0, tokens - usedTokens),
1422
- reset: (currentWindow + 1) * windowSize
1609
+ remaining: Math.max(0, remaining),
1610
+ reset: (currentWindow + 1) * windowSize,
1611
+ limit: effectiveLimit
1423
1612
  };
1424
1613
  },
1425
1614
  async resetTokens(ctx, identifier) {
@@ -1453,7 +1642,9 @@ var RegionRatelimit = class extends Ratelimit {
1453
1642
  const intervalDuration = ms(interval);
1454
1643
  return () => ({
1455
1644
  async limit(ctx, identifier, rate) {
1456
- if (ctx.cache) {
1645
+ const now = Date.now();
1646
+ const incrementBy = rate ?? 1;
1647
+ if (ctx.cache && incrementBy > 0) {
1457
1648
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1458
1649
  if (blocked) {
1459
1650
  return {
@@ -1466,38 +1657,43 @@ var RegionRatelimit = class extends Ratelimit {
1466
1657
  };
1467
1658
  }
1468
1659
  }
1469
- const now = Date.now();
1470
- const incrementBy = rate ? Math.max(1, rate) : 1;
1471
- const [remaining, reset] = await safeEval(
1660
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1661
+ const [remaining, reset, effectiveLimit] = await safeEval(
1472
1662
  ctx,
1473
1663
  SCRIPTS.singleRegion.tokenBucket.limit,
1474
- [identifier],
1664
+ [identifier, dynamicLimitKey],
1475
1665
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1476
1666
  );
1477
1667
  const success = remaining >= 0;
1478
- if (ctx.cache && !success) {
1479
- ctx.cache.blockUntil(identifier, reset);
1668
+ if (ctx.cache) {
1669
+ if (!success) {
1670
+ ctx.cache.blockUntil(identifier, reset);
1671
+ } else if (incrementBy < 0) {
1672
+ ctx.cache.pop(identifier);
1673
+ }
1480
1674
  }
1481
1675
  return {
1482
1676
  success,
1483
- limit: maxTokens,
1484
- remaining,
1677
+ limit: effectiveLimit,
1678
+ remaining: Math.max(0, remaining),
1485
1679
  reset,
1486
1680
  pending: Promise.resolve()
1487
1681
  };
1488
1682
  },
1489
1683
  async getRemaining(ctx, identifier) {
1490
- const [remainingTokens, refilledAt] = await safeEval(
1684
+ const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1685
+ const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
1491
1686
  ctx,
1492
1687
  SCRIPTS.singleRegion.tokenBucket.getRemaining,
1493
- [identifier],
1688
+ [identifier, dynamicLimitKey],
1494
1689
  [maxTokens]
1495
1690
  );
1496
1691
  const freshRefillAt = Date.now() + intervalDuration;
1497
1692
  const identifierRefillsAt = refilledAt + intervalDuration;
1498
1693
  return {
1499
- remaining: remainingTokens,
1500
- reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt
1694
+ remaining: Math.max(0, remainingTokens),
1695
+ reset: refilledAt === tokenBucketIdentifierNotFound ? freshRefillAt : identifierRefillsAt,
1696
+ limit: effectiveLimit
1501
1697
  };
1502
1698
  },
1503
1699
  async resetTokens(ctx, identifier) {
@@ -1545,13 +1741,18 @@ var RegionRatelimit = class extends Ratelimit {
1545
1741
  if (!ctx.cache) {
1546
1742
  throw new Error("This algorithm requires a cache");
1547
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
+ }
1548
1749
  const bucket = Math.floor(Date.now() / windowDuration);
1549
1750
  const key = [identifier, bucket].join(":");
1550
1751
  const reset = (bucket + 1) * windowDuration;
1551
- const incrementBy = rate ? Math.max(1, rate) : 1;
1752
+ const incrementBy = rate ?? 1;
1552
1753
  const hit = typeof ctx.cache.get(key) === "number";
1553
1754
  if (hit) {
1554
- const cachedTokensAfterUpdate = ctx.cache.incr(key);
1755
+ const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
1555
1756
  const success = cachedTokensAfterUpdate < tokens;
1556
1757
  const pending = success ? safeEval(
1557
1758
  ctx,
@@ -1594,7 +1795,8 @@ var RegionRatelimit = class extends Ratelimit {
1594
1795
  const cachedUsedTokens = ctx.cache.get(key) ?? 0;
1595
1796
  return {
1596
1797
  remaining: Math.max(0, tokens - cachedUsedTokens),
1597
- reset: (bucket + 1) * windowDuration
1798
+ reset: (bucket + 1) * windowDuration,
1799
+ limit: tokens
1598
1800
  };
1599
1801
  }
1600
1802
  const usedTokens = await safeEval(
@@ -1605,7 +1807,8 @@ var RegionRatelimit = class extends Ratelimit {
1605
1807
  );
1606
1808
  return {
1607
1809
  remaining: Math.max(0, tokens - usedTokens),
1608
- reset: (bucket + 1) * windowDuration
1810
+ reset: (bucket + 1) * windowDuration,
1811
+ limit: tokens
1609
1812
  };
1610
1813
  },
1611
1814
  async resetTokens(ctx, identifier) {