@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.mjs CHANGED
@@ -94,6 +94,12 @@ var Cache = class {
94
94
  this.cache.set(key, value);
95
95
  return value;
96
96
  }
97
+ pop(key) {
98
+ this.cache.delete(key);
99
+ }
100
+ empty() {
101
+ this.cache.clear();
102
+ }
97
103
  };
98
104
 
99
105
  // src/duration.ts
@@ -121,7 +127,7 @@ function ms(d) {
121
127
  }
122
128
 
123
129
  // src/lua-scripts/multi.ts
124
- var fixedWindowScript = `
130
+ var fixedWindowLimitScript = `
125
131
  local key = KEYS[1]
126
132
  local id = ARGV[1]
127
133
  local window = ARGV[2]
@@ -137,7 +143,15 @@ var fixedWindowScript = `
137
143
 
138
144
  return fields
139
145
  `;
140
- var slidingWindowScript = `
146
+ var fixedWindowRemainingTokensScript = `
147
+ local key = KEYS[1]
148
+ local tokens = 0
149
+
150
+ local fields = redis.call("HGETALL", key)
151
+
152
+ return fields
153
+ `;
154
+ var slidingWindowLimitScript = `
141
155
  local currentKey = KEYS[1] -- identifier including prefixes
142
156
  local previousKey = KEYS[2] -- key of the previous bucket
143
157
  local tokens = tonumber(ARGV[1]) -- tokens per window
@@ -172,6 +186,54 @@ var slidingWindowScript = `
172
186
  end
173
187
  return {currentFields, previousFields, true}
174
188
  `;
189
+ var slidingWindowRemainingTokensScript = `
190
+ local currentKey = KEYS[1] -- identifier including prefixes
191
+ local previousKey = KEYS[2] -- key of the previous bucket
192
+ local now = ARGV[1] -- current timestamp in milliseconds
193
+ local window = ARGV[2] -- interval in milliseconds
194
+
195
+ local currentFields = redis.call("HGETALL", currentKey)
196
+ local requestsInCurrentWindow = 0
197
+ for i = 2, #currentFields, 2 do
198
+ requestsInCurrentWindow = requestsInCurrentWindow + tonumber(currentFields[i])
199
+ end
200
+
201
+ local previousFields = redis.call("HGETALL", previousKey)
202
+ local requestsInPreviousWindow = 0
203
+ for i = 2, #previousFields, 2 do
204
+ requestsInPreviousWindow = requestsInPreviousWindow + tonumber(previousFields[i])
205
+ end
206
+
207
+ local percentageInCurrent = ( now % window) / window
208
+ requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
209
+
210
+ return requestsInCurrentWindow + requestsInPreviousWindow
211
+ `;
212
+
213
+ // src/lua-scripts/reset.ts
214
+ var resetScript = `
215
+ local pattern = KEYS[1]
216
+
217
+ -- Initialize cursor to start from 0
218
+ local cursor = "0"
219
+
220
+ repeat
221
+ -- Scan for keys matching the pattern
222
+ local scan_result = redis.call('SCAN', cursor, 'MATCH', pattern)
223
+
224
+ -- Extract cursor for the next iteration
225
+ cursor = scan_result[1]
226
+
227
+ -- Extract keys from the scan result
228
+ local keys = scan_result[2]
229
+
230
+ for i=1, #keys do
231
+ redis.call('DEL', keys[i])
232
+ end
233
+
234
+ -- Continue scanning until cursor is 0 (end of keyspace)
235
+ until cursor == "0"
236
+ `;
175
237
 
176
238
  // src/ratelimit.ts
