@upstash/ratelimit 1.0.1 → 1.0.3

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
@@ -130,7 +130,7 @@ function ms(d) {
130
130
  if (!match) {
131
131
  throw new Error(`Unable to parse window size: ${d}`);
132
132
  }
133
- const time = parseInt(match[1]);
133
+ const time = Number.parseInt(match[1]);
134
134
  const unit = match[2];
135
135
  switch (unit) {
136
136
  case "ms":
@@ -148,6 +148,59 @@ function ms(d) {
148
148
  }
149
149
  }
150
150
 
151
+ // src/lua-scripts/multi.ts
152
+ var fixedWindowScript = `
153
+ local key = KEYS[1]
154
+ local id = ARGV[1]
155
+ local window = ARGV[2]
156
+ local incrementBy = tonumber(ARGV[3])
157
+
158
+ redis.call("HSET", key, id, incrementBy)
159
+ local fields = redis.call("HGETALL", key)
160
+ if #fields == 1 and tonumber(fields[1])==incrementBy then
161
+ -- The first time this key is set, and the value will be equal to incrementBy.
162
+ -- So we only need the expire command once
163
+ redis.call("PEXPIRE", key, window)
164
+ end
165
+
166
+ return fields
167
+ `;
168
+ var slidingWindowScript = `
169
+ local currentKey = KEYS[1] -- identifier including prefixes
170
+ local previousKey = KEYS[2] -- key of the previous bucket
171
+ local tokens = tonumber(ARGV[1]) -- tokens per window
172
+ local now = ARGV[2] -- current timestamp in milliseconds
173
+ local window = ARGV[3] -- interval in milliseconds
174
+ local requestId = ARGV[4] -- uuid for this request
175
+ local incrementBy = tonumber(ARGV[5]) -- custom rate, default is 1
176
+
177
+ local currentFields = redis.call("HGETALL", currentKey)
178
+ local requestsInCurrentWindow = 0
179
+ for i = 2, #currentFields, 2 do
180
+ requestsInCurrentWindow = requestsInCurrentWindow + tonumber(currentFields[i])
181
+ end
182
+
183
+ local previousFields = redis.call("HGETALL", previousKey)
184
+ local requestsInPreviousWindow = 0
185
+ for i = 2, #previousFields, 2 do
186
+ requestsInPreviousWindow = requestsInPreviousWindow + tonumber(previousFields[i])
187
+ end
188
+
189
+ local percentageInCurrent = ( now % window) / window
190
+ if requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
191
+ return {currentFields, previousFields, false}
192
+ end
193
+
194
+ redis.call("HSET", currentKey, requestId, incrementBy)
195
+
196
+ if requestsInCurrentWindow == 0 then
197
+ -- The first time this key is set, the value will be equal to incrementBy.
198
+ -- So we only need the expire command once
199
+ redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
200
+ end
201
+ return {currentFields, previousFields, true}
202
+ `;
203
+
151
204
  // src/ratelimit.ts
152
205
  var Ratelimit = class {
153
206
  limiter;
@@ -188,12 +241,29 @@ var Ratelimit = class {
188
241
  * }
189
242
  * return "Yes"
190
243
  * ```
244
+ *
245
+ * @param req.rate - The rate at which tokens will be added or consumed from the token bucket. A higher rate allows for more requests to be processed. Defaults to 1 token per interval if not specified.
246
+ *
247
+ * Usage with `req.rate`
248
+ * @example
249
+ * ```ts
250
+ * const ratelimit = new Ratelimit({
251
+ * redis: Redis.fromEnv(),
252
+ * limiter: Ratelimit.slidingWindow(100, "10 s")
253
+ * })
254
+ *
255
+ * const { success } = await ratelimit.limit(id, {rate: 10})
256
+ * if (!success){
257
+ * return "Nope"
258
+ * }
259
+ * return "Yes"
260
+ * ```
191
261
  */
192
262
  limit = async (identifier, req) => {
193
263
  const key = [this.prefix, identifier].join(":");
194
264
  let timeoutId = null;
195
265
  try {
196
- const arr = [this.limiter(this.ctx, key)];
266
+ const arr = [this.limiter(this.ctx, key, req?.rate)];
197
267
  if (this.timeout > 0) {
198
268
  arr.push(
199
269
  new Promise((resolve) => {
@@ -325,22 +395,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
325
395
  */
326
396
  static fixedWindow(tokens, window) {
327
397
  const windowDuration = ms(window);
328
- const script = `
329
- local key = KEYS[1]
330
- local id = ARGV[1]
331
- local window = ARGV[2]
332
-
333
- redis.call("SADD", key, id)
334
- local members = redis.call("SMEMBERS", key)
335
- if #members == 1 then
336
- -- The first time this key is set, the value will be 1.
337
- -- So we only need the expire command once
338
- redis.call("PEXPIRE", key, window)
339
- end
340
-
341
- return members
342
- `;
343
- return async function(ctx, identifier) {
398
+ return async (ctx, identifier, rate) => {
344
399
  if (ctx.cache) {
345
400
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
346
401
  if (blocked) {
@@ -356,26 +411,60 @@ var MultiRegionRatelimit = class extends Ratelimit {
356
411
  const requestId = randomId();
357
412
  const bucket = Math.floor(Date.now() / windowDuration);
358
413
  const key = [identifier, bucket].join(":");
414
+ const incrementBy = rate ? Math.max(1, rate) : 1;
359
415
  const dbs = ctx.redis.map((redis) => ({
360
416
  redis,
361
- request: redis.eval(script, [key], [requestId, windowDuration])
417
+ request: redis.eval(
418
+ fixedWindowScript,
419
+ [key],
420
+ [requestId, windowDuration, incrementBy]
421
+ )
362
422
  }));
363
423
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
364
- const usedTokens = firstResponse.length;
365
- const remaining = tokens - usedTokens - 1;
424
+ const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
425
+ let parsedToken = 0;
426
+ if (index % 2) {
427
+ parsedToken = Number.parseInt(usedToken);
428
+ }
429
+ return accTokens + parsedToken;
430
+ }, 0);
431
+ const remaining = tokens - usedTokens;
366
432
  async function sync() {
367
433
  const individualIDs = await Promise.all(dbs.map((s) => s.request));
368
- const allIDs = Array.from(new Set(individualIDs.flatMap((_) => _)).values());
434
+ const allIDs = Array.from(
435
+ new Set(
436
+ individualIDs.flatMap((_) => _).reduce((acc, curr, index) => {
437
+ if (index % 2 === 0) {
438
+ acc.push(curr);
439
+ }
440
+ return acc;
441
+ }, [])
442
+ ).values()
443
+ );
369
444
  for (const db of dbs) {
370
- const ids = await db.request;
371
- if (ids.length >= tokens) {
445
+ const usedDbTokens = (await db.request).reduce((accTokens, usedToken, index) => {
446
+ let parsedToken = 0;
447
+ if (index % 2) {
448
+ parsedToken = Number.parseInt(usedToken);
449
+ }
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);
455
+ }
456
+ return ids;
457
+ }, []);
458
+ if (usedDbTokens >= tokens) {
372
459
  continue;
373
460
  }
374
- const diff = allIDs.filter((id) => !ids.includes(id));
461
+ const diff = allIDs.filter((id) => !dbIds.includes(id));
375
462
  if (diff.length === 0) {
376
463
  continue;
377
464
  }
378
- await db.redis.sadd(key, ...allIDs);
465
+ for (const requestId2 of diff) {
466
+ await db.redis.hset(key, { [requestId2]: incrementBy });
467
+ }
379
468
  }
380
469
  }
381
470
  const success = remaining > 0;
@@ -410,69 +499,76 @@ var MultiRegionRatelimit = class extends Ratelimit {
410
499
  */
411
500
  static slidingWindow(tokens, window) {
412
501
  const windowSize = ms(window);
413
- const script = `
414
- local currentKey = KEYS[1] -- identifier including prefixes
415
- local previousKey = KEYS[2] -- key of the previous bucket
416
- local tokens = tonumber(ARGV[1]) -- tokens per window
417
- local now = ARGV[2] -- current timestamp in milliseconds
418
- local window = ARGV[3] -- interval in milliseconds
419
- local requestId = ARGV[4] -- uuid for this request
420
-
421
-
422
- local currentMembers = redis.call("SMEMBERS", currentKey)
423
- local requestsInCurrentWindow = #currentMembers
424
- local previousMembers = redis.call("SMEMBERS", previousKey)
425
- local requestsInPreviousWindow = #previousMembers
426
-
427
- local percentageInCurrent = ( now % window) / window
428
- if requestsInPreviousWindow * ( 1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
429
- return {currentMembers, previousMembers, false}
430
- end
431
-
432
- redis.call("SADD", currentKey, requestId)
433
- table.insert(currentMembers, requestId)
434
- if requestsInCurrentWindow == 0 then
435
- -- The first time this key is set, the value will be 1.
436
- -- So we only need the expire command once
437
- redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
438
- end
439
- return {currentMembers, previousMembers, true}
440
- `;
441
502
  const windowDuration = ms(window);
442
- return async function(ctx, identifier) {
503
+ return async (ctx, identifier, rate) => {
443
504
  const requestId = randomId();
444
505
  const now = Date.now();
445
506
  const currentWindow = Math.floor(now / windowSize);
446
507
  const currentKey = [identifier, currentWindow].join(":");
447
508
  const previousWindow = currentWindow - 1;
448
509
  const previousKey = [identifier, previousWindow].join(":");
510
+ const incrementBy = rate ? Math.max(1, rate) : 1;
449
511
  const dbs = ctx.redis.map((redis) => ({
450
512
  redis,
451
513
  request: redis.eval(
452
- script,
514
+ slidingWindowScript,
453
515
  [currentKey, previousKey],
454
- [tokens, now, windowDuration, requestId]
516
+ [tokens, now, windowDuration, requestId, incrementBy]
455
517
  // lua seems to return `1` for true and `null` for false
456
518
  )
457
519
  }));
458
520
  const percentageInCurrent = now % windowDuration / windowDuration;
459
521
  const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
460
- const previousPartialUsed = previous.length * (1 - percentageInCurrent);
461
- const usedTokens = previousPartialUsed + current.length;
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;
462
538
  const remaining = tokens - usedTokens;
463
539
  async function sync() {
464
540
  const res = await Promise.all(dbs.map((s) => s.request));
465
- const allCurrentIds = res.flatMap(([current2]) => current2);
541
+ const allCurrentIds = res.flatMap(([current2]) => current2).reduce((accCurrentIds, curr, index) => {
542
+ if (index % 2 === 0) {
543
+ accCurrentIds.push(curr);
544
+ }
545
+ return accCurrentIds;
546
+ }, []);
466
547
  for (const db of dbs) {
467
- const [ids] = await db.request;
468
- if (ids.length >= tokens) {
548
+ const [_current, previous2, _success] = await db.request;
549
+ const dbIds = previous2.reduce((ids, currentId, index) => {
550
+ if (index % 2 === 0) {
551
+ ids.push(currentId);
552
+ }
553
+ return ids;
554
+ }, []);
555
+ const usedDbTokens = previous2.reduce((accTokens, usedToken, index) => {
556
+ let parsedToken = 0;
557
+ if (index % 2) {
558
+ parsedToken = Number.parseInt(usedToken);
559
+ }
560
+ return accTokens + parsedToken;
561
+ }, 0);
562
+ if (usedDbTokens >= tokens) {
469
563
  continue;
470
564
  }
471
- const diff = allCurrentIds.filter((id) => !ids.includes(id));
565
+ const diff = allCurrentIds.filter((id) => !dbIds.includes(id));
472
566
  if (diff.length === 0) {
473
567
  continue;
474
568
  }
475
- await db.redis.sadd(currentKey, ...diff);
569
+ for (const requestId2 of diff) {
570
+ await db.redis.hset(currentKey, { [requestId2]: incrementBy });
571
+ }
476
572
  }
477
573
  }
478
574
  const reset = (currentWindow + 1) * windowDuration;
@@ -482,7 +578,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
482
578
  return {
483
579
  success: Boolean(success),
484
580
  limit: tokens,
485
- remaining,
581
+ remaining: Math.max(0, remaining),
486
582
  reset,
487
583
  pending: sync()
488
584
  };
@@ -490,6 +586,107 @@ var MultiRegionRatelimit = class extends Ratelimit {
490
586
  }
491
587
  };
492
588
 
589
+ // src/lua-scripts/single.ts
590
+ var fixedWindowScript2 = `
591
+ local key = KEYS[1]
592
+ local window = ARGV[1]
593
+ local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
594
+
595
+ local r = redis.call("INCRBY", key, incrementBy)
596
+ if r == incrementBy then
597
+ -- The first time this key is set, the value will be equal to incrementBy.
598
+ -- So we only need the expire command once
599
+ redis.call("PEXPIRE", key, window)
600
+ end
601
+
602
+ return r
603
+ `;
604
+ var slidingWindowScript2 = `
605
+ local currentKey = KEYS[1] -- identifier including prefixes
606
+ local previousKey = KEYS[2] -- key of the previous bucket
607
+ local tokens = tonumber(ARGV[1]) -- tokens per window
608
+ local now = ARGV[2] -- current timestamp in milliseconds
609
+ local window = ARGV[3] -- interval in milliseconds
610
+ local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
611
+
612
+ local requestsInCurrentWindow = redis.call("GET", currentKey)
613
+ if requestsInCurrentWindow == false then
614
+ requestsInCurrentWindow = 0
615
+ end
616
+
617
+ local requestsInPreviousWindow = redis.call("GET", previousKey)
618
+ if requestsInPreviousWindow == false then
619
+ requestsInPreviousWindow = 0
620
+ end
621
+ local percentageInCurrent = ( now % window ) / window
622
+ -- weighted requests to consider from the previous window
623
+ requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
624
+ if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
625
+ return -1
626
+ end
627
+
628
+ local newValue = redis.call("INCRBY", currentKey, incrementBy)
629
+ if newValue == incrementBy then
630
+ -- The first time this key is set, the value will be equal to incrementBy.
631
+ -- So we only need the expire command once
632
+ redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
633
+ end
634
+ return tokens - ( newValue + requestsInPreviousWindow )
635
+ `;
636
+ var tokenBucketScript = `
637
+ local key = KEYS[1] -- identifier including prefixes
638
+ local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
639
+ local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
640
+ local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
641
+ local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
642
+ local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
643
+
644
+ local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
645
+
646
+ local refilledAt
647
+ local tokens
648
+
649
+ if bucket[1] == false then
650
+ refilledAt = now
651
+ tokens = maxTokens
652
+ else
653
+ refilledAt = tonumber(bucket[1])
654
+ tokens = tonumber(bucket[2])
655
+ end
656
+
657
+ if now >= refilledAt + interval then
658
+ local numRefills = math.floor((now - refilledAt) / interval)
659
+ tokens = math.min(maxTokens, tokens + numRefills * refillRate)
660
+
661
+ refilledAt = refilledAt + numRefills * interval
662
+ end
663
+
664
+ if tokens == 0 then
665
+ return {-1, refilledAt + interval}
666
+ end
667
+
668
+ local remaining = tokens - incrementBy
669
+ local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
670
+
671
+ redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
672
+ redis.call("PEXPIRE", key, expireAt)
673
+ return {remaining, refilledAt + interval}
674
+ `;
675
+ var cachedFixedWindowScript = `
676
+ local key = KEYS[1]
677
+ local window = ARGV[1]
678
+ local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
679
+
680
+ local r = redis.call("INCRBY", key, incrementBy)
681
+ if r == incrementBy then
682
+ -- The first time this key is set, the value will be equal to incrementBy.
683
+ -- So we only need the expire command once
684
+ redis.call("PEXPIRE", key, window)
685
+ end
686
+
687
+ return r
688
+ `;
689
+
493
690
  // src/single.ts
494
691
  var RegionRatelimit = class extends Ratelimit {
495
692
  /**
@@ -527,20 +724,7 @@ var RegionRatelimit = class extends Ratelimit {
527
724
  */
528
725
  static fixedWindow(tokens, window) {
529
726
  const windowDuration = ms(window);
530
- const script = `
531
- local key = KEYS[1]
532
- local window = ARGV[1]
533
-
534
- local r = redis.call("INCR", key)
535
- if r == 1 then
536
- -- The first time this key is set, the value will be 1.
537
- -- So we only need the expire command once
538
- redis.call("PEXPIRE", key, window)
539
- end
540
-
541
- return r
542
- `;
543
- return async function(ctx, identifier) {
727
+ return async (ctx, identifier, rate) => {
544
728
  const bucket = Math.floor(Date.now() / windowDuration);
545
729
  const key = [identifier, bucket].join(":");
546
730
  if (ctx.cache) {
@@ -555,12 +739,14 @@ var RegionRatelimit = class extends Ratelimit {
555
739
  };
556
740
  }
557
741
  }
742
+ const incrementBy = rate ? Math.max(1, rate) : 1;
558
743
  const usedTokensAfterUpdate = await ctx.redis.eval(
559
- script,
744
+ fixedWindowScript2,
560
745
  [key],
561
- [windowDuration]
746
+ [windowDuration, incrementBy]
562
747
  );
563
748
  const success = usedTokensAfterUpdate <= tokens;
749
+ const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
564
750
  const reset = (bucket + 1) * windowDuration;
565
751
  if (ctx.cache && !success) {
566
752
  ctx.cache.blockUntil(identifier, reset);
@@ -568,7 +754,7 @@ var RegionRatelimit = class extends Ratelimit {
568
754
  return {
569
755
  success,
570
756
  limit: tokens,
571
- remaining: Math.max(0, tokens - usedTokensAfterUpdate),
757
+ remaining: remainingTokens,
572
758
  reset,
573
759
  pending: Promise.resolve()
574
760
  };
@@ -591,39 +777,8 @@ var RegionRatelimit = class extends Ratelimit {
591
777
  * @param window - The duration in which the user can max X requests.
592
778
  */
593
779
  static slidingWindow(tokens, window) {
594
- const script = `
595
- local currentKey = KEYS[1] -- identifier including prefixes
596
- local previousKey = KEYS[2] -- key of the previous bucket
597
- local tokens = tonumber(ARGV[1]) -- tokens per window
598
- local now = ARGV[2] -- current timestamp in milliseconds
599
- local window = ARGV[3] -- interval in milliseconds
600
-
601
- local requestsInCurrentWindow = redis.call("GET", currentKey)
602
- if requestsInCurrentWindow == false then
603
- requestsInCurrentWindow = 0
604
- end
605
-
606
- local requestsInPreviousWindow = redis.call("GET", previousKey)
607
- if requestsInPreviousWindow == false then
608
- requestsInPreviousWindow = 0
609
- end
610
- local percentageInCurrent = ( now % window ) / window
611
- -- weighted requests to consider from the previous window
612
- requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
613
- if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
614
- return -1
615
- end
616
-
617
- local newValue = redis.call("INCR", currentKey)
618
- if newValue == 1 then
619
- -- The first time this key is set, the value will be 1.
620
- -- So we only need the expire command once
621
- redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
622
- end
623
- return tokens - ( newValue + requestsInPreviousWindow )
624
- `;
625
780
  const windowSize = ms(window);
626
- return async function(ctx, identifier) {
781
+ return async (ctx, identifier, rate) => {
627
782
  const now = Date.now();
628
783
  const currentWindow = Math.floor(now / windowSize);
629
784
  const currentKey = [identifier, currentWindow].join(":");
@@ -641,12 +796,13 @@ var RegionRatelimit = class extends Ratelimit {
641
796
  };
642
797
  }
643
798
  }
644
- const remaining = await ctx.redis.eval(
645
- script,
799
+ const incrementBy = rate ? Math.max(1, rate) : 1;
800
+ const remainingTokens = await ctx.redis.eval(
801
+ slidingWindowScript2,
646
802
  [currentKey, previousKey],
647
- [tokens, now, windowSize]
803
+ [tokens, now, windowSize, incrementBy]
648
804
  );
649
- const success = remaining >= 0;
805
+ const success = remainingTokens >= 0;
650
806
  const reset = (currentWindow + 1) * windowSize;
651
807
  if (ctx.cache && !success) {
652
808
  ctx.cache.blockUntil(identifier, reset);
@@ -654,7 +810,7 @@ var RegionRatelimit = class extends Ratelimit {
654
810
  return {
655
811
  success,
656
812
  limit: tokens,
657
- remaining: Math.max(0, remaining),
813
+ remaining: Math.max(0, remainingTokens),
658
814
  reset,
659
815
  pending: Promise.resolve()
660
816
  };
@@ -674,46 +830,8 @@ var RegionRatelimit = class extends Ratelimit {
674
830
  * than `refillRate`
675
831
  */
676
832
  static tokenBucket(refillRate, interval, maxTokens) {
677
- const script = `
678
- local key = KEYS[1] -- identifier including prefixes
679
- local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
680
- local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
681
- local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
682
- local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
683
-
684
- local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
685
-
686
- local refilledAt
687
- local tokens
688
-
689
- if bucket[1] == false then
690
- refilledAt = now
691
- tokens = maxTokens
692
- else
693
- refilledAt = tonumber(bucket[1])
694
- tokens = tonumber(bucket[2])
695
- end
696
-
697
- if now >= refilledAt + interval then
698
- local numRefills = math.floor((now - refilledAt) / interval)
699
- tokens = math.min(maxTokens, tokens + numRefills * refillRate)
700
-
701
- refilledAt = refilledAt + numRefills * interval
702
- end
703
-
704
- if tokens == 0 then
705
- return {-1, refilledAt + interval}
706
- end
707
-
708
- local remaining = tokens - 1
709
- local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
710
-
711
- redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
712
- redis.call("PEXPIRE", key, expireAt)
713
- return {remaining, refilledAt + interval}
714
- `;
715
833
  const intervalDuration = ms(interval);
716
- return async function(ctx, identifier) {
834
+ return async (ctx, identifier, rate) => {
717
835
  if (ctx.cache) {
718
836
  const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
719
837
  if (blocked) {
@@ -727,10 +845,11 @@ var RegionRatelimit = class extends Ratelimit {
727
845
  }
728
846
  }
729
847
  const now = Date.now();
848
+ const incrementBy = rate ? Math.max(1, rate) : 1;
730
849
  const [remaining, reset] = await ctx.redis.eval(
731
- script,
850
+ tokenBucketScript,
732
851
  [identifier],
733
- [maxTokens, intervalDuration, refillRate, now]
852
+ [maxTokens, intervalDuration, refillRate, now, incrementBy]
734
853
  );
735
854
  const success = remaining >= 0;
736
855
  if (ctx.cache && !success) {
@@ -771,31 +890,19 @@ var RegionRatelimit = class extends Ratelimit {
771
890
  */
772
891
  static cachedFixedWindow(tokens, window) {
773
892
  const windowDuration = ms(window);
774
- const script = `
775
- local key = KEYS[1]
776
- local window = ARGV[1]
777
-
778
- local r = redis.call("INCR", key)
779
- if r == 1 then
780
- -- The first time this key is set, the value will be 1.
781
- -- So we only need the expire command once
782
- redis.call("PEXPIRE", key, window)
783
- end
784
-
785
- return r
786
- `;
787
- return async function(ctx, identifier) {
893
+ return async (ctx, identifier, rate) => {
788
894
  if (!ctx.cache) {
789
895
  throw new Error("This algorithm requires a cache");
790
896
  }
791
897
  const bucket = Math.floor(Date.now() / windowDuration);
792
898
  const key = [identifier, bucket].join(":");
793
899
  const reset = (bucket + 1) * windowDuration;
900
+ const incrementBy = rate ? Math.max(1, rate) : 1;
794
901
  const hit = typeof ctx.cache.get(key) === "number";
795
902
  if (hit) {
796
903
  const cachedTokensAfterUpdate = ctx.cache.incr(key);
797
904
  const success = cachedTokensAfterUpdate < tokens;
798
- const pending = success ? ctx.redis.eval(script, [key], [windowDuration]).then((t) => {
905
+ const pending = success ? ctx.redis.eval(cachedFixedWindowScript, [key], [windowDuration, incrementBy]).then((t) => {
799
906
  ctx.cache.set(key, t);
800
907
  }) : Promise.resolve();
801
908
  return {
@@ -807,9 +914,9 @@ var RegionRatelimit = class extends Ratelimit {
807
914
  };
808
915
  }
809
916
  const usedTokensAfterUpdate = await ctx.redis.eval(
810
- script,
917
+ cachedFixedWindowScript,
811
918
  [key],
812
- [windowDuration]
919
+ [windowDuration, incrementBy]
813
920
  );
814
921
  ctx.cache.set(key, usedTokensAfterUpdate);
815
922
  const remaining = tokens - usedTokensAfterUpdate;