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