177
239
  var Ratelimit = class {
@@ -235,7 +297,7 @@ var Ratelimit = class {
235
297
  const key = [this.prefix, identifier].join(":");
236
298
  let timeoutId = null;
237
299
  try {
238
- const arr = [this.limiter(this.ctx, key, req?.rate)];
300
+ const arr = [this.limiter().limit(this.ctx, key, req?.rate)];
239
301
  if (this.timeout > 0) {
240
302
  arr.push(
241
303
  new Promise((resolve) => {
@@ -319,6 +381,14 @@ var Ratelimit = class {
319
381
  }
320
382
  return res;
321
383
  };
384
+ resetUsedTokens = async (identifier) => {
385
+ const pattern = [this.prefix, identifier].join(":");
386
+ await this.limiter().resetTokens(this.ctx, pattern);
387
+ };
388
+ getRemaining = async (identifier) => {
389
+ const pattern = [this.prefix, identifier].join(":");
390
+ return await this.limiter().getRemaining(this.ctx, pattern);
391
+ };
322
392
  };
323
393
 
324
394
  // src/multi.ts
@@ -367,91 +437,122 @@ var MultiRegionRatelimit = class extends Ratelimit {
367
437
  */
368
438
  static fixedWindow(tokens, window) {
369
439
  const windowDuration = ms(window);
370
- return async (ctx, identifier, rate) => {
371
- if (ctx.cache) {
372
- const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
373
- if (blocked) {
374
- return {
375
- success: false,
376
- limit: tokens,
377
- remaining: 0,
378
- reset: reset2,
379
- pending: Promise.resolve()
380
- };
381
- }
382
- }
383
- const requestId = randomId();
384
- const bucket = Math.floor(Date.now() / windowDuration);
385
- const key = [identifier, bucket].join(":");
386
- const incrementBy = rate ? Math.max(1, rate) : 1;
387
- const dbs = ctx.redis.map((redis) => ({
388
- redis,
389
- request: redis.eval(
390
- fixedWindowScript,
391
- [key],
392
- [requestId, windowDuration, incrementBy]
393
- )
394
- }));
395
- const firstResponse = await Promise.any(dbs.map((s) => s.request));
396
- const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
397
- let parsedToken = 0;
398
- if (index % 2) {
399
- parsedToken = Number.parseInt(usedToken);
440
+ return () => ({
441
+ async limit(ctx, identifier, rate) {
442
+ if (ctx.cache) {
443
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
444
+ if (blocked) {
445
+ return {
446
+ success: false,
447
+ limit: tokens,
448
+ remaining: 0,
449
+ reset: reset2,
450
+ pending: Promise.resolve()
451
+ };
452
+ }
400
453
  }
401
- return accTokens + parsedToken;
402
- }, 0);
403
- const remaining = tokens - usedTokens;
404
- async function sync() {
405
- const individualIDs = await Promise.all(dbs.map((s) => s.request));
406
- const allIDs = Array.from(
407
- new Set(
408
- individualIDs.flatMap((_) => _).reduce((acc, curr, index) => {
454
+ const requestId = randomId();
455
+ const bucket = Math.floor(Date.now() / windowDuration);
456
+ const key = [identifier, bucket].join(":");
457
+ const incrementBy = rate ? Math.max(1, rate) : 1;
458
+ const dbs = ctx.redis.map((redis) => ({
459
+ redis,
460
+ request: redis.eval(
461
+ fixedWindowLimitScript,
462
+ [key],
463
+ [requestId, windowDuration, incrementBy]
464
+ )
465
+ }));
466
+ const firstResponse = await Promise.any(dbs.map((s) => s.request));
467
+ const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
468
+ let parsedToken = 0;
469
+ if (index % 2) {
470
+ parsedToken = Number.parseInt(usedToken);
471
+ }
472
+ return accTokens + parsedToken;
473
+ }, 0);
474
+ const remaining = tokens - usedTokens;
475
+ async function sync() {
476
+ const individualIDs = await Promise.all(dbs.map((s) => s.request));
477
+ const allIDs = Array.from(
478
+ new Set(
479
+ individualIDs.flatMap((_) => _).reduce((acc, curr, index) => {
480
+ if (index % 2 === 0) {
481
+ acc.push(curr);
482
+ }
483
+ return acc;
484
+ }, [])
485
+ ).values()
486
+ );
487
+ for (const db of dbs) {
488
+ const usedDbTokens = (await db.request).reduce(
489
+ (accTokens, usedToken, index) => {
490
+ let parsedToken = 0;
491
+ if (index % 2) {
492
+ parsedToken = Number.parseInt(usedToken);
493
+ }
494
+ return accTokens + parsedToken;
495
+ },
496
+ 0
497
+ );
498
+ const dbIds = (await db.request).reduce((ids, currentId, index) => {
409
499
  if (index % 2 === 0) {
410
- acc.push(curr);
500
+ ids.push(currentId);
411
501
  }
412
- return acc;
413
- }, [])
414
- ).values()
415
- );
416
- for (const db of dbs) {
417
- const usedDbTokens = (await db.request).reduce((accTokens, usedToken, index) => {
418
- let parsedToken = 0;
419
- if (index % 2) {
420
- parsedToken = Number.parseInt(usedToken);
502
+ return ids;
503
+ }, []);
504
+ if (usedDbTokens >= tokens) {
505
+ continue;
421
506
  }
422
- return accTokens + parsedToken;
423
- }, 0);
424
- const dbIds = (await db.request).reduce((ids, currentId, index) => {
425
- if (index % 2 === 0) {
426
- ids.push(currentId);
507
+ const diff = allIDs.filter((id) => !dbIds.includes(id));
508
+ if (diff.length === 0) {
509
+ continue;
510
+ }
511
+ for (const requestId2 of diff) {
512
+ await db.redis.hset(key, { [requestId2]: incrementBy });
427
513
  }
428
- return ids;
429
- }, []);
430
- if (usedDbTokens >= tokens) {
431
- continue;
432
- }
433
- const diff = allIDs.filter((id) => !dbIds.includes(id));
434
- if (diff.length === 0) {
435
- continue;
436
514
  }
437
- for (const requestId2 of diff) {
438
- await db.redis.hset(key, { [requestId2]: incrementBy });
515
+ }
516
+ const success = remaining > 0;
517
+ const reset = (bucket + 1) * windowDuration;
518
+ if (ctx.cache && !success) {
519
+ ctx.cache.blockUntil(identifier, reset);
520
+ }
521
+ return {
522
+ success,
523
+ limit: tokens,
524
+ remaining,
525
+ reset,
526
+ pending: sync()
527
+ };
528
+ },
529
+ async getRemaining(ctx, identifier) {
530
+ const bucket = Math.floor(Date.now() / windowDuration);
531
+ const key = [identifier, bucket].join(":");
532
+ const dbs = ctx.redis.map((redis) => ({
533
+ redis,
534
+ request: redis.eval(fixedWindowRemainingTokensScript, [key], [null])
535
+ }));
536
+ const firstResponse = await Promise.any(dbs.map((s) => s.request));
537
+ const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
538
+ let parsedToken = 0;
539
+ if (index % 2) {
540
+ parsedToken = Number.parseInt(usedToken);
439
541
  }
542
+ return accTokens + parsedToken;
543
+ }, 0);
544
+ return Math.max(0, tokens - usedTokens);
545
+ },
546
+ async resetTokens(ctx, identifier) {
547
+ const pattern = [identifier, "*"].join(":");
548
+ if (ctx.cache) {
549
+ ctx.cache.pop(identifier);
550
+ }
551
+ for (const db of ctx.redis) {
552
+ await db.eval(resetScript, [pattern], [null]);
440
553
  }
441
554
  }
442
- const success = remaining > 0;
443
- const reset = (bucket + 1) * windowDuration;
444
- if (ctx.cache && !success) {
445
- ctx.cache.blockUntil(identifier, reset);
446
- }
447
- return {
448
- success,
449
- limit: tokens,
450
- remaining,
451
- reset,
452
- pending: sync()
453
- };
454
- };
555
+ });
455
556
  }
456
557
  /**
457
558
  * Combined approach of `slidingLogs` and `fixedWindow` with lower storage
@@ -472,100 +573,129 @@ var MultiRegionRatelimit = class extends Ratelimit {
472
573
  static slidingWindow(tokens, window) {
473
574
  const windowSize = ms(window);
474
575
  const windowDuration = ms(window);
475
- return async (ctx, identifier, rate) => {
476
- const requestId = randomId();
477
- const now = Date.now();
478
- const currentWindow = Math.floor(now / windowSize);
479
- const currentKey = [identifier, currentWindow].join(":");
480
- const previousWindow = currentWindow - 1;
481
- const previousKey = [identifier, previousWindow].join(":");
482
- const incrementBy = rate ? Math.max(1, rate) : 1;
483
- const dbs = ctx.redis.map((redis) => ({
484
- redis,
485
- request: redis.eval(
486
- slidingWindowScript,
487
- [currentKey, previousKey],
488
- [tokens, now, windowDuration, requestId, incrementBy]
489
- // lua seems to return `1` for true and `null` for false
490
- )
491
- }));
492
- const percentageInCurrent = now % windowDuration / windowDuration;
493
- const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
494
- const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
495
- let parsedToken = 0;
496
- if (index % 2) {
497
- parsedToken = Number.parseInt(usedToken);
498
- }
499
- return accTokens + parsedToken;
500
- }, 0);
501
- const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
502
- let parsedToken = 0;
503
- if (index % 2) {
504
- parsedToken = Number.parseInt(usedToken);
505
- }
506
- return accTokens + parsedToken;
507
- }, 0);
508
- const previousPartialUsed = previousUsedTokens * (1 - percentageInCurrent);
509
- const usedTokens = previousPartialUsed + currentUsedTokens;
510
- const remaining = tokens - usedTokens;
511
- async function sync() {
512
- const res = await Promise.all(dbs.map((s) => s.request));
513
- const allCurrentIds = res.flatMap(([current2]) => current2).reduce((accCurrentIds, curr, index) => {
514
- if (index % 2 === 0) {
515
- accCurrentIds.push(curr);
576
+ return () => ({
577
+ async limit(ctx, identifier, rate) {
578
+ const requestId = randomId();
579
+ const now = Date.now();
580
+ const currentWindow = Math.floor(now / windowSize);
581
+ const currentKey = [identifier, currentWindow].join(":");
582
+ const previousWindow = currentWindow - 1;
583
+ const previousKey = [identifier, previousWindow].join(":");
584
+ const incrementBy = rate ? Math.max(1, rate) : 1;
585
+ const dbs = ctx.redis.map((redis) => ({
586
+ redis,
587
+ request: redis.eval(
588
+ slidingWindowLimitScript,
589
+ [currentKey, previousKey],
590
+ [tokens, now, windowDuration, requestId, incrementBy]
591
+ // lua seems to return `1` for true and `null` for false
592
+ )
593
+ }));
594
+ const percentageInCurrent = now % windowDuration / windowDuration;
595
+ const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
596
+ const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
597
+ let parsedToken = 0;
598
+ if (index % 2) {
599
+ parsedToken = Number.parseInt(usedToken);
516
600
  }
517
- return accCurrentIds;
518
- }, []);
519
- for (const db of dbs) {
520
- const [_current, previous2, _success] = await db.request;
521
- const dbIds = previous2.reduce((ids, currentId, index) => {
601
+ return accTokens + parsedToken;
602
+ }, 0);
603
+ const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
604
+ let parsedToken = 0;
605
+ if (index % 2) {
606
+ parsedToken = Number.parseInt(usedToken);
607
+ }
608
+ return accTokens + parsedToken;
609
+ }, 0);
610
+ const previousPartialUsed = previousUsedTokens * (1 - percentageInCurrent);
611
+ const usedTokens = previousPartialUsed + currentUsedTokens;
612
+ const remaining = tokens - usedTokens;
613
+ async function sync() {
614
+ const res = await Promise.all(dbs.map((s) => s.request));
615
+ const allCurrentIds = res.flatMap(([current2]) => current2).reduce((accCurrentIds, curr, index) => {
522
616
  if (index % 2 === 0) {
523
- ids.push(currentId);
617
+ accCurrentIds.push(curr);
524
618
  }
525
- return ids;
619
+ return accCurrentIds;
526
620
  }, []);
527
- const usedDbTokens = previous2.reduce((accTokens, usedToken, index) => {
528
- let parsedToken = 0;
529
- if (index % 2) {
530
- parsedToken = Number.parseInt(usedToken);
621
+ for (const db of dbs) {
622
+ const [_current, previous2, _success] = await db.request;
623
+ const dbIds = previous2.reduce((ids, currentId, index) => {
624
+ if (index % 2 === 0) {
625
+ ids.push(currentId);
626
+ }
627
+ return ids;
628
+ }, []);
629
+ const usedDbTokens = previous2.reduce((accTokens, usedToken, index) => {
630
+ let parsedToken = 0;
631
+ if (index % 2) {
632
+ parsedToken = Number.parseInt(usedToken);
633
+ }
634
+ return accTokens + parsedToken;
635
+ }, 0);
636
+ if (usedDbTokens >= tokens) {
637
+ continue;
638
+ }
639
+ const diff = allCurrentIds.filter((id) => !dbIds.includes(id));
640
+ if (diff.length === 0) {
641
+ continue;
642
+ }
643
+ for (const requestId2 of diff) {
644
+ await db.redis.hset(currentKey, { [requestId2]: incrementBy });
531
645
  }
532
- return accTokens + parsedToken;
533
- }, 0);
534
- if (usedDbTokens >= tokens) {
535
- continue;
536
- }
537
- const diff = allCurrentIds.filter((id) => !dbIds.includes(id));
538
- if (diff.length === 0) {
539
- continue;
540
- }
541
- for (const requestId2 of diff) {
542
- await db.redis.hset(currentKey, { [requestId2]: incrementBy });
543
646
  }
544
647
  }
648
+ const reset = (currentWindow + 1) * windowDuration;
649
+ if (ctx.cache && !success) {
650
+ ctx.cache.blockUntil(identifier, reset);
651
+ }
652
+ return {
653
+ success: Boolean(success),
654
+ limit: tokens,
655
+ remaining: Math.max(0, remaining),
656
+ reset,
657
+ pending: sync()
658
+ };
659
+ },
660
+ async getRemaining(ctx, identifier) {
661
+ const now = Date.now();
662
+ const currentWindow = Math.floor(now / windowSize);
663
+ const currentKey = [identifier, currentWindow].join(":");
664
+ const previousWindow = currentWindow - 1;
665
+ const previousKey = [identifier, previousWindow].join(":");
666
+ const dbs = ctx.redis.map((redis) => ({
667
+ redis,
668
+ request: redis.eval(
669
+ slidingWindowRemainingTokensScript,
670
+ [currentKey, previousKey],
671
+ [now, windowSize]
672
+ // lua seems to return `1` for true and `null` for false
673
+ )
674
+ }));
675
+ const usedTokens = await Promise.any(dbs.map((s) => s.request));
676
+ return Math.max(0, tokens - usedTokens);
677
+ },
678
+ async resetTokens(ctx, identifier) {
679
+ const pattern = [identifier, "*"].join(":");
680
+ if (ctx.cache) {
681
+ ctx.cache.pop(identifier);
682
+ }
683
+ for (const db of ctx.redis) {
684
+ await db.eval(resetScript, [pattern], [null]);
685
+ }
545
686
  }
546
- const reset = (currentWindow + 1) * windowDuration;
547
- if (ctx.cache && !success) {
548
- ctx.cache.blockUntil(identifier, reset);
549
- }
550
- return {
551
- success: Boolean(success),
552
- limit: tokens,
553
- remaining: Math.max(0, remaining),
554
- reset,
555
- pending: sync()
556
- };
557
- };
687
+ });
558
688
  }
559
689
  };
560
690
 
561
691
  // src/lua-scripts/single.ts
562
- var fixedWindowScript2 = `
692
+ var fixedWindowLimitScript2 = `
563
693
  local key = KEYS[1]
564
694
  local window = ARGV[1]
565
695
  local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
566
696
 
567
697
  local r = redis.call("INCRBY", key, incrementBy)
568
- if r == incrementBy then
698
+ if r == tonumber(incrementBy) then
569
699
  -- The first time this key is set, the value will be equal to incrementBy.
570
700
  -- So we only need the expire command once
571
701
  redis.call("PEXPIRE", key, window)
@@ -573,7 +703,17 @@ var fixedWindowScript2 = `
573
703
 
574
704
  return r
575
705
  `;
576
- var slidingWindowScript2 = `
706
+ var fixedWindowRemainingTokensScript2 = `
707
+ local key = KEYS[1]
708
+ local tokens = 0
709
+
710
+ local value = redis.call('GET', key)
711
+ if value then
712
+ tokens = value
713
+ end
714
+ return tokens
715
+ `;
716
+ var slidingWindowLimitScript2 = `
577
717
  local currentKey = KEYS[1] -- identifier including prefixes
578
718
  local previousKey = KEYS[2] -- key of the previous bucket
579
719
  local tokens = tonumber(ARGV[1]) -- tokens per window
@@ -598,14 +738,36 @@ var slidingWindowScript2 = `
598
738
  end
599
739
 
600
740
  local newValue = redis.call("INCRBY", currentKey, incrementBy)
601
- if newValue == incrementBy then
741
+ if newValue == tonumber(incrementBy) then
602
742
  -- The first time this key is set, the value will be equal to incrementBy.
603
743
  -- So we only need the expire command once
604
744
  redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
605
745
  end
606
746
  return tokens - ( newValue + requestsInPreviousWindow )
607
747
  `;
608
- var tokenBucketScript = `
748
+ var slidingWindowRemainingTokensScript2 = `
749
+ local currentKey = KEYS[1] -- identifier including prefixes
750
+ local previousKey = KEYS[2] -- key of the previous bucket
751
+ local now = ARGV[1] -- current timestamp in milliseconds
752
+ local window = ARGV[2] -- interval in milliseconds
753
+
754
+ local requestsInCurrentWindow = redis.call("GET", currentKey)
755
+ if requestsInCurrentWindow == false then
756
+ requestsInCurrentWindow = 0
757
+ end
758
+
759
+ local requestsInPreviousWindow = redis.call("GET", previousKey)
760
+ if requestsInPreviousWindow == false then
761
+ requestsInPreviousWindow = 0
762
+ end
763
+
764
+ local percentageInCurrent = ( now % window ) / window
765
+ -- weighted requests to consider from the previous window
766
+ requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
767
+
768
+ return requestsInPreviousWindow + requestsInCurrentWindow
769
+ `;
770
+ var tokenBucketLimitScript = `
609
771
  local key = KEYS[1] -- identifier including prefixes
610
772
  local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
611
773
  local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
@@ -644,7 +806,19 @@ var tokenBucketScript = `
644
806
  redis.call("PEXPIRE", key, expireAt)
645
807
  return {remaining, refilledAt + interval}
646
808
  `;
647
- var cachedFixedWindowScript = `
809
+ var tokenBucketRemainingTokensScript = `
810
+ local key = KEYS[1]
811
+ local maxTokens = tonumber(ARGV[1])
812
+
813
+ local bucket = redis.call("HMGET", key, "tokens")
814
+
815
+ if bucket[1] == false then
816
+ return maxTokens
817
+ end
818
+
819
+ return tonumber(bucket[1])
820
+ `;
821
+ var cachedFixedWindowLimitScript = `
648
822
  local key = KEYS[1]
649
823
  local window = ARGV[1]
650
824
  local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
@@ -658,6 +832,16 @@ var cachedFixedWindowScript = `
658
832
 
659
833
  return r
660
834
  `;
835
+ var cachedFixedWindowRemainingTokenScript = `
836
+ local key = KEYS[1]
837
+ local tokens = 0
838
+
839
+ local value = redis.call('GET', key)
840
+ if value then
841
+ tokens = value
842
+ end
843
+ return tokens
844
+ `;
661
845
 
662
846
  // src/single.ts
663
847
  var RegionRatelimit = class extends Ratelimit {
@@ -696,41 +880,60 @@ var RegionRatelimit = class extends Ratelimit {
696
880
  */
697
881
  static fixedWindow(tokens, window) {
698
882
  const windowDuration = ms(window);
699
- return async (ctx, identifier, rate) => {
700
- const bucket = Math.floor(Date.now() / windowDuration);
701
- const key = [identifier, bucket].join(":");
702
- if (ctx.cache) {
703
- const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
704
- if (blocked) {
705
- return {
706
- success: false,
707
- limit: tokens,
708
- remaining: 0,
709
- reset: reset2,
710
- pending: Promise.resolve()
711
- };
883
+ return () => ({
884
+ async limit(ctx, identifier, rate) {
885
+ const bucket = Math.floor(Date.now() / windowDuration);
886
+ const key = [identifier, bucket].join(":");
887
+ if (ctx.cache) {
888
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
889
+ if (blocked) {
890
+ return {
891
+ success: false,
892
+ limit: tokens,
893
+ remaining: 0,
894
+ reset: reset2,
895
+ pending: Promise.resolve()
896
+ };
897
+ }
712
898
  }
899
+ const incrementBy = rate ? Math.max(1, rate) : 1;
900
+ const usedTokensAfterUpdate = await ctx.redis.eval(
901
+ fixedWindowLimitScript2,
902
+ [key],
903
+ [windowDuration, incrementBy]
904
+ );
905
+ const success = usedTokensAfterUpdate <= tokens;
906
+ const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
907
+ const reset = (bucket + 1) * windowDuration;
908
+ if (ctx.cache && !success) {
909
+ ctx.cache.blockUntil(identifier, reset);
910
+ }
911
+ return {
912
+ success,
913
+ limit: tokens,
914
+ remaining: remainingTokens,
915
+ reset,
916
+ pending: Promise.resolve()
917
+ };
918
+ },
919
+ async getRemaining(ctx, identifier) {
920
+ const bucket = Math.floor(Date.now() / windowDuration);
921
+ const key = [identifier, bucket].join(":");
922
+ const usedTokens = await ctx.redis.eval(
923
+ fixedWindowRemainingTokensScript2,
924
+ [key],
925
+ [null]
926
+ );
927
+ return Math.max(0, tokens - usedTokens);
928
+ },
929
+ async resetTokens(ctx, identifier) {
930
+ const pattern = [identifier, "*"].join(":");
931
+ if (ctx.cache) {
932
+ ctx.cache.pop(identifier);
933
+ }
934
+ await ctx.redis.eval(resetScript, [pattern], [null]);
713
935
  }
714
- const incrementBy = rate ? Math.max(1, rate) : 1;
715
- const usedTokensAfterUpdate = await ctx.redis.eval(
716
- fixedWindowScript2,
717
- [key],
718
- [windowDuration, incrementBy]
719
- );
720
- const success = usedTokensAfterUpdate <= tokens;
721
- const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
722
- const reset = (bucket + 1) * windowDuration;
723
- if (ctx.cache && !success) {
724
- ctx.cache.blockUntil(identifier, reset);
725
- }
726
- return {
727
- success,
728
- limit: tokens,
729
- remaining: remainingTokens,
730
- reset,
731
- pending: Promise.resolve()
732
- };
733
- };
936
+ });
734
937
  }
735
938
  /**
736
939
  * Combined approach of `slidingLogs` and `fixedWindow` with lower storage
@@ -750,43 +953,65 @@ var RegionRatelimit = class extends Ratelimit {
750
953
  */
751
954
  static slidingWindow(tokens, window) {
752
955
  const windowSize = ms(window);
753
- return async (ctx, identifier, rate) => {
754
- const now = Date.now();
755
- const currentWindow = Math.floor(now / windowSize);
756
- const currentKey = [identifier, currentWindow].join(":");
757
- const previousWindow = currentWindow - 1;
758
- const previousKey = [identifier, previousWindow].join(":");
759
- if (ctx.cache) {
760
- const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
761
- if (blocked) {
762
- return {
763
- success: false,
764
- limit: tokens,
765
- remaining: 0,
766
- reset: reset2,
767
- pending: Promise.resolve()
768
- };
956
+ return () => ({
957
+ async limit(ctx, identifier, rate) {
958
+ const now = Date.now();
959
+ const currentWindow = Math.floor(now / windowSize);
960
+ const currentKey = [identifier, currentWindow].join(":");
961
+ const previousWindow = currentWindow - 1;
962
+ const previousKey = [identifier, previousWindow].join(":");
963
+ if (ctx.cache) {
964
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
965
+ if (blocked) {
966
+ return {
967
+ success: false,
968
+ limit: tokens,
969
+ remaining: 0,
970
+ reset: reset2,
971
+ pending: Promise.resolve()
972
+ };
973
+ }
769
974
  }
975
+ const incrementBy = rate ? Math.max(1, rate) : 1;
976
+ const remainingTokens = await ctx.redis.eval(
977
+ slidingWindowLimitScript2,
978
+ [currentKey, previousKey],
979
+ [tokens, now, windowSize, incrementBy]
980
+ );
981
+ const success = remainingTokens >= 0;
982
+ const reset = (currentWindow + 1) * windowSize;
983
+ if (ctx.cache && !success) {
984
+ ctx.cache.blockUntil(identifier, reset);
985
+ }
986
+ return {
987
+ success,
988
+ limit: tokens,
989
+ remaining: Math.max(0, remainingTokens),
990
+ reset,
991
+ pending: Promise.resolve()
992
+ };
993
+ },
994
+ async getRemaining(ctx, identifier) {
995
+ const now = Date.now();
996
+ const currentWindow = Math.floor(now / windowSize);
997
+ const currentKey = [identifier, currentWindow].join(":");
998
+ const previousWindow = currentWindow - 1;
999
+ const previousKey = [identifier, previousWindow].join(":");
1000
+ const usedTokens = await ctx.redis.eval(
1001
+ slidingWindowRemainingTokensScript2,
1002
+ [currentKey, previousKey],
1003
+ [now, windowSize]
1004
+ );
1005
+ return Math.max(0, tokens - usedTokens);
1006
+ },
1007
+ async resetTokens(ctx, identifier) {
1008
+ const pattern = [identifier, "*"].join(":");
1009
+ if (ctx.cache) {
1010
+ ctx.cache.pop(identifier);
1011
+ }
1012
+ await ctx.redis.eval(resetScript, [pattern], [null]);
770
1013
  }
771
- const incrementBy = rate ? Math.max(1, rate) : 1;
772
- const remainingTokens = await ctx.redis.eval(
773
- slidingWindowScript2,
774
- [currentKey, previousKey],
775
- [tokens, now, windowSize, incrementBy]
776
- );
777
- const success = remainingTokens >= 0;
778
- const reset = (currentWindow + 1) * windowSize;
779
- if (ctx.cache && !success) {
780
- ctx.cache.blockUntil(identifier, reset);
781
- }
782
- return {
783
- success,
784
- limit: tokens,
785
- remaining: Math.max(0, remainingTokens),
786
- reset,
787
- pending: Promise.resolve()
788
- };
789
- };
1014
+ });
790
1015
  }
791
1016
  /**
792
1017
  * You have a bucket filled with `{maxTokens}` tokens that refills constantly
@@ -803,38 +1028,55 @@ var RegionRatelimit = class extends Ratelimit {
803
1028
  */
804
1029
  static tokenBucket(refillRate, interval, maxTokens) {
805
1030
  const intervalDuration = ms(interval);
806
- return async (ctx, identifier, rate) => {
807
- if (ctx.cache) {
808
- const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
809
- if (blocked) {
810
- return {
811
- success: false,
812
- limit: maxTokens,
813
- remaining: 0,
814
- reset: reset2,
815
- pending: Promise.resolve()
816
- };
1031
+ return () => ({
1032
+ async limit(ctx, identifier, rate) {
1033
+ if (ctx.cache) {
1034
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
1035
+ if (blocked) {
1036
+ return {
1037
+ success: false,
1038
+ limit: maxTokens,
1039
+ remaining: 0,
1040
+ reset: reset2,
1041
+ pending: Promise.resolve()
1042
+ };
1043
+ }
817
1044
  }
1045
+ const now = Date.now();
1046
+ const incrementBy = rate ? Math.max(1, rate) : 1;
1047
+ const [remaining, reset] = await ctx.redis.eval(
1048
+ tokenBucketLimitScript,
1049
+ [identifier],
1050
+ [maxTokens, intervalDuration, refillRate, now, incrementBy]
1051
+ );
1052
+ const success = remaining >= 0;
1053
+ if (ctx.cache && !success) {
1054
+ ctx.cache.blockUntil(identifier, reset);
1055
+ }
1056
+ return {
1057
+ success,
1058
+ limit: maxTokens,
1059
+ remaining,
1060
+ reset,
1061
+ pending: Promise.resolve()
1062
+ };
1063
+ },
1064
+ async getRemaining(ctx, identifier) {
1065
+ const remainingTokens = await ctx.redis.eval(
1066
+ tokenBucketRemainingTokensScript,
1067
+ [identifier],
1068
+ [maxTokens]
1069
+ );
1070
+ return remainingTokens;
1071
+ },
1072
+ async resetTokens(ctx, identifier) {
1073
+ const pattern = identifier;
1074
+ if (ctx.cache) {
1075
+ ctx.cache.pop(identifier);
1076
+ }
1077
+ await ctx.redis.eval(resetScript, [pattern], [null]);
818
1078
  }
819
- const now = Date.now();
820
- const incrementBy = rate ? Math.max(1, rate) : 1;
821
- const [remaining, reset] = await ctx.redis.eval(
822
- tokenBucketScript,
823
- [identifier],
824
- [maxTokens, intervalDuration, refillRate, now, incrementBy]
825
- );
826
- const success = remaining >= 0;
827
- if (ctx.cache && !success) {
828
- ctx.cache.blockUntil(identifier, reset);
829
- }
830
- return {
831
- success,
832
- limit: maxTokens,
833
- remaining,
834
- reset,
835
- pending: Promise.resolve()
836
- };
837
- };
1079
+ });
838
1080
  }
839
1081
  /**
840
1082
  * cachedFixedWindow first uses the local cache to decide if a request may pass and then updates
@@ -862,44 +1104,72 @@ var RegionRatelimit = class extends Ratelimit {
862
1104
  */
863
1105
  static cachedFixedWindow(tokens, window) {
864
1106
  const windowDuration = ms(window);
865
- return async (ctx, identifier, rate) => {
866
- if (!ctx.cache) {
867
- throw new Error("This algorithm requires a cache");
868
- }
869
- const bucket = Math.floor(Date.now() / windowDuration);
870
- const key = [identifier, bucket].join(":");
871
- const reset = (bucket + 1) * windowDuration;
872
- const incrementBy = rate ? Math.max(1, rate) : 1;
873
- const hit = typeof ctx.cache.get(key) === "number";
874
- if (hit) {
875
- const cachedTokensAfterUpdate = ctx.cache.incr(key);
876
- const success = cachedTokensAfterUpdate < tokens;
877
- const pending = success ? ctx.redis.eval(cachedFixedWindowScript, [key], [windowDuration, incrementBy]).then((t) => {
878
- ctx.cache.set(key, t);
879
- }) : Promise.resolve();
1107
+ return () => ({
1108
+ async limit(ctx, identifier, rate) {
1109
+ if (!ctx.cache) {
1110
+ throw new Error("This algorithm requires a cache");
1111
+ }
1112
+ const bucket = Math.floor(Date.now() / windowDuration);
1113
+ const key = [identifier, bucket].join(":");
1114
+ const reset = (bucket + 1) * windowDuration;
1115
+ const incrementBy = rate ? Math.max(1, rate) : 1;
1116
+ const hit = typeof ctx.cache.get(key) === "number";
1117
+ if (hit) {
1118
+ const cachedTokensAfterUpdate = ctx.cache.incr(key);
1119
+ const success = cachedTokensAfterUpdate < tokens;
1120
+ const pending = success ? ctx.redis.eval(cachedFixedWindowLimitScript, [key], [windowDuration, incrementBy]).then((t) => {
1121
+ ctx.cache.set(key, t);
1122
+ }) : Promise.resolve();
1123
+ return {
1124
+ success,
1125
+ limit: tokens,
1126
+ remaining: tokens - cachedTokensAfterUpdate,
1127
+ reset,
1128
+ pending
1129
+ };
1130
+ }
1131
+ const usedTokensAfterUpdate = await ctx.redis.eval(
1132
+ cachedFixedWindowLimitScript,
1133
+ [key],
1134
+ [windowDuration, incrementBy]
1135
+ );
1136
+ ctx.cache.set(key, usedTokensAfterUpdate);
1137
+ const remaining = tokens - usedTokensAfterUpdate;
880
1138
  return {
881
- success,
1139
+ success: remaining >= 0,
882
1140
  limit: tokens,
883
- remaining: tokens - cachedTokensAfterUpdate,
1141
+ remaining,
884
1142
  reset,
885
- pending
1143
+ pending: Promise.resolve()
886
1144
  };
1145
+ },
1146
+ async getRemaining(ctx, identifier) {
1147
+ if (!ctx.cache) {
1148
+ throw new Error("This algorithm requires a cache");
1149
+ }
1150
+ const bucket = Math.floor(Date.now() / windowDuration);
1151
+ const key = [identifier, bucket].join(":");
1152
+ const hit = typeof ctx.cache.get(key) === "number";
1153
+ if (hit) {
1154
+ const cachedUsedTokens = ctx.cache.get(key) ?? 0;
1155
+ return Math.max(0, tokens - cachedUsedTokens);
1156
+ }
1157
+ const usedTokens = await ctx.redis.eval(
1158
+ cachedFixedWindowRemainingTokenScript,
1159
+ [key],
1160
+ [null]
1161
+ );
1162
+ return Math.max(0, tokens - usedTokens);
1163
+ },
1164
+ async resetTokens(ctx, identifier) {
1165
+ const pattern = [identifier, "*"].join(":");
1166
+ if (!ctx.cache) {
1167
+ throw new Error("This algorithm requires a cache");
1168
+ }
1169
+ ctx.cache.pop(identifier);
1170
+ await ctx.redis.eval(resetScript, [pattern], [null]);
887
1171
  }
888
- const usedTokensAfterUpdate = await ctx.redis.eval(
889
- cachedFixedWindowScript,
890
- [key],
891
- [windowDuration, incrementBy]
892
- );
893
- ctx.cache.set(key, usedTokensAfterUpdate);
894
- const remaining = tokens - usedTokensAfterUpdate;
895
- return {
896
- success: remaining >= 0,
897
- limit: tokens,
898
- remaining,
899
- reset,
900
- pending: Promise.resolve()
901
- };
902
- };
1172
+ });
903
1173
  }
904
1174
  };
905
1175
  export {