@upstash/ratelimit 2.0.6 → 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.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
  }
@@ -182,7 +182,7 @@ var slidingWindowLimitScript = `
182
182
  local tokens = tonumber(ARGV[1]) -- tokens per window
183
183
  local now = ARGV[2] -- current timestamp in milliseconds
184
184
  local window = ARGV[3] -- interval in milliseconds
185
- local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
185
+ local incrementBy = tonumber(ARGV[4]) -- increment rate per request at a given value, default is 1
186
186
 
187
187
  local requestsInCurrentWindow = redis.call("GET", currentKey)
188
188
  if requestsInCurrentWindow == false then
@@ -196,12 +196,14 @@ var slidingWindowLimitScript = `
196
196
  local percentageInCurrent = ( now % window ) / window
197
197
  -- weighted requests to consider from the previous window
198
198
  requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
199
- if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
199
+
200
+ -- Only check limit if not refunding (negative rate)
201
+ if incrementBy > 0 and requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
200
202
  return -1
201
203
  end
202
204
 
203
205
  local newValue = redis.call("INCRBY", currentKey, incrementBy)
204
- if newValue == tonumber(incrementBy) then
206
+ if newValue == incrementBy then
205
207
  -- The first time this key is set, the value will be equal to incrementBy.
206
208
  -- So we only need the expire command once
207
209
  redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
@@ -258,7 +260,8 @@ var tokenBucketLimitScript = `
258
260
  refilledAt = refilledAt + numRefills * interval
259
261
  end
260
262
 
261
- if tokens == 0 then
263
+ -- Only reject if tokens are 0 and we're consuming (not refunding)
264
+ if tokens == 0 and incrementBy > 0 then
262
265
  return {-1, refilledAt + interval}
263
266
  end
264
267
 
@@ -266,7 +269,10 @@ var tokenBucketLimitScript = `
266
269
  local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
267
270
 
268
271
  redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
269
- redis.call("PEXPIRE", key, expireAt)
272
+
273
+ if (expireAt > 0) then
274
+ redis.call("PEXPIRE", key, expireAt)
275
+ end
270
276
  return {remaining, refilledAt + interval}
271
277
  `;
272
278
  var tokenBucketIdentifierNotFound = -1;
@@ -354,7 +360,9 @@ var slidingWindowLimitScript2 = `
354
360
  end
355
361
 
356
362
  local percentageInCurrent = ( now % window) / window
357
- if requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
363
+
364
+ -- Only check limit if not refunding (negative rate)
365
+ if incrementBy > 0 and requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow + incrementBy > tokens then
358
366
  return {currentFields, previousFields, false}
359
367
  end
360
368
 
