@upstash/ratelimit 1.0.3 → 1.1.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.js CHANGED
@@ -122,6 +122,9 @@ var Cache = class {
122
122
  this.cache.set(key, value);
123
123
  return value;
124
124
  }
125
+ empty() {
126
+ this.cache.clear();
127
+ }
125
128
  };
126
129
 
127
130
  // src/duration.ts
@@ -149,7 +152,7 @@ function ms(d) {
149
152
  }
150
153
 
151
154
  // src/lua-scripts/multi.ts
152
- var fixedWindowScript = `
155
+ var fixedWindowLimitScript = `
153
156
  local key = KEYS[1]
154
157
  local id = ARGV[1]
155
158
  local window = ARGV[2]
@@ -165,7 +168,15 @@ var fixedWindowScript = `
165
168
 
166
169
  return fields
167
170
  `;
168
- var slidingWindowScript = `
171
+ var fixedWindowRemainingTokensScript = `
172
+ local key = KEYS[1]
173
+ local tokens = 0
174
+
175
+ local fields = redis.call("HGETALL", key)
176
+
177
+ return fields
178
+ `;
179
+ var slidingWindowLimitScript = `
169
180
  local currentKey = KEYS[1] -- identifier including prefixes
170
181
  local previousKey = KEYS[2] -- key of the previous bucket
171
182
  local tokens = tonumber(ARGV[1]) -- tokens per window
@@ -200,6 +211,54 @@ var slidingWindowScript = `
200
211
  end
201
212
  return {currentFields, previousFields, true}
202
213
  `;
214
+ var slidingWindowRemainingTokensScript = `
215
+ local currentKey = KEYS[1] -- identifier including prefixes
216
+ local previousKey = KEYS[2] -- key of the previous bucket
217
+ local now = ARGV[1] -- current timestamp in milliseconds
218
+ local window = ARGV[2] -- interval in milliseconds
219
+
220
+ local currentFields = redis.call("HGETALL", currentKey)
221
+ local requestsInCurrentWindow = 0
222
+ for i = 2, #currentFields, 2 do
223
+ requestsInCurrentWindow = requestsInCurrentWindow + tonumber(currentFields[i])
224
+ end
225
+
226
+ local previousFields = redis.call("HGETALL", previousKey)
227
+ local requestsInPreviousWindow = 0
228
+ for i = 2, #previousFields, 2 do
229
+ requestsInPreviousWindow = requestsInPreviousWindow + tonumber(previousFields[i])
230
+ end
231
+
232
+ local percentageInCurrent = ( now % window) / window
233
+ requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
234
+
235
+ return requestsInCurrentWindow + requestsInPreviousWindow
236
+ `;
237
+
238
+ // src/lua-scripts/reset.ts
239
+ var resetScript = `
240
+ local pattern = KEYS[1]
241
+
242
+ -- Initialize cursor to start from 0
243
+ local cursor = "0"
244
+
245
+ repeat
246
+ -- Scan for keys matching the pattern
247
+ local scan_result = redis.call('SCAN', cursor, 'MATCH', pattern)
248
+
249
+ -- Extract cursor for the next iteration
250
+ cursor = scan_result[1]
251
+
252
+ -- Extract keys from the scan result
253
+ local keys = scan_result[2]
254
+
255
+ for i=1, #keys do
256
+ redis.call('DEL', keys[i])
257
+ end
258
+
259
+ -- Continue scanning until cursor is 0 (end of keyspace)
260
+ until cursor == "0"
261
+ `;
203
262
 
204
263
  // src/ratelimit.ts
