@upstash/ratelimit 1.0.3 → 1.1.0-canary-1

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