@@ -432,7 +440,7 @@ var SCRIPTS = {
432
440
  slidingWindow: {
433
441
  limit: {
434
442
  script: slidingWindowLimitScript,
435
- hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
443
+ hash: "9b7842963bd73721f1a3011650c23c0010848ee3"
436
444
  },
437
445
  getRemaining: {
438
446
  script: slidingWindowRemainingTokensScript,
@@ -442,7 +450,7 @@ var SCRIPTS = {
442
450
  tokenBucket: {
443
451
  limit: {
444
452
  script: tokenBucketLimitScript,
445
- hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
453
+ hash: "d1f857ebbdaeca90ccd2cd4eada61d7c8e5db1ca"
446
454
  },
447
455
  getRemaining: {
448
456
  script: tokenBucketRemainingTokensScript,
@@ -474,7 +482,7 @@ var SCRIPTS = {
474
482
  slidingWindow: {
475
483
  limit: {
476
484
  script: slidingWindowLimitScript2,
477
- hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
485
+ hash: "1e7ca8dcd2d600a6d0124a67a57ea225ed62921b"
478
486
  },
479
487
  getRemaining: {
480
488
  script: slidingWindowRemainingTokensScript2,
@@ -950,7 +958,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
950
958
  const windowDuration = ms(window);
951
959
  return () => ({
952
960
  async limit(ctx, identifier, rate) {
953
- if (ctx.cache) {
961
+ const requestId = randomId();
962
+ const bucket = Math.floor(Date.now() / windowDuration);
963
+ const key = [identifier, bucket].join(":");
964
+ const incrementBy = rate ?? 1;
965
+ if (ctx.cache && incrementBy > 0) {
954
966
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
955
967
  if (blocked) {
956
968
  return {
@@ -963,10 +975,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
963
975
  };
964
976
  }
965
977
  }
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
978
  const dbs = ctx.regionContexts.map((regionContext) => ({
971
979
  redis: regionContext.redis,
972
980
  request: safeEval(
@@ -977,24 +985,29 @@ var MultiRegionRatelimit = class extends Ratelimit {
977
985
  )
978
986
  }));
979
987
  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);
988
+ const usedTokens = firstResponse.reduce(
989
+ (accTokens, usedToken, index) => {
990
+ let parsedToken = 0;
991
+ if (index % 2) {
992
+ parsedToken = Number.parseInt(usedToken);
993
+ }
994
+ return accTokens + parsedToken;
995
+ },
996
+ 0
997
+ );
987
998
  const remaining = tokens - usedTokens;
988
999
  async function sync() {
989
1000
  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()];
1001
+ const allIDs = [
1002
+ ...new Set(
1003
+ individualIDs.flat().reduce((acc, curr, index) => {
1004
+ if (index % 2 === 0) {
1005
+ acc.push(curr);
1006
+ }
1007
+ return acc;
1008
+ }, [])
1009
+ ).values()
1010
+ ];
998
1011
  for (const db of dbs) {
999
1012
  const usedDbTokensRequest = await db.request;
1000
1013
  const usedDbTokens = usedDbTokensRequest.reduce(
@@ -1008,12 +1021,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
1008
1021
  0
1009
1022
  );
1010
1023
  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
- }, []);
1024
+ const dbIds = dbIdsRequest.reduce(
1025
+ (ids, currentId, index) => {
1026
+ if (index % 2 === 0) {
1027
+ ids.push(currentId);
1028
+ }
1029
+ return ids;
1030
+ },
1031
+ []
1032
+ );
1017
1033
  if (usedDbTokens >= tokens) {
1018
1034
  continue;
1019
1035
  }
@@ -1026,10 +1042,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1026
1042
  }
1027
1043
  }
1028
1044
  }
1029
- const success = remaining > 0;
1045
+ const success = remaining >= 0;
1030
1046
  const reset = (bucket + 1) * windowDuration;
1031
- if (ctx.cache && !success) {
1032
- ctx.cache.blockUntil(identifier, reset);
1047
+ if (ctx.cache) {
1048
+ if (!success) {
1049
+ ctx.cache.blockUntil(identifier, reset);
1050
+ } else if (incrementBy < 0) {
1051
+ ctx.cache.pop(identifier);
1052
+ }
1033
1053
  }
1034
1054
  return {
1035
1055
  success,
@@ -1052,13 +1072,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
1052
1072
  )
1053
1073
  }));
1054
1074
  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);
1075
+ const usedTokens = firstResponse.reduce(
1076
+ (accTokens, usedToken, index) => {
1077
+ let parsedToken = 0;
1078
+ if (index % 2) {
1079
+ parsedToken = Number.parseInt(usedToken);
1080
+ }
1081
+ return accTokens + parsedToken;
1082
+ },
1083
+ 0
1084
+ );
1062
1085
  return {
1063
1086
  remaining: Math.max(0, tokens - usedTokens),
1064
1087
  reset: (bucket + 1) * windowDuration
@@ -1069,14 +1092,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1069
1092
  if (ctx.cache) {
1070
1093
  ctx.cache.pop(identifier);
1071
1094
  }
1072
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1073
- safeEval(
1074
- regionContext,
1075
- RESET_SCRIPT,
1076
- [pattern],
1077
- [null]
1078
- );
1079
- }));
1095
+ await Promise.all(
1096
+ ctx.regionContexts.map((regionContext) => {
1097
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1098
+ })
1099
+ );
1080
1100
  }
1081
1101
  });
1082
1102
  }
@@ -1101,7 +1121,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
1101
1121
  const windowDuration = ms(window);
