@upstash/ratelimit 2.1.0-rc → 2.2.0

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
@@ -114,33 +114,30 @@ var DYNAMIC_LIMIT_KEY_SUFFIX = ":dynamic:global";
114
114
  var DEFAULT_PREFIX = "@upstash/ratelimit";
115
115
 
116
116
  // src/duration.ts
117
+ var MULTIPLIERS = {
118
+ ms: 1,
119
+ s: 1e3,
120
+ m: 1e3 * 60,
121
+ h: 1e3 * 60 * 60,
122
+ d: 1e3 * 60 * 60 * 24,
123
+ w: 1e3 * 60 * 60 * 24 * 7
124
+ };
117
125
  function ms(d) {
118
- const match = d.match(/^(\d+)\s?(ms|s|m|h|d)$/);
126
+ const match = d.match(/^(\d+(?:\.\d+)?)\s?(ms|s|m|h|d|w)$/);
119
127
  if (!match) {
120
128
  throw new Error(`Unable to parse window size: ${d}`);
121
129
  }
122
- const time = Number.parseInt(match[1]);
130
+ const time = Number.parseFloat(match[1]);
123
131
  const unit = match[2];
124
- switch (unit) {
125
- case "ms": {
126
- return time;
127
- }
128
- case "s": {
129
- return time * 1e3;
130
- }
131
- case "m": {
132
- return time * 1e3 * 60;
133
- }
134
- case "h": {
135
- return time * 1e3 * 60 * 60;
136
- }
137
- case "d": {
138
- return time * 1e3 * 60 * 60 * 24;
139
- }
140
- default: {
141
- throw new Error(`Unable to parse window size: ${d}`);
142
- }
132
+ const multiplier = MULTIPLIERS[unit];
133
+ if (multiplier === void 0) {
134
+ throw new Error(`Unable to parse window size: ${d}`);
143
135
  }
136
+ const result = Math.round(time * multiplier);
137
+ if (result < 1) {
138
+ throw new Error(`Window size must be at least 1 ms, got: ${d}`);
139
+ }
140
+ return result;
144
141
  }
145
142
 
146
143
  // src/hash.ts
@@ -165,7 +162,7 @@ var fixedWindowLimitScript = `#!lua flags=allow-key-locking
165
162
 
166
163
  -- Check for dynamic limit
167
164
  local effectiveLimit = tokens
168
- if dynamicLimitKey ~= "" then
165
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
169
166
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
170
167
  if dynamicLimit then
171
168
  effectiveLimit = tonumber(dynamicLimit)
@@ -188,7 +185,7 @@ var fixedWindowRemainingTokensScript = `#!lua flags=allow-key-locking
188
185
 
189
186
  -- Check for dynamic limit
190
187
  local effectiveLimit = tokens
191
- if dynamicLimitKey ~= "" then
188
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
192
189
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
193
190
  if dynamicLimit then
194
191
  effectiveLimit = tonumber(dynamicLimit)
@@ -214,7 +211,7 @@ var slidingWindowLimitScript = `#!lua flags=allow-key-locking
214
211
 
215
212
  -- Check for dynamic limit
216
213
  local effectiveLimit = tokens
217
- if dynamicLimitKey ~= "" then
214
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
218
215
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
219
216
  if dynamicLimit then
220
217
  effectiveLimit = tonumber(dynamicLimit)
@@ -257,7 +254,7 @@ var slidingWindowRemainingTokensScript = `#!lua flags=allow-key-locking
257
254
 
258
255
  -- Check for dynamic limit
259
256
  local effectiveLimit = tokens
260
- if dynamicLimitKey ~= "" then
257
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
261
258
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
262
259
  if dynamicLimit then
263
260
  effectiveLimit = tonumber(dynamicLimit)
@@ -292,7 +289,7 @@ var tokenBucketLimitScript = `#!lua flags=allow-key-locking
292
289
 
293
290
  -- Check for dynamic limit
294
291
  local effectiveLimit = maxTokens
295
- if dynamicLimitKey ~= "" then
292
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
296
293
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
297
294
  if dynamicLimit then
298
295
  effectiveLimit = tonumber(dynamicLimit)
@@ -319,8 +316,12 @@ var tokenBucketLimitScript = `#!lua flags=allow-key-locking
319
316
  refilledAt = refilledAt + numRefills * interval
320
317
  end
321
318
 
322
- -- Only reject if tokens are 0 and we're consuming (not refunding)
323
- if tokens == 0 and incrementBy > 0 then
319
+ -- Only reject when consuming (not refunding) and there are not enough tokens
320
+ -- for this request. Checking tokens == 0 only covered incrementBy == 1: with
321
+ -- a larger rate (e.g. 2 tokens left, incrementBy 5) the request fell through,
322
+ -- drove tokens negative and still wrote it back, so a rejected request
323
+ -- consumed tokens and a single large rate could lock the identifier out.
324
+ if incrementBy > 0 and tokens < incrementBy then
324
325
  return {-1, refilledAt + interval, effectiveLimit}
325
326
  end
326
327
 
@@ -342,7 +343,7 @@ var tokenBucketRemainingTokensScript = `#!lua flags=allow-key-locking
342
343
 
343
344
  -- Check for dynamic limit
344
345
  local effectiveLimit = maxTokens
345
- if dynamicLimitKey ~= "" then
346
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
346
347
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
347
348
  if dynamicLimit then
348
349
  effectiveLimit = tonumber(dynamicLimit)
@@ -499,31 +500,31 @@ var SCRIPTS = {
499
500
  fixedWindow: {
500
501
  limit: {
501
502
  script: fixedWindowLimitScript,
502
- hash: "4ef8749b9e927b157546ebad13061fa86b9ffc2e"
503
+ hash: "b2efc02798c8e9b471a10f9b1d5745db89923ff3"
503
504
  },
504
505
  getRemaining: {
505
506
  script: fixedWindowRemainingTokensScript,
506
- hash: "e62252cb05b4676b27a5d396da02f0ca0d375d88"
507
+ hash: "823cb2a5a902d69681244f5e28e5ebb8f2a89edd"
507
508
  }
508
509
  },
509
510
  slidingWindow: {
510
511
  limit: {
511
512
  script: slidingWindowLimitScript,
512
- hash: "5cd9665be7533e4dfabb3581021737fc69b41ae8"
513
+ hash: "74740bb3d7792848093cdef586b6b6037c455fd6"
513
514
  },
514
515
  getRemaining: {
515
516
  script: slidingWindowRemainingTokensScript,
516
- hash: "1bdbbdb2082bb557084518c64486d7d5a29d1bfe"
517
+ hash: "7ca36169e0fc9caedd81b4eef8e99854c157f9b5"
517
518
  }
518
519
  },
519
520
  tokenBucket: {
520
521
  limit: {
521
522
  script: tokenBucketLimitScript,
522
- hash: "8f7286b9b0f65d631f760ba615f7b3c598a569a3"
523
+ hash: "d60be5bd3d83482ae229309f1f7425424f891204"
523
524
  },
524
525
  getRemaining: {
525
526
  script: tokenBucketRemainingTokensScript,
526
- hash: "08d3b3381c507b6f7bfec1bbfa0a6053434b16bd"
527
+ hash: "40daf4a441df736f5c99f8b77bbe176c909e5de4"
527
528
  }
528
529
  },
529
530
  cachedFixedWindow: {
@@ -848,7 +849,7 @@ var Ratelimit = class {
848
849
  throw new Error("This should not happen");
849
850
  }
850
851
  const wait = Math.min(res.reset, deadline) - Date.now();
851
- await new Promise((r) => setTimeout(r, wait));
852
+ await new Promise((r) => setTimeout(r, wait > 0 ? wait : 100));
852
853
  if (Date.now() > deadline) {
853
854
  break;
854
855
  }
@@ -1037,7 +1038,7 @@ var Ratelimit = class {
1037
1038
  };
1038
1039
 
1039
1040
  // src/version.ts
1040
- var VERSION = "2.1.0-rc";
1041
+ var VERSION = "2.2.0";
1041
1042
 
1042
1043
  // src/telemetry.ts
1043
1044
  var taggedClients = /* @__PURE__ */ new WeakSet();
@@ -1499,11 +1500,11 @@ var RegionRatelimit = class extends Ratelimit {
1499
1500
  };
1500
1501
  }
1501
1502
  }
1502
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1503
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1503
1504
  const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
1504
1505
  ctx,
1505
1506
  SCRIPTS.singleRegion.fixedWindow.limit,
1506
- [key, dynamicLimitKey],
1507
+ [key, ...dynamicLimitKeys],
1507
1508
  [tokens, windowDuration, incrementBy]
1508
1509
  );
1509
1510
  const success = usedTokensAfterUpdate <= effectiveLimit;
@@ -1527,11 +1528,11 @@ var RegionRatelimit = class extends Ratelimit {
1527
1528
  async getRemaining(ctx, identifier) {
1528
1529
  const bucket = Math.floor(Date.now() / windowDuration);
1529
1530
  const key = [identifier, bucket].join(":");
1530
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1531
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1531
1532
  const [remaining, effectiveLimit] = await safeEval(
1532
1533
  ctx,
1533
1534
  SCRIPTS.singleRegion.fixedWindow.getRemaining,
1534
- [key, dynamicLimitKey],
1535
+ [key, ...dynamicLimitKeys],
1535
1536
  [tokens]
1536
1537
  );
1537
1538
  return {
@@ -1593,11 +1594,11 @@ var RegionRatelimit = class extends Ratelimit {
1593
1594
  };
1594
1595
  }
1595
1596
  }
1596
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1597
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1597
1598
  const [remainingTokens, effectiveLimit] = await safeEval(
1598
1599
  ctx,
1599
1600
  SCRIPTS.singleRegion.slidingWindow.limit,
1600
- [currentKey, previousKey, dynamicLimitKey],
1601
+ [currentKey, previousKey, ...dynamicLimitKeys],
1601
1602
  [tokens, now, windowSize, incrementBy]
1602
1603
  );
1603
1604
  const success = remainingTokens >= 0;
@@ -1623,11 +1624,11 @@ var RegionRatelimit = class extends Ratelimit {
1623
1624
  const currentKey = [identifier, currentWindow].join(":");
1624
1625
  const previousWindow = currentWindow - 1;
1625
1626
  const previousKey = [identifier, previousWindow].join(":");
1626
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1627
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1627
1628
  const [remaining, effectiveLimit] = await safeEval(
1628
1629
  ctx,
1629
1630
  SCRIPTS.singleRegion.slidingWindow.getRemaining,
1630
- [currentKey, previousKey, dynamicLimitKey],
1631
+ [currentKey, previousKey, ...dynamicLimitKeys],
1631
1632
  [tokens, now, windowSize]
1632
1633
  );
1633
1634
  return {
@@ -1682,11 +1683,11 @@ var RegionRatelimit = class extends Ratelimit {
1682
1683
  };
1683
1684
  }
1684
1685
  }
1685
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1686
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1686
1687
  const [remaining, reset, effectiveLimit] = await safeEval(
1687
1688
  ctx,
1688
1689
  SCRIPTS.singleRegion.tokenBucket.limit,
1689
- [identifier, dynamicLimitKey],
1690
+ [identifier, ...dynamicLimitKeys],
1690
1691
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1691
1692
  );
1692
1693
  const success = remaining >= 0;
@@ -1706,11 +1707,11 @@ var RegionRatelimit = class extends Ratelimit {
1706
1707
  };
1707
1708
  },
1708
1709
  async getRemaining(ctx, identifier) {
1709
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1710
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1710
1711
  const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
1711
1712
  ctx,
1712
1713
  SCRIPTS.singleRegion.tokenBucket.getRemaining,
1713
- [identifier, dynamicLimitKey],
1714
+ [identifier, ...dynamicLimitKeys],
1714
1715
  [maxTokens]
1715
1716
  );
1716
1717
  const freshRefillAt = Date.now() + intervalDuration;
@@ -1778,7 +1779,7 @@ var RegionRatelimit = class extends Ratelimit {
1778
1779
  const hit = typeof ctx.cache.get(key) === "number";
1779
1780
  if (hit) {
1780
1781
  const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
1781
- const success = cachedTokensAfterUpdate < tokens;
1782
+ const success = cachedTokensAfterUpdate <= tokens;
1782
1783
  const pending = success ? safeEval(
1783
1784
  ctx,
1784
1785
  SCRIPTS.singleRegion.cachedFixedWindow.limit,