@upstash/ratelimit 2.0.5 → 2.0.7

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.d.mts CHANGED
@@ -12,7 +12,7 @@ type EphemeralCache = {
12
12
  blockUntil: (identifier: string, reset: number) => void;
13
13
  set: (key: string, value: number) => void;
14
14
  get: (key: string) => number | null;
15
- incr: (key: string) => number;
15
+ incr: (key: string, incrementAmount?: number) => number;
16
16
  pop: (key: string) => void;
17
17
  empty: () => void;
18
18
  size: () => number;
@@ -497,7 +497,7 @@ declare class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
497
497
  window: Duration): Algorithm<MultiRegionContext>;
498
498
  }
499
499
 
500
- type Redis = Pick<Redis$1, "get" | "set">;
500
+ type Redis = Pick<Redis$1, "evalsha" | "get" | "set">;
501
501
  type RegionRatelimitConfig = {
502
502
  /**
503
503
  * Instance of `@upstash/redis`
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ type EphemeralCache = {
12
12
  blockUntil: (identifier: string, reset: number) => void;
13
13
  set: (key: string, value: number) => void;
14
14
  get: (key: string) => number | null;
15
- incr: (key: string) => number;
15
+ incr: (key: string, incrementAmount?: number) => number;
16
16
  pop: (key: string) => void;
17
17
  empty: () => void;
18
18
  size: () => number;
@@ -497,7 +497,7 @@ declare class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
497
497
  window: Duration): Algorithm<MultiRegionContext>;
498
498
  }
499
499
 
500
- type Redis = Pick<Redis$1, "get" | "set">;
500
+ type Redis = Pick<Redis$1, "evalsha" | "get" | "set">;
501
501
  type RegionRatelimitConfig = {
502
502
  /**
503
503
  * Instance of `@upstash/redis`
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
  }
@@ -168,13 +168,7 @@ var safeEval = async (ctx, script, keys, args) => {
168
168
  return await ctx.redis.evalsha(script.hash, keys, args);
169
169
  } catch (error) {
170
170
  if (`${error}`.includes("NOSCRIPT")) {
171
- const hash = await ctx.redis.scriptLoad(script.script);
172
- if (hash !== script.hash) {
173
- console.warn(
174
- "Upstash Ratelimit: Expected hash and the hash received from Redis are different. Ratelimit will work as usual but performance will be reduced."
175
- );
176
- }
177
- return await ctx.redis.evalsha(hash, keys, args);
171
+ return await ctx.redis.eval(script.script, keys, args);
178
172
  }
179
173
  throw error;
180
174
  }
@@ -211,7 +205,7 @@ var slidingWindowLimitScript = `
211
205
  local tokens = tonumber(ARGV[1]) -- tokens per window
212
206
  local now = ARGV[2] -- current timestamp in milliseconds
213
207
  local window = ARGV[3] -- interval in milliseconds
214
- local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
208
+ local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
215
209
 
216
210
  local requestsInCurrentWindow = redis.call("GET", currentKey)
217
211
  if requestsInCurrentWindow == false then
@@ -225,12 +219,14 @@ var slidingWindowLimitScript = `
225
219
  local percentageInCurrent = ( now % window ) / window
226
220
  -- weighted requests to consider from the previous window
227
221
  requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
228
- if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
222
+
223
+ -- Only check limit if not refunding (negative rate)
224
+ if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
229
225
  return -1
230
226
  end
231
227
 
232
228
  local newValue = redis.call("INCRBY", currentKey, incrementBy)
233
- if newValue == tonumber(incrementBy) then
229
+ if newValue == incrementBy then
234
230
  -- The first time this key is set, the value will be equal to incrementBy.
235
231
  -- So we only need the expire command once
236
232
  redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
@@ -287,7 +283,8 @@ var tokenBucketLimitScript = `
287
283
  refilledAt = refilledAt + numRefills * interval
288
284
  end
289
285
 
290
- if tokens == 0 then
286
+ -- Only reject if tokens are 0 and we're consuming (not refunding)
287
+ if tokens == 0 and incrementBy > 0 then
291
288
  return {-1, refilledAt + interval}
292
289
  end
293
290
 
@@ -295,7 +292,10 @@ var tokenBucketLimitScript = `
295
292
  local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
296
293
 
297
294
  redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
298
- redis.call("PEXPIRE", key, expireAt)
295
+
296
+ if (expireAt > 0) then
297
+ redis.call("PEXPIRE", key, expireAt)
298
+ end
299
299
  return {remaining, refilledAt + interval}
300
300
  `;
301
301
  var tokenBucketIdentifierNotFound = -1;
@@ -383,7 +383,9 @@ var slidingWindowLimitScript2 = `
383
383
  end
384
384
 
385
385
  local percentageInCurrent = ( now % window) / window
386
- if requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
386
+
387
+ -- Only check limit if not refunding (negative rate)
388
+ if incrementBy > 0 and requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow + incrementBy > tokens then
387
389
  return {currentFields, previousFields, false}
388
390
  end
389
391
 
@@ -461,7 +463,7 @@ var SCRIPTS = {
461
463
  slidingWindow: {
462
464
  limit: {
463
465
  script: slidingWindowLimitScript,
464
- hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
466
+ hash: "9b7842963bd73721f1a3011650c23c0010848ee3"
465
467
  },
466
468
  getRemaining: {
467
469
  script: slidingWindowRemainingTokensScript,
@@ -471,7 +473,7 @@ var SCRIPTS = {
471
473
  tokenBucket: {
472
474
  limit: {
473
475
  script: tokenBucketLimitScript,
474
- hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
476
+ hash: "d1f857ebbdaeca90ccd2cd4eada61d7c8e5db1ca"
475
477
  },
476
478
  getRemaining: {
477
479
  script: tokenBucketRemainingTokensScript,
@@ -503,7 +505,7 @@ var SCRIPTS = {
503
505
  slidingWindow: {
504
506
  limit: {
505
507
  script: slidingWindowLimitScript2,
506
- hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
508
+ hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
507
509
  },
508
510
  getRemaining: {
509
511
  script: slidingWindowRemainingTokensScript2,
@@ -979,7 +981,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
979
981
  const windowDuration = ms(window);
980
982
  return () => ({
981
983
  async limit(ctx, identifier, rate) {
982
- if (ctx.cache) {
984
+ const requestId = randomId();
985
+ const bucket = Math.floor(Date.now() / windowDuration);
986
+ const key = [identifier, bucket].join(":");
987
+ const incrementBy = rate ?? 1;
988
+ if (ctx.cache && incrementBy > 0) {
983
989
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
984
990
  if (blocked) {
985
991
  return {
@@ -992,10 +998,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
992
998
  };
993
999
  }
994
1000
  }
995
- const requestId = randomId();
996
- const bucket = Math.floor(Date.now() / windowDuration);
997
- const key = [identifier, bucket].join(":");
998
- const incrementBy = rate ? Math.max(1, rate) : 1;
999
1001
  const dbs = ctx.regionContexts.map((regionContext) => ({
1000
1002
  redis: regionContext.redis,
1001
1003
  request: safeEval(
@@ -1006,24 +1008,29 @@ var MultiRegionRatelimit = class extends Ratelimit {
1006
1008
  )
1007
1009
  }));
1008
1010
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
1009
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
1010
- let parsedToken = 0;
1011
- if (index % 2) {
1012
- parsedToken = Number.parseInt(usedToken);
1013
- }
1014
- return accTokens + parsedToken;
1015
- }, 0);
1011
+ const usedTokens = firstResponse.reduce(
1012
+ (accTokens, usedToken, index) => {
1013
+ let parsedToken = 0;
1014
+ if (index % 2) {
1015
+ parsedToken = Number.parseInt(usedToken);
1016
+ }
1017
+ return accTokens + parsedToken;
1018
+ },
1019
+ 0
1020
+ );
1016
1021
  const remaining = tokens - usedTokens;
1017
1022
  async function sync() {
1018
1023
  const individualIDs = await Promise.all(dbs.map((s) => s.request));
1019
- const allIDs = [...new Set(
1020
- individualIDs.flat().reduce((acc, curr, index) => {
1021
- if (index % 2 === 0) {
1022
- acc.push(curr);
1023
- }
1024
- return acc;
1025
- }, [])
1026
- ).values()];
1024
+ const allIDs = [
1025
+ ...new Set(
1026
+ individualIDs.flat().reduce((acc, curr, index) => {
1027
+ if (index % 2 === 0) {
1028
+ acc.push(curr);
1029
+ }
1030
+ return acc;
1031
+ }, [])
1032
+ ).values()
1033
+ ];
1027
1034
  for (const db of dbs) {
1028
1035
  const usedDbTokensRequest = await db.request;
1029
1036
  const usedDbTokens = usedDbTokensRequest.reduce(
@@ -1037,12 +1044,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
1037
1044
  0
1038
1045
  );
1039
1046
  const dbIdsRequest = await db.request;
1040
- const dbIds = dbIdsRequest.reduce((ids, currentId, index) => {
1041
- if (index % 2 === 0) {
1042
- ids.push(currentId);
1043
- }
1044
- return ids;
1045
- }, []);
1047
+ const dbIds = dbIdsRequest.reduce(
1048
+ (ids, currentId, index) => {
1049
+ if (index % 2 === 0) {
1050
+ ids.push(currentId);
1051
+ }
1052
+ return ids;
1053
+ },
1054
+ []
1055
+ );
1046
1056
  if (usedDbTokens >= tokens) {
1047
1057
  continue;
1048
1058
  }
@@ -1055,10 +1065,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1055
1065
  }
1056
1066
  }
1057
1067
  }
1058
- const success = remaining > 0;
1068
+ const success = remaining >= 0;
1059
1069
  const reset = (bucket + 1) * windowDuration;
1060
- if (ctx.cache && !success) {
1061
- ctx.cache.blockUntil(identifier, reset);
1070
+ if (ctx.cache) {
1071
+ if (!success) {
1072
+ ctx.cache.blockUntil(identifier, reset);
1073
+ } else if (incrementBy < 0) {
1074
+ ctx.cache.pop(identifier);
1075
+ }
1062
1076
  }
1063
1077
  return {
1064
1078
  success,
@@ -1081,13 +1095,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
1081
1095
  )
1082
1096
  }));
1083
1097
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
1084
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
1085
- let parsedToken = 0;
1086
- if (index % 2) {
1087
- parsedToken = Number.parseInt(usedToken);
1088
- }
1089
- return accTokens + parsedToken;
1090
- }, 0);
1098
+ const usedTokens = firstResponse.reduce(
1099
+ (accTokens, usedToken, index) => {
1100
+ let parsedToken = 0;
1101
+ if (index % 2) {
1102
+ parsedToken = Number.parseInt(usedToken);
1103
+ }
1104
+ return accTokens + parsedToken;
1105
+ },
1106
+ 0
1107
+ );
1091
1108
  return {
1092
1109
  remaining: Math.max(0, tokens - usedTokens),
1093
1110
  reset: (bucket + 1) * windowDuration
@@ -1098,14 +1115,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1098
1115
  if (ctx.cache) {
1099
1116
  ctx.cache.pop(identifier);
1100
1117
  }
1101
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1102
- safeEval(
1103
- regionContext,
1104
- RESET_SCRIPT,
1105
- [pattern],
1106
- [null]
1107
- );
1108
- }));
1118
+ await Promise.all(
1119
+ ctx.regionContexts.map((regionContext) => {
1120
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1121
+ })
1122
+ );
1109
1123
  }
1110
1124
  });
1111
1125
  }
@@ -1130,7 +1144,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1130
1144
  const windowDuration = ms(window);
1131
1145
  return () => ({
1132
1146
  async limit(ctx, identifier, rate) {
1133
- if (ctx.cache) {
1147
+ const requestId = randomId();
1148
+ const now = Date.now();
1149
+ const currentWindow = Math.floor(now / windowSize);
1150
+ const currentKey = [identifier, currentWindow].join(":");
1151
+ const previousWindow = currentWindow - 1;
1152
+ const previousKey = [identifier, previousWindow].join(":");
1153
+ const incrementBy = rate ?? 1;
1154
+ if (ctx.cache && incrementBy > 0) {
1134
1155
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1135
1156
  if (blocked) {
1136
1157
  return {
@@ -1143,13 +1164,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
1143
1164
  };
1144
1165
  }
1145
1166
  }
1146
- const requestId = randomId();
1147
- const now = Date.now();
1148
- const currentWindow = Math.floor(now / windowSize);
1149
- const currentKey = [identifier, currentWindow].join(":");
1150
- const previousWindow = currentWindow - 1;
1151
- const previousKey = [identifier, previousWindow].join(":");
1152
- const incrementBy = rate ? Math.max(1, rate) : 1;
1153
1167
  const dbs = ctx.regionContexts.map((regionContext) => ({
1154
1168
  redis: regionContext.redis,
1155
1169
  request: safeEval(
@@ -1161,37 +1175,49 @@ var MultiRegionRatelimit = class extends Ratelimit {
1161
1175
  )
1162
1176
  }));
1163
1177
  const percentageInCurrent = now % windowDuration / windowDuration;
1164
- const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
1178
+ const [current, previous, success] = await Promise.any(
1179
+ dbs.map((s) => s.request)
1180
+ );
1165
1181
  if (success) {
1166
1182
  current.push(requestId, incrementBy.toString());
1167
1183
  }
1168
- const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
1169
- let parsedToken = 0;
1170
- if (index % 2) {
1171
- parsedToken = Number.parseInt(usedToken);
1172
- }
1173
- return accTokens + parsedToken;
1174
- }, 0);
1175
- const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
1176
- let parsedToken = 0;
1177
- if (index % 2) {
1178
- parsedToken = Number.parseInt(usedToken);
1179
- }
1180
- return accTokens + parsedToken;
1181
- }, 0);
1182
- const previousPartialUsed = Math.ceil(previousUsedTokens * (1 - percentageInCurrent));
1184
+ const previousUsedTokens = previous.reduce(
1185
+ (accTokens, usedToken, index) => {
1186
+ let parsedToken = 0;
1187
+ if (index % 2) {
1188
+ parsedToken = Number.parseInt(usedToken);
1189
+ }
1190
+ return accTokens + parsedToken;
1191
+ },
1192
+ 0
1193
+ );
1194
+ const currentUsedTokens = current.reduce(
1195
+ (accTokens, usedToken, index) => {
1196
+ let parsedToken = 0;
1197
+ if (index % 2) {
1198
+ parsedToken = Number.parseInt(usedToken);
1199
+ }
1200
+ return accTokens + parsedToken;
1201
+ },
1202
+ 0
1203
+ );
1204
+ const previousPartialUsed = Math.ceil(
1205
+ previousUsedTokens * (1 - percentageInCurrent)
1206
+ );
1183
1207
  const usedTokens = previousPartialUsed + currentUsedTokens;
1184
1208
  const remaining = tokens - usedTokens;
1185
1209
  async function sync() {
1186
1210
  const res = await Promise.all(dbs.map((s) => s.request));
1187
- const allCurrentIds = [...new Set(
1188
- res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1189
- if (index % 2 === 0) {
1190
- acc.push(curr);
1191
- }
1192
- return acc;
1193
- }, [])
1194
- ).values()];
1211
+ const allCurrentIds = [
1212
+ ...new Set(
1213
+ res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1214
+ if (index % 2 === 0) {
1215
+ acc.push(curr);
1216
+ }
1217
+ return acc;
1218
+ }, [])
1219
+ ).values()
1220
+ ];
1195
1221
  for (const db of dbs) {
1196
1222
  const [current2, _previous, _success] = await db.request;
1197
1223
  const dbIds = current2.reduce((ids, currentId, index) => {
@@ -1200,13 +1226,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
1200
1226
  }
1201
1227
  return ids;
1202
1228
  }, []);
1203
- const usedDbTokens = current2.reduce((accTokens, usedToken, index) => {
1204
- let parsedToken = 0;
1205
- if (index % 2) {
1206
- parsedToken = Number.parseInt(usedToken);
1207
- }
1208
- return accTokens + parsedToken;
1209
- }, 0);
1229
+ const usedDbTokens = current2.reduce(
1230
+ (accTokens, usedToken, index) => {
1231
+ let parsedToken = 0;
1232
+ if (index % 2) {
1233
+ parsedToken = Number.parseInt(usedToken);
1234
+ }
1235
+ return accTokens + parsedToken;
1236
+ },
1237
+ 0
1238
+ );
1210
1239
  if (usedDbTokens >= tokens) {
1211
1240
  continue;
1212
1241
  }
@@ -1220,8 +1249,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
1220
1249
  }
1221
1250
  }
1222
1251
  const reset = (currentWindow + 1) * windowDuration;
1223
- if (ctx.cache && !success) {
1224
- ctx.cache.blockUntil(identifier, reset);
1252
+ if (ctx.cache) {
1253
+ if (!success) {
1254
+ ctx.cache.blockUntil(identifier, reset);
1255
+ } else if (incrementBy < 0) {
1256
+ ctx.cache.pop(identifier);
1257
+ }
1225
1258
  }
1226
1259
  return {
1227
1260
  success: Boolean(success),
@@ -1258,14 +1291,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1258
1291
  if (ctx.cache) {
1259
1292
  ctx.cache.pop(identifier);
1260
1293
  }
1261
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1262
- safeEval(
1263
- regionContext,
1264
- RESET_SCRIPT,
1265
- [pattern],
1266
- [null]
1267
- );
1268
- }));
1294
+ await Promise.all(
1295
+ ctx.regionContexts.map((regionContext) => {
1296
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1297
+ })
1298
+ );
1269
1299
  }
1270
1300
  });
1271
1301
  }
@@ -1314,7 +1344,8 @@ var RegionRatelimit = class extends Ratelimit {
1314
1344
  async limit(ctx, identifier, rate) {
1315
1345
  const bucket = Math.floor(Date.now() / windowDuration);
1316
1346
  const key = [identifier, bucket].join(":");
1317
- if (ctx.cache) {
1347
+ const incrementBy = rate ?? 1;
1348
+ if (ctx.cache && incrementBy > 0) {
1318
1349
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1319
1350
  if (blocked) {
1320
1351
  return {
@@ -1327,7 +1358,6 @@ var RegionRatelimit = class extends Ratelimit {
1327
1358
  };
1328
1359
  }
1329
1360
  }
1330
- const incrementBy = rate ? Math.max(1, rate) : 1;
1331
1361
  const usedTokensAfterUpdate = await safeEval(
1332
1362
  ctx,
1333
1363
  SCRIPTS.singleRegion.fixedWindow.limit,
@@ -1337,8 +1367,12 @@ var RegionRatelimit = class extends Ratelimit {
1337
1367
  const success = usedTokensAfterUpdate <= tokens;
1338
1368
  const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
1339
1369
  const reset = (bucket + 1) * windowDuration;
1340
- if (ctx.cache && !success) {
1341
- ctx.cache.blockUntil(identifier, reset);
1370
+ if (ctx.cache) {
1371
+ if (!success) {
1372
+ ctx.cache.blockUntil(identifier, reset);
1373
+ } else if (incrementBy < 0) {
1374
+ ctx.cache.pop(identifier);
1375
+ }
1342
1376
  }
1343
1377
  return {
1344
1378
  success,
@@ -1401,7 +1435,8 @@ var RegionRatelimit = class extends Ratelimit {
1401
1435
  const currentKey = [identifier, currentWindow].join(":");
1402
1436
  const previousWindow = currentWindow - 1;
1403
1437
  const previousKey = [identifier, previousWindow].join(":");
1404
- if (ctx.cache) {
1438
+ const incrementBy = rate ?? 1;
1439
+ if (ctx.cache && incrementBy > 0) {
1405
1440
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1406
1441
  if (blocked) {
1407
1442
  return {
@@ -1414,7 +1449,6 @@ var RegionRatelimit = class extends Ratelimit {
1414
1449
  };
1415
1450
  }
1416
1451
  }
1417
- const incrementBy = rate ? Math.max(1, rate) : 1;
1418
1452
  const remainingTokens = await safeEval(
1419
1453
  ctx,
1420
1454
  SCRIPTS.singleRegion.slidingWindow.limit,
@@ -1423,8 +1457,12 @@ var RegionRatelimit = class extends Ratelimit {
1423
1457
  );
1424
1458
  const success = remainingTokens >= 0;
1425
1459
  const reset = (currentWindow + 1) * windowSize;
1426
- if (ctx.cache && !success) {
1427
- ctx.cache.blockUntil(identifier, reset);
1460
+ if (ctx.cache) {
1461
+ if (!success) {
1462
+ ctx.cache.blockUntil(identifier, reset);
1463
+ } else if (incrementBy < 0) {
1464
+ ctx.cache.pop(identifier);
1465
+ }
1428
1466
  }
1429
1467
  return {
1430
1468
  success,
@@ -1482,7 +1520,9 @@ var RegionRatelimit = class extends Ratelimit {
1482
1520
  const intervalDuration = ms(interval);
1483
1521
  return () => ({
1484
1522
  async limit(ctx, identifier, rate) {
1485
- if (ctx.cache) {
1523
+ const now = Date.now();
1524
+ const incrementBy = rate ?? 1;
1525
+ if (ctx.cache && incrementBy > 0) {
1486
1526
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1487
1527
  if (blocked) {
1488
1528
  return {
@@ -1495,8 +1535,6 @@ var RegionRatelimit = class extends Ratelimit {
1495
1535
  };
1496
1536
  }
1497
1537
  }
1498
- const now = Date.now();
1499
- const incrementBy = rate ? Math.max(1, rate) : 1;
1500
1538
  const [remaining, reset] = await safeEval(
1501
1539
  ctx,
1502
1540
  SCRIPTS.singleRegion.tokenBucket.limit,
@@ -1504,8 +1542,12 @@ var RegionRatelimit = class extends Ratelimit {
1504
1542
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1505
1543
  );
1506
1544
  const success = remaining >= 0;
1507
- if (ctx.cache && !success) {
1508
- ctx.cache.blockUntil(identifier, reset);
1545
+ if (ctx.cache) {
1546
+ if (!success) {
1547
+ ctx.cache.blockUntil(identifier, reset);
1548
+ } else if (incrementBy < 0) {
1549
+ ctx.cache.pop(identifier);
1550
+ }
1509
1551
  }
1510
1552
  return {
1511
1553
  success,
@@ -1577,10 +1619,10 @@ var RegionRatelimit = class extends Ratelimit {
1577
1619
  const bucket = Math.floor(Date.now() / windowDuration);
1578
1620
  const key = [identifier, bucket].join(":");
1579
1621
  const reset = (bucket + 1) * windowDuration;
1580
- const incrementBy = rate ? Math.max(1, rate) : 1;
1622
+ const incrementBy = rate ?? 1;
1581
1623
  const hit = typeof ctx.cache.get(key) === "number";
1582
1624
  if (hit) {
1583
- const cachedTokensAfterUpdate = ctx.cache.incr(key);
1625
+ const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
1584
1626
  const success = cachedTokensAfterUpdate < tokens;
1585
1627
  const pending = success ? safeEval(
1586
1628
  ctx,