1102
1122
  return () => ({
1103
1123
  async limit(ctx, identifier, rate) {
1104
- if (ctx.cache) {
1124
+ const requestId = randomId();
1125
+ const now = Date.now();
1126
+ const currentWindow = Math.floor(now / windowSize);
1127
+ const currentKey = [identifier, currentWindow].join(":");
1128
+ const previousWindow = currentWindow - 1;
1129
+ const previousKey = [identifier, previousWindow].join(":");
1130
+ const incrementBy = rate ?? 1;
1131
+ if (ctx.cache && incrementBy > 0) {
1105
1132
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1106
1133
  if (blocked) {
1107
1134
  return {
@@ -1114,13 +1141,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
1114
1141
  };
1115
1142
  }
1116
1143
  }
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
1144
  const dbs = ctx.regionContexts.map((regionContext) => ({
1125
1145
  redis: regionContext.redis,
1126
1146
  request: safeEval(
@@ -1132,37 +1152,49 @@ var MultiRegionRatelimit = class extends Ratelimit {
1132
1152
  )
1133
1153
  }));
1134
1154
  const percentageInCurrent = now % windowDuration / windowDuration;
1135
- const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
1155
+ const [current, previous, success] = await Promise.any(
1156
+ dbs.map((s) => s.request)
1157
+ );
1136
1158
  if (success) {
1137
1159
  current.push(requestId, incrementBy.toString());
1138
1160
  }
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));
1161
+ const previousUsedTokens = previous.reduce(
1162
+ (accTokens, usedToken, index) => {
1163
+ let parsedToken = 0;
1164
+ if (index % 2) {
1165
+ parsedToken = Number.parseInt(usedToken);
1166
+ }
1167
+ return accTokens + parsedToken;
1168
+ },
1169
+ 0
1170
+ );
1171
+ const currentUsedTokens = current.reduce(
1172
+ (accTokens, usedToken, index) => {
1173
+ let parsedToken = 0;
1174
+ if (index % 2) {
1175
+ parsedToken = Number.parseInt(usedToken);
1176
+ }
1177
+ return accTokens + parsedToken;
1178
+ },
1179
+ 0
1180
+ );
1181
+ const previousPartialUsed = Math.ceil(
1182
+ previousUsedTokens * (1 - percentageInCurrent)
1183
+ );
1154
1184
  const usedTokens = previousPartialUsed + currentUsedTokens;
1155
1185
  const remaining = tokens - usedTokens;
1156
1186
  async function sync() {
1157
1187
  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()];
1188
+ const allCurrentIds = [
1189
+ ...new Set(
1190
+ res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
1191
+ if (index % 2 === 0) {
1192
+ acc.push(curr);
1193
+ }
1194
+ return acc;
1195
+ }, [])
1196
+ ).values()
1197
+ ];
1166
1198
  for (const db of dbs) {
1167
1199
  const [current2, _previous, _success] = await db.request;
1168
1200
  const dbIds = current2.reduce((ids, currentId, index) => {
@@ -1171,13 +1203,16 @@ var MultiRegionRatelimit = class extends Ratelimit {
1171
1203
  }
1172
1204
  return ids;
1173
1205
  }, []);
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);
1206
+ const usedDbTokens = current2.reduce(
1207
+ (accTokens, usedToken, index) => {
1208
+ let parsedToken = 0;
1209
+ if (index % 2) {
1210
+ parsedToken = Number.parseInt(usedToken);
1211
+ }
1212
+ return accTokens + parsedToken;
1213
+ },
1214
+ 0
1215
+ );
1181
1216
  if (usedDbTokens >= tokens) {
1182
1217
  continue;
1183
1218
  }
@@ -1191,8 +1226,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
1191
1226
  }
1192
1227
  }
1193
1228
  const reset = (currentWindow + 1) * windowDuration;
1194
- if (ctx.cache && !success) {
1195
- ctx.cache.blockUntil(identifier, reset);
1229
+ if (ctx.cache) {
1230
+ if (!success) {
1231
+ ctx.cache.blockUntil(identifier, reset);
1232
+ } else if (incrementBy < 0) {
1233
+ ctx.cache.pop(identifier);
1234
+ }
1196
1235
  }
1197
1236
  return {
1198
1237
  success: Boolean(success),
@@ -1229,14 +1268,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
1229
1268
  if (ctx.cache) {
1230
1269
  ctx.cache.pop(identifier);
1231
1270
  }
1232
- await Promise.all(ctx.regionContexts.map((regionContext) => {
1233
- safeEval(
1234
- regionContext,
1235
- RESET_SCRIPT,
1236
- [pattern],
1237
- [null]
1238
- );
1239
- }));
1271
+ await Promise.all(
1272
+ ctx.regionContexts.map((regionContext) => {
1273
+ safeEval(regionContext, RESET_SCRIPT, [pattern], [null]);
1274
+ })
1275
+ );
1240
1276
  }
1241
1277
  });
1242
1278
  }