205
264
  var Ratelimit = class {
@@ -263,7 +322,7 @@ var Ratelimit = class {
263
322
  const key = [this.prefix, identifier].join(":");
264
323
  let timeoutId = null;
265
324
  try {
266
- const arr = [this.limiter(this.ctx, key, req?.rate)];
325
+ const arr = [this.limiter().limit(this.ctx, key, req?.rate)];
267
326
  if (this.timeout > 0) {
268
327
  arr.push(
269
328
  new Promise((resolve) => {
@@ -347,6 +406,14 @@ var Ratelimit = class {
347
406
  }
348
407
  return res;
349
408
  };
409
+ resetUsedTokens = async (identifier) => {
410
+ const pattern = [this.prefix, identifier].join(":");
411
+ await this.limiter().resetTokens(this.ctx, pattern);
412
+ };
413
+ getRemaining = async (identifier) => {
414
+ const pattern = [this.prefix, identifier].join(":");
415
+ return await this.limiter().getRemaining(this.ctx, pattern);
416
+ };
350
417
  };
351
418
 
352
419
  // src/multi.ts
@@ -395,91 +462,119 @@ var MultiRegionRatelimit = class extends Ratelimit {
395
462
  */
396
463
  static fixedWindow(tokens, window) {
397
464
  const windowDuration = ms(window);
398
- return async (ctx, identifier, rate) => {
399
- if (ctx.cache) {
400
- const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
401
- if (blocked) {
402
- return {
403
- success: false,
404
- limit: tokens,
405
- remaining: 0,
406
- reset: reset2,
407
- pending: Promise.resolve()
408
- };
409
- }
410
- }
411
- const requestId = randomId();
412
- const bucket = Math.floor(Date.now() / windowDuration);
413
- const key = [identifier, bucket].join(":");
414
- const incrementBy = rate ? Math.max(1, rate) : 1;
415
- const dbs = ctx.redis.map((redis) => ({
416
- redis,
417
- request: redis.eval(
418
- fixedWindowScript,
419
- [key],
420
- [requestId, windowDuration, incrementBy]
421
- )
422
- }));
423
- const firstResponse = await Promise.any(dbs.map((s) => s.request));
424
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
425
- let parsedToken = 0;
426
- if (index % 2) {
427
- parsedToken = Number.parseInt(usedToken);
465
+ return () => ({
466
+ async limit(ctx, identifier, rate) {
467
+ if (ctx.cache) {
468
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
469
+ if (blocked) {
470
+ return {
471
+ success: false,
472
+ limit: tokens,
473
+ remaining: 0,
474
+ reset: reset2,
475
+ pending: Promise.resolve()
476
+ };
477
+ }
428
478
  }
429
- return accTokens + parsedToken;
430
- }, 0);
431
- const remaining = tokens - usedTokens;
432
- async function sync() {
433
- const individualIDs = await Promise.all(dbs.map((s) => s.request));
434
- const allIDs = Array.from(
435
- new Set(
436
- individualIDs.flatMap((_) => _).reduce((acc, curr, index) => {
479
+ const requestId = randomId();
480
+ const bucket = Math.floor(Date.now() / windowDuration);
481
+ const key = [identifier, bucket].join(":");
482
+ const incrementBy = rate ? Math.max(1, rate) : 1;
483
+ const dbs = ctx.redis.map((redis) => ({
484
+ redis,
485
+ request: redis.eval(
486
+ fixedWindowLimitScript,
487
+ [key],
488
+ [requestId, windowDuration, incrementBy]
489
+ )
490
+ }));
491
+ const firstResponse = await Promise.any(dbs.map((s) => s.request));
492
+ const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
493
+ let parsedToken = 0;
494
+ if (index % 2) {
495
+ parsedToken = Number.parseInt(usedToken);
496
+ }
497
+ return accTokens + parsedToken;
498
+ }, 0);
499
+ const remaining = tokens - usedTokens;
500
+ async function sync() {
501
+ const individualIDs = await Promise.all(dbs.map((s) => s.request));
502
+ const allIDs = Array.from(
503
+ new Set(
504
+ individualIDs.flatMap((_) => _).reduce((acc, curr, index) => {
505
+ if (index % 2 === 0) {
506
+ acc.push(curr);
507
+ }
508
+ return acc;
509
+ }, [])
510
+ ).values()
511
+ );
512
+ for (const db of dbs) {
513
+ const usedDbTokens = (await db.request).reduce(
514
+ (accTokens, usedToken, index) => {
515
+ let parsedToken = 0;
516
+ if (index % 2) {
517
+ parsedToken = Number.parseInt(usedToken);
518
+ }
519
+ return accTokens + parsedToken;
520
+ },
521
+ 0
522
+ );
523
+ const dbIds = (await db.request).reduce((ids, currentId, index) => {
437
524
  if (index % 2 === 0) {
438
- acc.push(curr);
525
+ ids.push(currentId);
439
526
  }
440
- return acc;
441
- }, [])
442
- ).values()
443
- );
444
- for (const db of dbs) {
445
- const usedDbTokens = (await db.request).reduce((accTokens, usedToken, index) => {
446
- let parsedToken = 0;
447
- if (index % 2) {
448
- parsedToken = Number.parseInt(usedToken);
527
+ return ids;
528
+ }, []);
529
+ if (usedDbTokens >= tokens) {
530
+ continue;
449
531
  }
450
- return accTokens + parsedToken;
451
- }, 0);
452
- const dbIds = (await db.request).reduce((ids, currentId, index) => {
453
- if (index % 2 === 0) {
454
- ids.push(currentId);
532
+ const diff = allIDs.filter((id) => !dbIds.includes(id));
533
+ if (diff.length === 0) {
534
+ continue;
535
+ }
536
+ for (const requestId2 of diff) {
537
+ await db.redis.hset(key, { [requestId2]: incrementBy });
455
538
  }
456
- return ids;
457
- }, []);
458
- if (usedDbTokens >= tokens) {
459
- continue;
460
- }
461
- const diff = allIDs.filter((id) => !dbIds.includes(id));
462
- if (diff.length === 0) {
463
- continue;
464
539
  }
465
- for (const requestId2 of diff) {
466
- await db.redis.hset(key, { [requestId2]: incrementBy });
540
+ }
541
+ const success = remaining > 0;
542
+ const reset = (bucket + 1) * windowDuration;
543
+ if (ctx.cache && !success) {
544
+ ctx.cache.blockUntil(identifier, reset);
545
+ }
546
+ return {
547
+ success,
548
+ limit: tokens,
549
+ remaining,
550
+ reset,
551
+ pending: sync()
552
+ };
553
+ },
554
+ async getRemaining(ctx, identifier) {
555
+ const bucket = Math.floor(Date.now() / windowDuration);
556
+ const key = [identifier, bucket].join(":");
557
+ const dbs = ctx.redis.map((redis) => ({
558
+ redis,
559
+ request: redis.eval(fixedWindowRemainingTokensScript, [key], [null])
560
+ }));
561
+ const firstResponse = await Promise.any(dbs.map((s) => s.request));
562
+ const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
563
+ let parsedToken = 0;
564
+ if (index % 2) {
565
+ parsedToken = Number.parseInt(usedToken);
467
566
  }
567
+ return accTokens + parsedToken;
568
+ }, 0);
569
+ return Math.max(0, tokens - usedTokens);
570
+ },
571
+ async resetTokens(ctx, identifier) {
572
+ const pattern = [identifier, "*"].join(":");
573
+ for (const db of ctx.redis) {
574
+ await db.eval(resetScript, [pattern], [null]);
468
575
  }
469
576
  }
470
- const success = remaining > 0;
471
- const reset = (bucket + 1) * windowDuration;
472
- if (ctx.cache && !success) {
473
- ctx.cache.blockUntil(identifier, reset);
474
- }
475
- return {
476
- success,
477
- limit: tokens,
478
- remaining,
479
- reset,
480
- pending: sync()
481
- };
482
- };
577
+ });
483
578
  }
484
579
  /**
485
580
  * Combined approach of `slidingLogs` and `fixedWindow` with lower storage
@@ -500,100 +595,126 @@ var MultiRegionRatelimit = class extends Ratelimit {
500
595
  static slidingWindow(tokens, window) {
501
596
  const windowSize = ms(window);
502
597
  const windowDuration = ms(window);
503
- return async (ctx, identifier, rate) => {
504
- const requestId = randomId();
505
- const now = Date.now();
506
- const currentWindow = Math.floor(now / windowSize);
507
- const currentKey = [identifier, currentWindow].join(":");
508
- const previousWindow = currentWindow - 1;
509
- const previousKey = [identifier, previousWindow].join(":");
510
- const incrementBy = rate ? Math.max(1, rate) : 1;
511
- const dbs = ctx.redis.map((redis) => ({
512
- redis,
513
- request: redis.eval(
514
- slidingWindowScript,
515
- [currentKey, previousKey],
516
- [tokens, now, windowDuration, requestId, incrementBy]
517
- // lua seems to return `1` for true and `null` for false
518
- )
519
- }));
520
- const percentageInCurrent = now % windowDuration / windowDuration;
521
- const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
522
- const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
523
- let parsedToken = 0;
524
- if (index % 2) {
525
- parsedToken = Number.parseInt(usedToken);
526
- }
527
- return accTokens + parsedToken;
528
- }, 0);
529
- const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
530
- let parsedToken = 0;
531
- if (index % 2) {
532
- parsedToken = Number.parseInt(usedToken);
533
- }
534
- return accTokens + parsedToken;
535
- }, 0);
536
- const previousPartialUsed = previousUsedTokens * (1 - percentageInCurrent);
537
- const usedTokens = previousPartialUsed + currentUsedTokens;
538
- const remaining = tokens - usedTokens;
539
- async function sync() {
540
- const res = await Promise.all(dbs.map((s) => s.request));
541
- const allCurrentIds = res.flatMap(([current2]) => current2).reduce((accCurrentIds, curr, index) => {
542
- if (index % 2 === 0) {
543
- accCurrentIds.push(curr);
598
+ return () => ({
599
+ async limit(ctx, identifier, rate) {
600
+ const requestId = randomId();
601
+ const now = Date.now();
602
+ const currentWindow = Math.floor(now / windowSize);
603
+ const currentKey = [identifier, currentWindow].join(":");
604
+ const previousWindow = currentWindow - 1;
605
+ const previousKey = [identifier, previousWindow].join(":");
606
+ const incrementBy = rate ? Math.max(1, rate) : 1;
607
+ const dbs = ctx.redis.map((redis) => ({
608
+ redis,
609
+ request: redis.eval(
610
+ slidingWindowLimitScript,
611
+ [currentKey, previousKey],
612
+ [tokens, now, windowDuration, requestId, incrementBy]
613
+ // lua seems to return `1` for true and `null` for false
614
+ )
615
+ }));
616
+ const percentageInCurrent = now % windowDuration / windowDuration;
617
+ const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
618
+ const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
619
+ let parsedToken = 0;
620
+ if (index % 2) {
621
+ parsedToken = Number.parseInt(usedToken);
622
+ }
623
+ return accTokens + parsedToken;
624
+ }, 0);
625
+ const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
626
+ let parsedToken = 0;
627
+ if (index % 2) {
628
+ parsedToken = Number.parseInt(usedToken);
544
629
  }
545
- return accCurrentIds;
546
- }, []);
547
- for (const db of dbs) {
548
- const [_current, previous2, _success] = await db.request;
549
- const dbIds = previous2.reduce((ids, currentId, index) => {
630
+ return accTokens + parsedToken;
631
+ }, 0);
632
+ const previousPartialUsed = previousUsedTokens * (1 - percentageInCurrent);
633
+ const usedTokens = previousPartialUsed + currentUsedTokens;
634
+ const remaining = tokens - usedTokens;
635
+ async function sync() {
636
+ const res = await Promise.all(dbs.map((s) => s.request));
637
+ const allCurrentIds = res.flatMap(([current2]) => current2).reduce((accCurrentIds, curr, index) => {
550
638
  if (index % 2 === 0) {
551
- ids.push(currentId);
639
+ accCurrentIds.push(curr);
552
640
  }
553
- return ids;
641
+ return accCurrentIds;
554
642
  }, []);
555
- const usedDbTokens = previous2.reduce((accTokens, usedToken, index) => {
556
- let parsedToken = 0;
557
- if (index % 2) {
558
- parsedToken = Number.parseInt(usedToken);
643
+ for (const db of dbs) {
644
+ const [_current, previous2, _success] = await db.request;
645
+ const dbIds = previous2.reduce((ids, currentId, index) => {
646
+ if (index % 2 === 0) {
647
+ ids.push(currentId);
648
+ }
649
+ return ids;
650
+ }, []);
651
+ const usedDbTokens = previous2.reduce((accTokens, usedToken, index) => {
652
+ let parsedToken = 0;
653
+ if (index % 2) {
654
+ parsedToken = Number.parseInt(usedToken);
655
+ }
656
+ return accTokens + parsedToken;
657
+ }, 0);
658
+ if (usedDbTokens >= tokens) {
659
+ continue;
660
+ }
661
+ const diff = allCurrentIds.filter((id) => !dbIds.includes(id));
662
+ if (diff.length === 0) {
663
+ continue;
664
+ }
665
+ for (const requestId2 of diff) {
666
+ await db.redis.hset(currentKey, { [requestId2]: incrementBy });
559
667
  }
560
- return accTokens + parsedToken;
561
- }, 0);
562
- if (usedDbTokens >= tokens) {
563
- continue;
564
- }
565
- const diff = allCurrentIds.filter((id) => !dbIds.includes(id));
566
- if (diff.length === 0) {
567
- continue;
568
- }
569
- for (const requestId2 of diff) {
570
- await db.redis.hset(currentKey, { [requestId2]: incrementBy });
571
668
  }
572
669
  }
670
+ const reset = (currentWindow + 1) * windowDuration;
671
+ if (ctx.cache && !success) {
672
+ ctx.cache.blockUntil(identifier, reset);
673
+ }
674
+ return {
675
+ success: Boolean(success),
676
+ limit: tokens,
677
+ remaining: Math.max(0, remaining),
678
+ reset,
679
+ pending: sync()
680
+ };
681
+ },
682
+ async getRemaining(ctx, identifier) {
683
+ const now = Date.now();
684
+ const currentWindow = Math.floor(now / windowSize);
685
+ const currentKey = [identifier, currentWindow].join(":");
686
+ const previousWindow = currentWindow - 1;
687
+ const previousKey = [identifier, previousWindow].join(":");
688
+ const dbs = ctx.redis.map((redis) => ({
689
+ redis,
690
+ request: redis.eval(
691
+ slidingWindowRemainingTokensScript,
692
+ [currentKey, previousKey],
693
+ [now, windowSize]
694
+ // lua seems to return `1` for true and `null` for false
695
+ )
696
+ }));
697
+ const usedTokens = await Promise.any(dbs.map((s) => s.request));
698
+ return Math.max(0, tokens - usedTokens);
699
+ },
700
+ async resetTokens(ctx, identifier) {
701
+ const pattern = [identifier, "*"].join(":");
702
+ for (const db of ctx.redis) {
703
+ await db.eval(resetScript, [pattern], [null]);
704
+ }
573
705
  }
574
- const reset = (currentWindow + 1) * windowDuration;
575
- if (ctx.cache && !success) {
576
- ctx.cache.blockUntil(identifier, reset);
577
- }
578
- return {
579
- success: Boolean(success),
580
- limit: tokens,
581
- remaining: Math.max(0, remaining),
582
- reset,
583
- pending: sync()
584
- };
585
- };
706
+ });
586
707
  }
587
708
  };
588
709
 
589
710
  // src/lua-scripts/single.ts
590
- var fixedWindowScript2 = `
711
+ var fixedWindowLimitScript2 = `
591
712
  local key = KEYS[1]
592
713
  local window = ARGV[1]
593
714
  local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
594
715
 
595
716
  local r = redis.call("INCRBY", key, incrementBy)
596
- if r == incrementBy then
717
+ if r == tonumber(incrementBy) then
597
718
  -- The first time this key is set, the value will be equal to incrementBy.
598
719
  -- So we only need the expire command once
599
720
  redis.call("PEXPIRE", key, window)
@@ -601,7 +722,17 @@ var fixedWindowScript2 = `
601
722
 
602
723
  return r
603
724
  `;
604
- var slidingWindowScript2 = `
725
+ var fixedWindowRemainingTokensScript2 = `
726
+ local key = KEYS[1]
727
+ local tokens = 0
728
+
729
+ local value = redis.call('GET', key)
730
+ if value then
731
+ tokens = value
732
+ end
733
+ return tokens
734
+ `;
735
+ var slidingWindowLimitScript2 = `
605
736
  local currentKey = KEYS[1] -- identifier including prefixes
606
737
  local previousKey = KEYS[2] -- key of the previous bucket
607
738
  local tokens = tonumber(ARGV[1]) -- tokens per window
@@ -626,14 +757,36 @@ var slidingWindowScript2 = `
626
757
  end
627
758
 
628
759
  local newValue = redis.call("INCRBY", currentKey, incrementBy)
629
- if newValue == incrementBy then
760
+ if newValue == tonumber(incrementBy) then
630
761
  -- The first time this key is set, the value will be equal to incrementBy.
631
762
  -- So we only need the expire command once
632
763
  redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
633
764
  end
634
765
  return tokens - ( newValue + requestsInPreviousWindow )
635
766
  `;
636
- var tokenBucketScript = `
767
+ var slidingWindowRemainingTokensScript2 = `
768
+ local currentKey = KEYS[1] -- identifier including prefixes
769
+ local previousKey = KEYS[2] -- key of the previous bucket
770
+ local now = ARGV[1] -- current timestamp in milliseconds
771
+ local window = ARGV[2] -- interval in milliseconds
772
+
773
+ local requestsInCurrentWindow = redis.call("GET", currentKey)
774
+ if requestsInCurrentWindow == false then
775
+ requestsInCurrentWindow = 0
776
+ end
777
+
778
+ local requestsInPreviousWindow = redis.call("GET", previousKey)
779
+ if requestsInPreviousWindow == false then
780
+ requestsInPreviousWindow = 0
781
+ end
782
+
783
+ local percentageInCurrent = ( now % window ) / window
784
+ -- weighted requests to consider from the previous window
785
+ requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
786
+
787
+ return requestsInPreviousWindow + requestsInCurrentWindow
788
+ `;
789
+ var tokenBucketLimitScript = `
637
790
  local key = KEYS[1] -- identifier including prefixes
638
791
  local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
639
792
  local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
@@ -672,7 +825,19 @@ var tokenBucketScript = `
672
825
  redis.call("PEXPIRE", key, expireAt)
673
826
  return {remaining, refilledAt + interval}
674
827
  `;
675
- var cachedFixedWindowScript = `
828
+ var tokenBucketRemainingTokensScript = `
829
+ local key = KEYS[1]
830
+ local maxTokens = tonumber(ARGV[1])
831
+
832
+ local bucket = redis.call("HMGET", key, "tokens")
833
+
834
+ if bucket[1] == false then
835
+ return maxTokens
836
+ end
837
+
838
+ return tonumber(bucket[1])
839
+ `;
840
+ var cachedFixedWindowLimitScript = `
676
841
  local key = KEYS[1]
677
842
  local window = ARGV[1]
678
843
  local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
@@ -686,6 +851,16 @@ var cachedFixedWindowScript = `
686
851
 
687
852
  return r
688
853
  `;
854
+ var cachedFixedWindowRemainingTokenScript = `
855
+ local key = KEYS[1]
856
+ local tokens = 0
857
+
858
+ local value = redis.call('GET', key)
859
+ if value then
860
+ tokens = value
861
+ end
862
+ return tokens
863
+ `;
689
864
 
690
865
  // src/single.ts
691
866
  var RegionRatelimit = class extends Ratelimit {
@@ -724,41 +899,57 @@ var RegionRatelimit = class extends Ratelimit {
724
899
  */
725
900
  static fixedWindow(tokens, window) {
726
901
  const windowDuration = ms(window);
727
- return async (ctx, identifier, rate) => {
728
- const bucket = Math.floor(Date.now() / windowDuration);
729
- const key = [identifier, bucket].join(":");
730
- if (ctx.cache) {
731
- const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
732
- if (blocked) {
733
- return {
734
- success: false,
735
- limit: tokens,
736
- remaining: 0,
737
- reset: reset2,
738
- pending: Promise.resolve()
739
- };
902
+ return () => ({
903
+ async limit(ctx, identifier, rate) {
904
+ const bucket = Math.floor(Date.now() / windowDuration);
905
+ const key = [identifier, bucket].join(":");
906
+ if (ctx.cache) {
907
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
908
+ if (blocked) {
909
+ return {
910
+ success: false,
911
+ limit: tokens,
912
+ remaining: 0,
913
+ reset: reset2,
914
+ pending: Promise.resolve()
915
+ };
916
+ }
740
917
  }
918
+ const incrementBy = rate ? Math.max(1, rate) : 1;
919
+ const usedTokensAfterUpdate = await ctx.redis.eval(
920
+ fixedWindowLimitScript2,
921
+ [key],
922
+ [windowDuration, incrementBy]
923
+ );
924
+ const success = usedTokensAfterUpdate <= tokens;
925
+ const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
926
+ const reset = (bucket + 1) * windowDuration;
927
+ if (ctx.cache && !success) {
928
+ ctx.cache.blockUntil(identifier, reset);
929
+ }
930
+ return {
931
+ success,
932
+ limit: tokens,
933
+ remaining: remainingTokens,
934
+ reset,
935
+ pending: Promise.resolve()
936
+ };
937
+ },
938
+ async getRemaining(ctx, identifier) {
939
+ const bucket = Math.floor(Date.now() / windowDuration);
940
+ const key = [identifier, bucket].join(":");
941
+ const usedTokens = await ctx.redis.eval(
942
+ fixedWindowRemainingTokensScript2,
943
+ [key],
944
+ [null]
945
+ );
946
+ return Math.max(0, tokens - usedTokens);
947
+ },
948
+ async resetTokens(ctx, identifier) {
949
+ const pattern = [identifier, "*"].join(":");
950
+ await ctx.redis.eval(resetScript, [pattern], [null]);
741
951
  }
742
- const incrementBy = rate ? Math.max(1, rate) : 1;
743
- const usedTokensAfterUpdate = await ctx.redis.eval(
744
- fixedWindowScript2,
745
- [key],
746
- [windowDuration, incrementBy]
747
- );
748
- const success = usedTokensAfterUpdate <= tokens;
749
- const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
750
- const reset = (bucket + 1) * windowDuration;
751
- if (ctx.cache && !success) {
752
- ctx.cache.blockUntil(identifier, reset);
753
- }
754
- return {
755
- success,
756
- limit: tokens,
757
- remaining: remainingTokens,
758
- reset,
759
- pending: Promise.resolve()
760
- };
761
- };
952
+ });
762
953
  }
763
954
  /**
764
955
  * Combined approach of `slidingLogs` and `fixedWindow` with lower storage
@@ -778,43 +969,62 @@ var RegionRatelimit = class extends Ratelimit {
778
969
  */
779
970
  static slidingWindow(tokens, window) {
780
971
  const windowSize = ms(window);
781
- return async (ctx, identifier, rate) => {
782
- const now = Date.now();
783
- const currentWindow = Math.floor(now / windowSize);
784
- const currentKey = [identifier, currentWindow].join(":");
785
- const previousWindow = currentWindow - 1;
786
- const previousKey = [identifier, previousWindow].join(":");
787
- if (ctx.cache) {
788
- const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
789
- if (blocked) {
790
- return {
791
- success: false,
792
- limit: tokens,
793
- remaining: 0,
794
- reset: reset2,
795
- pending: Promise.resolve()
796
- };
972
+ return () => ({
973
+ async limit(ctx, identifier, rate) {
974
+ const now = Date.now();
975
+ const currentWindow = Math.floor(now / windowSize);
976
+ const currentKey = [identifier, currentWindow].join(":");
977
+ const previousWindow = currentWindow - 1;
978
+ const previousKey = [identifier, previousWindow].join(":");
979
+ if (ctx.cache) {
980
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
981
+ if (blocked) {
982
+ return {
983
+ success: false,
984
+ limit: tokens,
985
+ remaining: 0,
986
+ reset: reset2,
987
+ pending: Promise.resolve()
988
+ };
989
+ }
797
990
  }
991
+ const incrementBy = rate ? Math.max(1, rate) : 1;
992
+ const remainingTokens = await ctx.redis.eval(
993
+ slidingWindowLimitScript2,
994
+ [currentKey, previousKey],
995
+ [tokens, now, windowSize, incrementBy]
996
+ );
997
+ const success = remainingTokens >= 0;
998
+ const reset = (currentWindow + 1) * windowSize;
999
+ if (ctx.cache && !success) {
1000
+ ctx.cache.blockUntil(identifier, reset);
1001
+ }
1002
+ return {
1003
+ success,
1004
+ limit: tokens,
1005
+ remaining: Math.max(0, remainingTokens),
1006
+ reset,
1007
+ pending: Promise.resolve()
1008
+ };
1009
+ },
1010
+ async getRemaining(ctx, identifier) {
1011
+ const now = Date.now();
1012
+ const currentWindow = Math.floor(now / windowSize);
1013
+ const currentKey = [identifier, currentWindow].join(":");
1014
+ const previousWindow = currentWindow - 1;
1015
+ const previousKey = [identifier, previousWindow].join(":");
1016
+ const usedTokens = await ctx.redis.eval(
1017
+ slidingWindowRemainingTokensScript2,
1018
+ [currentKey, previousKey],
1019
+ [now, windowSize]
1020
+ );
1021
+ return Math.max(0, tokens - usedTokens);
1022
+ },
1023
+ async resetTokens(ctx, identifier) {
1024
+ const pattern = [identifier, "*"].join(":");
1025
+ await ctx.redis.eval(resetScript, [pattern], [null]);
798
1026
  }
799
- const incrementBy = rate ? Math.max(1, rate) : 1;
800
- const remainingTokens = await ctx.redis.eval(
801
- slidingWindowScript2,
802
- [currentKey, previousKey],
803
- [tokens, now, windowSize, incrementBy]
804
- );
805
- const success = remainingTokens >= 0;
806
- const reset = (currentWindow + 1) * windowSize;
807
- if (ctx.cache && !success) {
808
- ctx.cache.blockUntil(identifier, reset);
809
- }
810
- return {
811
- success,
812
- limit: tokens,
813
- remaining: Math.max(0, remainingTokens),
814
- reset,
815
- pending: Promise.resolve()
816
- };
817
- };
1027
+ });
818
1028
  }
819
1029
  /**
820
1030
  * You have a bucket filled with `{maxTokens}` tokens that refills constantly
@@ -831,38 +1041,52 @@ var RegionRatelimit = class extends Ratelimit {
831
1041
  */
832
1042
  static tokenBucket(refillRate, interval, maxTokens) {
833
1043
  const intervalDuration = ms(interval);
834
- return async (ctx, identifier, rate) => {
835
- if (ctx.cache) {
836
- const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
837
- if (blocked) {
838
- return {
839
- success: false,
840
- limit: maxTokens,
841
- remaining: 0,
842
- reset: reset2,
843
- pending: Promise.resolve()
844
- };
1044
+ return () => ({
1045
+ async limit(ctx, identifier, rate) {
1046
+ if (ctx.cache) {
1047
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1048
+ if (blocked) {
1049
+ return {
1050
+ success: false,
1051
+ limit: maxTokens,
1052
+ remaining: 0,
1053
+ reset: reset2,
1054
+ pending: Promise.resolve()
1055
+ };
1056
+ }
845
1057
  }
1058
+ const now = Date.now();
1059
+ const incrementBy = rate ? Math.max(1, rate) : 1;
1060
+ const [remaining, reset] = await ctx.redis.eval(
1061
+ tokenBucketLimitScript,
1062
+ [identifier],
1063
+ [maxTokens, intervalDuration, refillRate, now, incrementBy]
1064
+ );
1065
+ const success = remaining >= 0;
1066
+ if (ctx.cache && !success) {
1067
+ ctx.cache.blockUntil(identifier, reset);
1068
+ }
1069
+ return {
1070
+ success,
1071
+ limit: maxTokens,
1072
+ remaining,
1073
+ reset,
1074
+ pending: Promise.resolve()
1075
+ };
1076
+ },
1077
+ async getRemaining(ctx, identifier) {
1078
+ const remainingTokens = await ctx.redis.eval(
1079
+ tokenBucketRemainingTokensScript,
1080
+ [identifier],
1081
+ [maxTokens]
1082
+ );
1083
+ return remainingTokens;
1084
+ },
1085
+ async resetTokens(ctx, identifier) {
1086
+ const pattern = identifier;
1087
+ await ctx.redis.eval(resetScript, [pattern], [null]);
846
1088
  }
847
- const now = Date.now();
848
- const incrementBy = rate ? Math.max(1, rate) : 1;
849
- const [remaining, reset] = await ctx.redis.eval(
850
- tokenBucketScript,
851
- [identifier],
852
- [maxTokens, intervalDuration, refillRate, now, incrementBy]
853
- );
854
- const success = remaining >= 0;
855
- if (ctx.cache && !success) {
856
- ctx.cache.blockUntil(identifier, reset);
857
- }
858
- return {
859
- success,
860
- limit: maxTokens,
861
- remaining,
862
- reset,
863
- pending: Promise.resolve()
864
- };
865
- };
1089
+ });
866
1090
  }
867
1091
  /**
868
1092
  * cachedFixedWindow first uses the local cache to decide if a request may pass and then updates
@@ -890,44 +1114,72 @@ var RegionRatelimit = class extends Ratelimit {
890
1114
  */
891
1115
  static cachedFixedWindow(tokens, window) {
892
1116
  const windowDuration = ms(window);
893
- return async (ctx, identifier, rate) => {
894
- if (!ctx.cache) {
895
- throw new Error("This algorithm requires a cache");
896
- }
897
- const bucket = Math.floor(Date.now() / windowDuration);
898
- const key = [identifier, bucket].join(":");
899
- const reset = (bucket + 1) * windowDuration;
900
- const incrementBy = rate ? Math.max(1, rate) : 1;
901
- const hit = typeof ctx.cache.get(key) === "number";
902
- if (hit) {
903
- const cachedTokensAfterUpdate = ctx.cache.incr(key);
904
- const success = cachedTokensAfterUpdate < tokens;
905
- const pending = success ? ctx.redis.eval(cachedFixedWindowScript, [key], [windowDuration, incrementBy]).then((t) => {
906
- ctx.cache.set(key, t);
907
- }) : Promise.resolve();
1117
+ return () => ({
1118
+ async limit(ctx, identifier, rate) {
1119
+ if (!ctx.cache) {
1120
+ throw new Error("This algorithm requires a cache");
1121
+ }
1122
+ const bucket = Math.floor(Date.now() / windowDuration);
1123
+ const key = [identifier, bucket].join(":");
1124
+ const reset = (bucket + 1) * windowDuration;
1125
+ const incrementBy = rate ? Math.max(1, rate) : 1;
1126
+ const hit = typeof ctx.cache.get(key) === "number";
1127
+ if (hit) {
1128
+ const cachedTokensAfterUpdate = ctx.cache.incr(key);
1129
+ const success = cachedTokensAfterUpdate < tokens;
1130
+ const pending = success ? ctx.redis.eval(cachedFixedWindowLimitScript, [key], [windowDuration, incrementBy]).then((t) => {
1131
+ ctx.cache.set(key, t);
1132
+ }) : Promise.resolve();
1133
+ return {
1134
+ success,
1135
+ limit: tokens,
1136
+ remaining: tokens - cachedTokensAfterUpdate,
1137
+ reset,
1138
+ pending
1139
+ };
1140
+ }
1141
+ const usedTokensAfterUpdate = await ctx.redis.eval(
1142
+ cachedFixedWindowLimitScript,
1143
+ [key],
1144
+ [windowDuration, incrementBy]
1145
+ );
1146
+ ctx.cache.set(key, usedTokensAfterUpdate);
1147
+ const remaining = tokens - usedTokensAfterUpdate;
908
1148
  return {
909
- success,
1149
+ success: remaining >= 0,
910
1150
  limit: tokens,
911
- remaining: tokens - cachedTokensAfterUpdate,
1151
+ remaining,
912
1152
  reset,
913
- pending
1153
+ pending: Promise.resolve()
914
1154
  };
1155
+ },
1156
+ async getRemaining(ctx, identifier) {
1157
+ if (!ctx.cache) {
1158
+ throw new Error("This algorithm requires a cache");
1159
+ }
1160
+ const bucket = Math.floor(Date.now() / windowDuration);
1161
+ const key = [identifier, bucket].join(":");
1162
+ const hit = typeof ctx.cache.get(key) === "number";
1163
+ if (hit) {
1164
+ const cachedUsedTokens = ctx.cache.get(key) ?? 0;
1165
+ return Math.max(0, tokens - cachedUsedTokens);
1166
+ }
1167
+ const usedTokens = await ctx.redis.eval(
1168
+ cachedFixedWindowRemainingTokenScript,
1169
+ [key],
1170
+ [null]
1171
+ );
1172
+ return Math.max(0, tokens - usedTokens);
1173
+ },
1174
+ async resetTokens(ctx, identifier) {
1175
+ const pattern = [identifier, "*"].join(":");
1176
+ if (!ctx.cache) {
1177
+ throw new Error("This algorithm requires a cache");
1178
+ }
1179
+ ctx.cache.empty();
1180
+ await ctx.redis.eval(resetScript, [pattern], [null]);
915
1181
  }
916
- const usedTokensAfterUpdate = await ctx.redis.eval(
917
- cachedFixedWindowScript,
918
- [key],
919
- [windowDuration, incrementBy]
920
- );
921
- ctx.cache.set(key, usedTokensAfterUpdate);
922
- const remaining = tokens - usedTokensAfterUpdate;
923
- return {
924
- success: remaining >= 0,
925
- limit: tokens,
926
- remaining,
927
- reset,
928
- pending: Promise.resolve()
929
- };
930
- };
1182
+ });
931
1183
  }
932
1184
  };
933
1185
  // Annotate the CommonJS export names for ESM import in node: