@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.d.mts CHANGED
@@ -172,7 +172,7 @@ declare class Analytics {
172
172
  }>;
173
173
  }
174
174
 
175
- type Unit = "ms" | "s" | "m" | "h" | "d";
175
+ type Unit = "ms" | "s" | "m" | "h" | "d" | "w";
176
176
  type Duration = `${number} ${Unit}` | `${number}${Unit}`;
177
177
 
178
178
  type RatelimitConfig<TContext> = {
package/dist/index.d.ts CHANGED
@@ -172,7 +172,7 @@ declare class Analytics {
172
172
  }>;
173
173
  }
174
174
 
175
- type Unit = "ms" | "s" | "m" | "h" | "d";
175
+ type Unit = "ms" | "s" | "m" | "h" | "d" | "w";
176
176
  type Duration = `${number} ${Unit}` | `${number}${Unit}`;
177
177
 
178
178
  type RatelimitConfig<TContext> = {
package/dist/index.js CHANGED
@@ -137,33 +137,30 @@ var DYNAMIC_LIMIT_KEY_SUFFIX = ":dynamic:global";
137
137
  var DEFAULT_PREFIX = "@upstash/ratelimit";
138
138
 
139
139
  // src/duration.ts
140
+ var MULTIPLIERS = {
141
+ ms: 1,
142
+ s: 1e3,
143
+ m: 1e3 * 60,
144
+ h: 1e3 * 60 * 60,
145
+ d: 1e3 * 60 * 60 * 24,
146
+ w: 1e3 * 60 * 60 * 24 * 7
147
+ };
140
148
  function ms(d) {
141
- const match = d.match(/^(\d+)\s?(ms|s|m|h|d)$/);
149
+ const match = d.match(/^(\d+(?:\.\d+)?)\s?(ms|s|m|h|d|w)$/);
142
150
  if (!match) {
143
151
  throw new Error(`Unable to parse window size: ${d}`);
144
152
  }
145
- const time = Number.parseInt(match[1]);
153
+ const time = Number.parseFloat(match[1]);
146
154
  const unit = match[2];
147
- switch (unit) {
148
- case "ms": {
149
- return time;
150
- }
151
- case "s": {
152
- return time * 1e3;
153
- }
154
- case "m": {
155
- return time * 1e3 * 60;
156
- }
157
- case "h": {
158
- return time * 1e3 * 60 * 60;
159
- }
160
- case "d": {
161
- return time * 1e3 * 60 * 60 * 24;
162
- }
163
- default: {
164
- throw new Error(`Unable to parse window size: ${d}`);
165
- }
155
+ const multiplier = MULTIPLIERS[unit];
156
+ if (multiplier === void 0) {
157
+ throw new Error(`Unable to parse window size: ${d}`);
166
158
  }
159
+ const result = Math.round(time * multiplier);
160
+ if (result < 1) {
161
+ throw new Error(`Window size must be at least 1 ms, got: ${d}`);
162
+ }
163
+ return result;
167
164
  }
168
165
 
169
166
  // src/hash.ts
@@ -188,7 +185,7 @@ var fixedWindowLimitScript = `#!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)
@@ -211,7 +208,7 @@ var fixedWindowRemainingTokensScript = `#!lua flags=allow-key-locking
211
208
 
212
209
  -- Check for dynamic limit
213
210
  local effectiveLimit = tokens
214
- if dynamicLimitKey ~= "" then
211
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
215
212
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
216
213
  if dynamicLimit then
217
214
  effectiveLimit = tonumber(dynamicLimit)
@@ -237,7 +234,7 @@ var slidingWindowLimitScript = `#!lua flags=allow-key-locking
237
234
 
238
235
  -- Check for dynamic limit
239
236
  local effectiveLimit = tokens
240
- if dynamicLimitKey ~= "" then
237
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
241
238
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
242
239
  if dynamicLimit then
243
240
  effectiveLimit = tonumber(dynamicLimit)
@@ -280,7 +277,7 @@ var slidingWindowRemainingTokensScript = `#!lua flags=allow-key-locking
280
277
 
281
278
  -- Check for dynamic limit
282
279
  local effectiveLimit = tokens
283
- if dynamicLimitKey ~= "" then
280
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
284
281
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
285
282
  if dynamicLimit then
286
283
  effectiveLimit = tonumber(dynamicLimit)
@@ -315,7 +312,7 @@ var tokenBucketLimitScript = `#!lua flags=allow-key-locking
315
312
 
316
313
  -- Check for dynamic limit
317
314
  local effectiveLimit = maxTokens
318
- if dynamicLimitKey ~= "" then
315
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
319
316
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
320
317
  if dynamicLimit then
321
318
  effectiveLimit = tonumber(dynamicLimit)
@@ -342,8 +339,12 @@ var tokenBucketLimitScript = `#!lua flags=allow-key-locking
342
339
  refilledAt = refilledAt + numRefills * interval
343
340
  end
344
341
 
345
- -- Only reject if tokens are 0 and we're consuming (not refunding)
346
- if tokens == 0 and incrementBy > 0 then
342
+ -- Only reject when consuming (not refunding) and there are not enough tokens
343
+ -- for this request. Checking tokens == 0 only covered incrementBy == 1: with
344
+ -- a larger rate (e.g. 2 tokens left, incrementBy 5) the request fell through,
345
+ -- drove tokens negative and still wrote it back, so a rejected request
346
+ -- consumed tokens and a single large rate could lock the identifier out.
347
+ if incrementBy > 0 and tokens < incrementBy then
347
348
  return {-1, refilledAt + interval, effectiveLimit}
348
349
  end
349
350
 
@@ -365,7 +366,7 @@ var tokenBucketRemainingTokensScript = `#!lua flags=allow-key-locking
365
366
 
366
367
  -- Check for dynamic limit
367
368
  local effectiveLimit = maxTokens
368
- if dynamicLimitKey ~= "" then
369
+ if dynamicLimitKey and dynamicLimitKey ~= "" then
369
370
  local dynamicLimit = redis.call("GET", dynamicLimitKey)
370
371
  if dynamicLimit then
371
372
  effectiveLimit = tonumber(dynamicLimit)
@@ -522,31 +523,31 @@ var SCRIPTS = {
522
523
  fixedWindow: {
523
524
  limit: {
524
525
  script: fixedWindowLimitScript,
525
- hash: "4ef8749b9e927b157546ebad13061fa86b9ffc2e"
526
+ hash: "b2efc02798c8e9b471a10f9b1d5745db89923ff3"
526
527
  },
527
528
  getRemaining: {
528
529
  script: fixedWindowRemainingTokensScript,
529
- hash: "e62252cb05b4676b27a5d396da02f0ca0d375d88"
530
+ hash: "823cb2a5a902d69681244f5e28e5ebb8f2a89edd"
530
531
  }
531
532
  },
532
533
  slidingWindow: {
533
534
  limit: {
534
535
  script: slidingWindowLimitScript,
535
- hash: "5cd9665be7533e4dfabb3581021737fc69b41ae8"
536
+ hash: "74740bb3d7792848093cdef586b6b6037c455fd6"
536
537
  },
537
538
  getRemaining: {
538
539
  script: slidingWindowRemainingTokensScript,
539
- hash: "1bdbbdb2082bb557084518c64486d7d5a29d1bfe"
540
+ hash: "7ca36169e0fc9caedd81b4eef8e99854c157f9b5"
540
541
  }
541
542
  },
542
543
  tokenBucket: {
543
544
  limit: {
544
545
  script: tokenBucketLimitScript,
545
- hash: "8f7286b9b0f65d631f760ba615f7b3c598a569a3"
546
+ hash: "d60be5bd3d83482ae229309f1f7425424f891204"
546
547
  },
547
548
  getRemaining: {
548
549
  script: tokenBucketRemainingTokensScript,
549
- hash: "08d3b3381c507b6f7bfec1bbfa0a6053434b16bd"
550
+ hash: "40daf4a441df736f5c99f8b77bbe176c909e5de4"
550
551
  }
551
552
  },
552
553
  cachedFixedWindow: {
@@ -871,7 +872,7 @@ var Ratelimit = class {
871
872
  throw new Error("This should not happen");
872
873
  }
873
874
  const wait = Math.min(res.reset, deadline) - Date.now();
874
- await new Promise((r) => setTimeout(r, wait));
875
+ await new Promise((r) => setTimeout(r, wait > 0 ? wait : 100));
875
876
  if (Date.now() > deadline) {
876
877
  break;
877
878
  }
@@ -1060,7 +1061,7 @@ var Ratelimit = class {
1060
1061
  };
1061
1062
 
1062
1063
  // src/version.ts
1063
- var VERSION = "2.1.0-rc";
1064
+ var VERSION = "2.2.0";
1064
1065
 
1065
1066
  // src/telemetry.ts
1066
1067
  var taggedClients = /* @__PURE__ */ new WeakSet();
@@ -1522,11 +1523,11 @@ var RegionRatelimit = class extends Ratelimit {
1522
1523
  };
1523
1524
  }
1524
1525
  }
1525
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1526
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1526
1527
  const [usedTokensAfterUpdate, effectiveLimit] = await safeEval(
1527
1528
  ctx,
1528
1529
  SCRIPTS.singleRegion.fixedWindow.limit,
1529
- [key, dynamicLimitKey],
1530
+ [key, ...dynamicLimitKeys],
1530
1531
  [tokens, windowDuration, incrementBy]
1531
1532
  );
1532
1533
  const success = usedTokensAfterUpdate <= effectiveLimit;
@@ -1550,11 +1551,11 @@ var RegionRatelimit = class extends Ratelimit {
1550
1551
  async getRemaining(ctx, identifier) {
1551
1552
  const bucket = Math.floor(Date.now() / windowDuration);
1552
1553
  const key = [identifier, bucket].join(":");
1553
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1554
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1554
1555
  const [remaining, effectiveLimit] = await safeEval(
1555
1556
  ctx,
1556
1557
  SCRIPTS.singleRegion.fixedWindow.getRemaining,
1557
- [key, dynamicLimitKey],
1558
+ [key, ...dynamicLimitKeys],
1558
1559
  [tokens]
1559
1560
  );
1560
1561
  return {
@@ -1616,11 +1617,11 @@ var RegionRatelimit = class extends Ratelimit {
1616
1617
  };
1617
1618
  }
1618
1619
  }
1619
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1620
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1620
1621
  const [remainingTokens, effectiveLimit] = await safeEval(
1621
1622
  ctx,
1622
1623
  SCRIPTS.singleRegion.slidingWindow.limit,
1623
- [currentKey, previousKey, dynamicLimitKey],
1624
+ [currentKey, previousKey, ...dynamicLimitKeys],
1624
1625
  [tokens, now, windowSize, incrementBy]
1625
1626
  );
1626
1627
  const success = remainingTokens >= 0;
@@ -1646,11 +1647,11 @@ var RegionRatelimit = class extends Ratelimit {
1646
1647
  const currentKey = [identifier, currentWindow].join(":");
1647
1648
  const previousWindow = currentWindow - 1;
1648
1649
  const previousKey = [identifier, previousWindow].join(":");
1649
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1650
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1650
1651
  const [remaining, effectiveLimit] = await safeEval(
1651
1652
  ctx,
1652
1653
  SCRIPTS.singleRegion.slidingWindow.getRemaining,
1653
- [currentKey, previousKey, dynamicLimitKey],
1654
+ [currentKey, previousKey, ...dynamicLimitKeys],
1654
1655
  [tokens, now, windowSize]
1655
1656
  );
1656
1657
  return {
@@ -1705,11 +1706,11 @@ var RegionRatelimit = class extends Ratelimit {
1705
1706
  };
1706
1707
  }
1707
1708
  }
1708
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1709
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1709
1710
  const [remaining, reset, effectiveLimit] = await safeEval(
1710
1711
  ctx,
1711
1712
  SCRIPTS.singleRegion.tokenBucket.limit,
1712
- [identifier, dynamicLimitKey],
1713
+ [identifier, ...dynamicLimitKeys],
1713
1714
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1714
1715
  );
1715
1716
  const success = remaining >= 0;
@@ -1729,11 +1730,11 @@ var RegionRatelimit = class extends Ratelimit {
1729
1730
  };
1730
1731
  },
1731
1732
  async getRemaining(ctx, identifier) {
1732
- const dynamicLimitKey = ctx.dynamicLimits ? `${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}` : "";
1733
+ const dynamicLimitKeys = ctx.dynamicLimits ? [`${ctx.prefix}${DYNAMIC_LIMIT_KEY_SUFFIX}`] : [];
1733
1734
  const [remainingTokens, refilledAt, effectiveLimit] = await safeEval(
1734
1735
  ctx,
1735
1736
  SCRIPTS.singleRegion.tokenBucket.getRemaining,
1736
- [identifier, dynamicLimitKey],
1737
+ [identifier, ...dynamicLimitKeys],
1737
1738
  [maxTokens]
1738
1739
  );
1739
1740
  const freshRefillAt = Date.now() + intervalDuration;
@@ -1801,7 +1802,7 @@ var RegionRatelimit = class extends Ratelimit {
1801
1802
  const hit = typeof ctx.cache.get(key) === "number";
1802
1803
  if (hit) {
1803
1804
  const cachedTokensAfterUpdate = ctx.cache.incr(key, incrementBy);
1804
- const success = cachedTokensAfterUpdate < tokens;
1805
+ const success = cachedTokensAfterUpdate <= tokens;
1805
1806
  const pending = success ? safeEval(
1806
1807
  ctx,
1807
1808
  SCRIPTS.singleRegion.cachedFixedWindow.limit,