@@ -1285,7 +1321,8 @@ var RegionRatelimit = class extends Ratelimit {
1285
1321
  async limit(ctx, identifier, rate) {
1286
1322
  const bucket = Math.floor(Date.now() / windowDuration);
1287
1323
  const key = [identifier, bucket].join(":");
1288
- if (ctx.cache) {
1324
+ const incrementBy = rate ?? 1;
1325
+ if (ctx.cache && incrementBy > 0) {
1289
1326
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1290
1327
  if (blocked) {
1291
1328
  return {
@@ -1298,7 +1335,6 @@ var RegionRatelimit = class extends Ratelimit {
1298
1335
  };
1299
1336
  }
1300
1337
  }
1301
- const incrementBy = rate ? Math.max(1, rate) : 1;
1302
1338
  const usedTokensAfterUpdate = await safeEval(
1303
1339
  ctx,
1304
1340
  SCRIPTS.singleRegion.fixedWindow.limit,
@@ -1308,8 +1344,12 @@ var RegionRatelimit = class extends Ratelimit {
1308
1344
  const success = usedTokensAfterUpdate <= tokens;
1309
1345
  const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
1310
1346
  const reset = (bucket + 1) * windowDuration;
1311
- if (ctx.cache && !success) {
1312
- ctx.cache.blockUntil(identifier, reset);
1347
+ if (ctx.cache) {
1348
+ if (!success) {
1349
+ ctx.cache.blockUntil(identifier, reset);
1350
+ } else if (incrementBy < 0) {
1351
+ ctx.cache.pop(identifier);
1352
+ }
1313
1353
  }
1314
1354
  return {
1315
1355
  success,
@@ -1372,7 +1412,8 @@ var RegionRatelimit = class extends Ratelimit {
1372
1412
  const currentKey = [identifier, currentWindow].join(":");
1373
1413
  const previousWindow = currentWindow - 1;
1374
1414
  const previousKey = [identifier, previousWindow].join(":");
1375
- if (ctx.cache) {
1415
+ const incrementBy = rate ?? 1;
1416
+ if (ctx.cache && incrementBy > 0) {
1376
1417
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1377
1418
  if (blocked) {
1378
1419
  return {
@@ -1385,7 +1426,6 @@ var RegionRatelimit = class extends Ratelimit {
1385
1426
  };
1386
1427
  }
1387
1428
  }
1388
- const incrementBy = rate ? Math.max(1, rate) : 1;
1389
1429
  const remainingTokens = await safeEval(
1390
1430
  ctx,
1391
1431
  SCRIPTS.singleRegion.slidingWindow.limit,
@@ -1394,8 +1434,12 @@ var RegionRatelimit = class extends Ratelimit {
1394
1434
  );
1395
1435
  const success = remainingTokens >= 0;
1396
1436
  const reset = (currentWindow + 1) * windowSize;
1397
- if (ctx.cache && !success) {
1398
- ctx.cache.blockUntil(identifier, reset);
1437
+ if (ctx.cache) {
1438
+ if (!success) {
1439
+ ctx.cache.blockUntil(identifier, reset);
1440
+ } else if (incrementBy < 0) {
1441
+ ctx.cache.pop(identifier);
1442
+ }
1399
1443
  }
1400
1444
  return {
1401
1445
  success,
@@ -1453,7 +1497,9 @@ var RegionRatelimit = class extends Ratelimit {
1453
1497
  const intervalDuration = ms(interval);
1454
1498
  return () => ({
1455
1499
  async limit(ctx, identifier, rate) {
1456
- if (ctx.cache) {
1500
+ const now = Date.now();
1501
+ const incrementBy = rate ?? 1;
1502
+ if (ctx.cache && incrementBy > 0) {
1457
1503
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1458
1504
  if (blocked) {
1459
1505
  return {
@@ -1466,8 +1512,6 @@ var RegionRatelimit = class extends Ratelimit {
1466
1512
  };
1467
1513
  }
1468
1514
  }
1469
- const now = Date.now();
1470
- const incrementBy = rate ? Math.max(1, rate) : 1;
1471
1515
  const [remaining, reset] = await safeEval(
1472
1516
  ctx,
1473
1517
  SCRIPTS.singleRegion.tokenBucket.limit,
@@ -1475,8 +1519,12 @@ var RegionRatelimit = class extends Ratelimit {
1475
1519
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1476
1520
  );
1477
1521
  const success = remaining >= 0;
1478
- if (ctx.cache && !success) {
1479
- ctx.cache.blockUntil(identifier, reset);
1522
+ if (ctx.cache) {
1523
+ if (!success) {
1524
+ ctx.cache.blockUntil(identifier, reset);
1525
+ } else if (incrementBy < 0) {
1526
+ ctx.cache.pop(identifier);
1527
+ }
1480
1528
  }
1481
1529
  return {
1482
1530
  success,
@@ -1548,10 +1596,10 @@ var RegionRatelimit = class extends Ratelimit {
1548
1596
  const bucket = Math.floor(Date.now() / windowDuration);
1549
1597
  const key = [identifier, bucket].join(":");
1550
1598
  const reset = (bucket + 1) * windowDuration;
1551
- const incrementBy = rate ? Math.max(1, rate) : 1;
1599
+ const incrementBy = rate ?? 1;
1552
1600
  const hit = typeof ctx.cache.get(key) === "number";
1553
1601
  if (hit) {
1554
- const cachedTokensAfterUpdate = ctx.cache.incr(key);
1602
+ const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
1555
1603
  const success = cachedTokensAfterUpdate < tokens;
1556
1604
  const pending = success ? safeEval(
1557
1605
  ctx,