@upstash/ratelimit 1.1.2 → 1.2.0-canary

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
@@ -125,6 +125,9 @@ var Cache = class {
125
125
  empty() {
126
126
  this.cache.clear();
127
127
  }
128
+ size() {
129
+ return this.cache.size;
130
+ }
128
131
  };
129
132
 
130
133
  // src/duration.ts
@@ -151,6 +154,37 @@ function ms(d) {
151
154
  }
152
155
  }
153
156
 
157
+ // src/hash.ts
158
+ var setHash = async (ctx, script, kind) => {
159
+ const regionContexts = "redis" in ctx ? [ctx] : ctx.regionContexts;
160
+ const hashSample = regionContexts[0].scriptHashes[kind];
161
+ if (!hashSample) {
162
+ await Promise.all(regionContexts.map(async (context) => {
163
+ context.scriptHashes[kind] = await context.redis.scriptLoad(script);
164
+ }));
165
+ }
166
+ ;
167
+ };
168
+ var safeEval = async (ctx, script, kind, keys, args) => {
169
+ if (!ctx.cacheScripts) {
170
+ return await ctx.redis.eval(script, keys, args);
171
+ }
172
+ ;
173
+ await setHash(ctx, script, kind);
174
+ try {
175
+ return await ctx.redis.evalsha(ctx.scriptHashes[kind], keys, args);
176
+ } catch (error) {
177
+ if (`${error}`.includes("NOSCRIPT")) {
178
+ console.log("Script with the expected hash was not found in redis db. It is probably flushed. Will load another scipt before continuing.");
179
+ ctx.scriptHashes[kind] = void 0;
180
+ await setHash(ctx, script, kind);
181
+ console.log(" New script successfully loaded.");
182
+ return await ctx.redis.evalsha(ctx.scriptHashes[kind], keys, args);
183
+ }
184
+ throw error;
185
+ }
186
+ };
187
+
154
188
  // src/lua-scripts/multi.ts
155
189
  var fixedWindowLimitScript = `
156
190
  local key = KEYS[1]
@@ -260,20 +294,71 @@ var resetScript = `
260
294
  until cursor == "0"
261
295
  `;
262
296
 
297
+ // src/deny-list.ts
298
+ var denyListCache = new Cache(/* @__PURE__ */ new Map());
299
+ var checkDenyListCache = (members) => {
300
+ return members.find(
301
+ (member) => denyListCache.isBlocked(member).blocked
302
+ );
303
+ };
304
+ var blockMember = (member) => {
305
+ if (denyListCache.size() > 1e3)
306
+ denyListCache.empty();
307
+ denyListCache.blockUntil(member, Date.now() + 6e4);
308
+ };
309
+ var checkDenyList = async (redis, prefix, members) => {
310
+ const deniedMembers = await redis.smismember(
311
+ [prefix, "denyList", "all"].join(":"),
312
+ members
313
+ );
314
+ let deniedMember = void 0;
315
+ deniedMembers.map((memberDenied, index) => {
316
+ if (memberDenied) {
317
+ blockMember(members[index]);
318
+ deniedMember = members[index];
319
+ }
320
+ });
321
+ return deniedMember;
322
+ };
323
+ var resolveResponses = ([ratelimitResponse, denyListResponse]) => {
324
+ if (denyListResponse) {
325
+ ratelimitResponse.success = false;
326
+ ratelimitResponse.remaining = 0;
327
+ ratelimitResponse.reason = "denyList";
328
+ ratelimitResponse.deniedValue = denyListResponse;
329
+ }
330
+ return ratelimitResponse;
331
+ };
332
+ var defaultDeniedResponse = (deniedValue) => {
333
+ return {
334
+ success: false,
335
+ limit: 0,
336
+ remaining: 0,
337
+ reset: 0,
338
+ pending: Promise.resolve(),
339
+ reason: "denyList",
340
+ deniedValue
341
+ };
342
+ };
343
+
263
344
  // src/ratelimit.ts
264
345
  var Ratelimit = class {
265
346
  limiter;
266
347
  ctx;
267
348
  prefix;
268
349
  timeout;
350
+ primaryRedis;
269
351
  analytics;
352
+ enableProtection;
270
353
  constructor(config) {
271
354
  this.ctx = config.ctx;
272
355
  this.limiter = config.limiter;
273
356
  this.timeout = config.timeout ?? 5e3;
274
357
  this.prefix = config.prefix ?? "@upstash/ratelimit";
358
+ this.enableProtection = config.enableProtection ?? false;
359
+ this.primaryRedis = "redis" in this.ctx ? this.ctx.redis : this.ctx.regionContexts[0].redis;
275
360
  this.analytics = config.analytics ? new Analytics({
276
- redis: Array.isArray(this.ctx.redis) ? this.ctx.redis[0] : this.ctx.redis,
361
+ redis: this.primaryRedis,
277
362
  prefix: this.prefix
278
363
  }) : void 0;
279
364
  if (config.ephemeralCache instanceof Map) {
@@ -319,43 +404,14 @@ var Ratelimit = class {
319
404
  * ```
320
405
  */
321
406
  limit = async (identifier, req) => {
322
- const key = [this.prefix, identifier].join(":");
323
407
  let timeoutId = null;
324
408
  try {
325
- const arr = [this.limiter().limit(this.ctx, key, req?.rate)];
326
- if (this.timeout > 0) {
327
- arr.push(
328
- new Promise((resolve) => {
329
- timeoutId = setTimeout(() => {
330
- resolve({
331
- success: true,
332
- limit: 0,
333
- remaining: 0,
334
- reset: 0,
335
- pending: Promise.resolve()
336
- });
337
- }, this.timeout);
338
- })
339
- );
340
- }
341
- const res = await Promise.race(arr);
342
- if (this.analytics) {
343
- try {
344
- const geo = req ? this.analytics.extractGeo(req) : void 0;
345
- const analyticsP = this.analytics.record({
346
- identifier,
347
- time: Date.now(),
348
- success: res.success,
349
- ...geo
350
- }).catch((err) => {
351
- console.warn("Failed to record analytics", err);
352
- });
353
- res.pending = Promise.all([res.pending, analyticsP]);
354
- } catch (err) {
355
- console.warn("Failed to record analytics", err);
356
- }
357
- }
358
- return res;
409
+ const response = this.getRatelimitResponse(identifier, req);
410
+ const { responseArray, newTimeoutId } = this.applyTimeout(response);
411
+ timeoutId = newTimeoutId;
412
+ const timedResponse = await Promise.race(responseArray);
413
+ const finalResponse = this.submitAnalytics(timedResponse, identifier, req);
414
+ return finalResponse;
359
415
  } finally {
360
416
  if (timeoutId) {
361
417
  clearTimeout(timeoutId);
@@ -414,6 +470,125 @@ var Ratelimit = class {
414
470
  const pattern = [this.prefix, identifier].join(":");
415
471
  return await this.limiter().getRemaining(this.ctx, pattern);
416
472
  };
473
+ /**
474
+ * Checks if the identifier or the values in req are in the deny list cache.
475
+ * If so, returns the default denied response.
476
+ *
477
+ * Otherwise, calls redis to check the rate limit and deny list. Returns after
478
+ * resolving the result. Resolving is overriding the rate limit result if
479
+ * the some value is in deny list.
480
+ *
481
+ * @param identifier identifier to block
482
+ * @param req options with ip, user agent, country, rate and geo info
483
+ * @returns rate limit response
484
+ */
485
+ getRatelimitResponse = async (identifier, req) => {
486
+ const key = this.getKey(identifier);
487
+ const definedMembers = this.getDefinedMembers(identifier, req);
488
+ const deniedMember = checkDenyListCache(definedMembers);
489
+ let result;
490
+ if (deniedMember) {
491
+ result = [defaultDeniedResponse(deniedMember), deniedMember];
492
+ } else {
493
+ result = await Promise.all([
494
+ this.limiter().limit(this.ctx, key, req?.rate),
495
+ checkDenyList(
496
+ this.primaryRedis,
497
+ this.prefix,
498
+ definedMembers
499
+ )
500
+ ]);
501
+ }
502
+ return resolveResponses(result);
503
+ };
504
+ /**
505
+ * Creates an array with the original response promise and a timeout promise
506
+ * if this.timeout > 0.
507
+ *
508
+ * @param response Ratelimit response promise
509
+ * @returns array with the response and timeout promise. also includes the timeout id
510
+ */
511
+ applyTimeout = (response) => {
512
+ let newTimeoutId = null;
513
+ const responseArray = [response];
514
+ if (this.timeout > 0) {
515
+ const timeoutResponse = new Promise((resolve) => {
516
+ newTimeoutId = setTimeout(() => {
517
+ resolve({
518
+ success: true,
519
+ limit: 0,
520
+ remaining: 0,
521
+ reset: 0,
522
+ pending: Promise.resolve(),
523
+ reason: "timeout"
524
+ });
525
+ }, this.timeout);
526
+ });
527
+ responseArray.push(timeoutResponse);
528
+ }
529
+ return {
530
+ responseArray,
531
+ newTimeoutId
532
+ };
533
+ };
534
+ /**
535
+ * submits analytics if this.analytics is set
536
+ *
537
+ * @param ratelimitResponse final rate limit response
538
+ * @param identifier identifier to submit
539
+ * @param req limit options
540
+ * @returns rate limit response after updating the .pending field
541
+ */
542
+ submitAnalytics = (ratelimitResponse, identifier, req) => {
543
+ if (this.analytics) {
544
+ try {
545
+ const geo = req ? this.analytics.extractGeo(req) : void 0;
546
+ const analyticsP = this.analytics.record({
547
+ identifier: ratelimitResponse.reason === "denyList" ? ratelimitResponse.deniedValue : identifier,
548
+ time: Date.now(),
549
+ success: ratelimitResponse.reason === "denyList" ? "denied" : ratelimitResponse.success,
550
+ ...geo
551
+ }).catch((err) => {
552
+ let errorMessage = "Failed to record analytics";
553
+ if (`${err}`.includes("WRONGTYPE")) {
554
+ errorMessage = `
555
+ Failed to record analytics. See the information below:
556
+
557
+ This can occur when you uprade to Ratelimit version 1.1.2
558
+ or later from an earlier version.
559
+
560
+ This occurs simply because the way we store analytics data
561
+ has changed. To avoid getting this error, disable analytics
562
+ for *an hour*, then simply enable it back.
563
+
564
+ `;
565
+ }
566
+ console.warn(errorMessage, err);
567
+ });
568
+ ratelimitResponse.pending = Promise.all([ratelimitResponse.pending, analyticsP]);
569
+ } catch (err) {
570
+ console.warn("Failed to record analytics", err);
571
+ }
572
+ ;
573
+ }
574
+ ;
575
+ return ratelimitResponse;
576
+ };
577
+ getKey = (identifier) => {
578
+ return [this.prefix, identifier].join(":");
579
+ };
580
+ /**
581
+ * returns a list of defined values from
582
+ * [identifier, req.ip, req.userAgent, req.country]
583
+ *
584
+ * @param identifier identifier
585
+ * @param req limit options
586
+ * @returns list of defined values
587
+ */
588
+ getDefinedMembers = (identifier, req) => {
589
+ const members = [identifier, req?.ip, req?.userAgent, req?.country];
590
+ return members.filter((item) => Boolean(item));
591
+ };
417
592
  };
418
593
 
419
594
  // src/multi.ts
@@ -437,7 +612,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
437
612
  timeout: config.timeout,
438
613
  analytics: config.analytics,
439
614
  ctx: {
440
- redis: config.redis,
615
+ regionContexts: config.redis.map((redis) => ({
616
+ redis,
617
+ scriptHashes: {},
618
+ cacheScripts: config.cacheScripts ?? true
619
+ })),
441
620
  cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
442
621
  }
443
622
  });
@@ -472,7 +651,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
472
651
  limit: tokens,
473
652
  remaining: 0,
474
653
  reset: reset2,
475
- pending: Promise.resolve()
654
+ pending: Promise.resolve(),
655
+ reason: "cacheBlock"
476
656
  };
477
657
  }
478
658
  }
@@ -480,10 +660,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
480
660
  const bucket = Math.floor(Date.now() / windowDuration);
481
661
  const key = [identifier, bucket].join(":");
482
662
  const incrementBy = rate ? Math.max(1, rate) : 1;
483
- const dbs = ctx.redis.map((redis) => ({
484
- redis,
485
- request: redis.eval(
663
+ const dbs = ctx.regionContexts.map((regionContext) => ({
664
+ redis: regionContext.redis,
665
+ request: safeEval(
666
+ regionContext,
486
667
  fixedWindowLimitScript,
668
+ "limitHash",
487
669
  [key],
488
670
  [requestId, windowDuration, incrementBy]
489
671
  )
@@ -554,9 +736,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
554
736
  async getRemaining(ctx, identifier) {
555
737
  const bucket = Math.floor(Date.now() / windowDuration);
556
738
  const key = [identifier, bucket].join(":");
557
- const dbs = ctx.redis.map((redis) => ({
558
- redis,
559
- request: redis.eval(fixedWindowRemainingTokensScript, [key], [null])
739
+ const dbs = ctx.regionContexts.map((regionContext) => ({
740
+ redis: regionContext.redis,
741
+ request: safeEval(
742
+ regionContext,
743
+ fixedWindowRemainingTokensScript,
744
+ "getRemainingHash",
745
+ [key],
746
+ [null]
747
+ )
560
748
  }));
561
749
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
562
750
  const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
@@ -573,9 +761,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
573
761
  if (ctx.cache) {
574
762
  ctx.cache.pop(identifier);
575
763
  }
576
- for (const db of ctx.redis) {
577
- await db.eval(resetScript, [pattern], [null]);
578
- }
764
+ await Promise.all(ctx.regionContexts.map((regionContext) => {
765
+ safeEval(
766
+ regionContext,
767
+ resetScript,
768
+ "resetHash",
769
+ [pattern],
770
+ [null]
771
+ );
772
+ }));
579
773
  }
580
774
  });
581
775
  }
@@ -600,6 +794,19 @@ var MultiRegionRatelimit = class extends Ratelimit {
600
794
  const windowDuration = ms(window);
601
795
  return () => ({
602
796
  async limit(ctx, identifier, rate) {
797
+ if (ctx.cache) {
798
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
799
+ if (blocked) {
800
+ return {
801
+ success: false,
802
+ limit: tokens,
803
+ remaining: 0,
804
+ reset: reset2,
805
+ pending: Promise.resolve(),
806
+ reason: "cacheBlock"
807
+ };
808
+ }
809
+ }
603
810
  const requestId = randomId();
604
811
  const now = Date.now();
605
812
  const currentWindow = Math.floor(now / windowSize);
@@ -607,10 +814,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
607
814
  const previousWindow = currentWindow - 1;
608
815
  const previousKey = [identifier, previousWindow].join(":");
609
816
  const incrementBy = rate ? Math.max(1, rate) : 1;
610
- const dbs = ctx.redis.map((redis) => ({
611
- redis,
612
- request: redis.eval(
817
+ const dbs = ctx.regionContexts.map((regionContext) => ({
818
+ redis: regionContext.redis,
819
+ request: safeEval(
820
+ regionContext,
613
821
  slidingWindowLimitScript,
822
+ "limitHash",
614
823
  [currentKey, previousKey],
615
824
  [tokens, now, windowDuration, requestId, incrementBy]
616
825
  // lua seems to return `1` for true and `null` for false
@@ -695,10 +904,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
695
904
  const currentKey = [identifier, currentWindow].join(":");
696
905
  const previousWindow = currentWindow - 1;
697
906
  const previousKey = [identifier, previousWindow].join(":");
698
- const dbs = ctx.redis.map((redis) => ({
699
- redis,
700
- request: redis.eval(
907
+ const dbs = ctx.regionContexts.map((regionContext) => ({
908
+ redis: regionContext.redis,
909
+ request: safeEval(
910
+ regionContext,
701
911
  slidingWindowRemainingTokensScript,
912
+ "getRemainingHash",
702
913
  [currentKey, previousKey],
703
914
  [now, windowSize]
704
915
  // lua seems to return `1` for true and `null` for false
@@ -712,9 +923,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
712
923
  if (ctx.cache) {
713
924
  ctx.cache.pop(identifier);
714
925
  }
715
- for (const db of ctx.redis) {
716
- await db.eval(resetScript, [pattern], [null]);
717
- }
926
+ await Promise.all(ctx.regionContexts.map((regionContext) => {
927
+ safeEval(
928
+ regionContext,
929
+ resetScript,
930
+ "resetHash",
931
+ [pattern],
932
+ [null]
933
+ );
934
+ }));
718
935
  }
719
936
  });
720
937
  }
@@ -887,9 +1104,12 @@ var RegionRatelimit = class extends Ratelimit {
887
1104
  timeout: config.timeout,
888
1105
  analytics: config.analytics,
889
1106
  ctx: {
890
- redis: config.redis
1107
+ redis: config.redis,
1108
+ scriptHashes: {},
1109
+ cacheScripts: config.cacheScripts ?? true
891
1110
  },
892
- ephemeralCache: config.ephemeralCache
1111
+ ephemeralCache: config.ephemeralCache,
1112
+ enableProtection: config.enableProtection
893
1113
  });
894
1114
  }
895
1115
  /**
@@ -924,13 +1144,16 @@ var RegionRatelimit = class extends Ratelimit {
924
1144
  limit: tokens,
925
1145
  remaining: 0,
926
1146
  reset: reset2,
927
- pending: Promise.resolve()
1147
+ pending: Promise.resolve(),
1148
+ reason: "cacheBlock"
928
1149
  };
929
1150
  }
930
1151
  }
931
1152
  const incrementBy = rate ? Math.max(1, rate) : 1;
932
- const usedTokensAfterUpdate = await ctx.redis.eval(
1153
+ const usedTokensAfterUpdate = await safeEval(
1154
+ ctx,
933
1155
  fixedWindowLimitScript2,
1156
+ "limitHash",
934
1157
  [key],
935
1158
  [windowDuration, incrementBy]
936
1159
  );
@@ -951,8 +1174,10 @@ var RegionRatelimit = class extends Ratelimit {
951
1174
  async getRemaining(ctx, identifier) {
952
1175
  const bucket = Math.floor(Date.now() / windowDuration);
953
1176
  const key = [identifier, bucket].join(":");
954
- const usedTokens = await ctx.redis.eval(
1177
+ const usedTokens = await safeEval(
1178
+ ctx,
955
1179
  fixedWindowRemainingTokensScript2,
1180
+ "getRemainingHash",
956
1181
  [key],
957
1182
  [null]
958
1183
  );
@@ -963,7 +1188,13 @@ var RegionRatelimit = class extends Ratelimit {
963
1188
  if (ctx.cache) {
964
1189
  ctx.cache.pop(identifier);
965
1190
  }
966
- await ctx.redis.eval(resetScript, [pattern], [null]);
1191
+ await safeEval(
1192
+ ctx,
1193
+ resetScript,
1194
+ "resetHash",
1195
+ [pattern],
1196
+ [null]
1197
+ );
967
1198
  }
968
1199
  });
969
1200
  }
@@ -1000,13 +1231,16 @@ var RegionRatelimit = class extends Ratelimit {
1000
1231
  limit: tokens,
1001
1232
  remaining: 0,
1002
1233
  reset: reset2,
1003
- pending: Promise.resolve()
1234
+ pending: Promise.resolve(),
1235
+ reason: "cacheBlock"
1004
1236
  };
1005
1237
  }
1006
1238
  }
1007
1239
  const incrementBy = rate ? Math.max(1, rate) : 1;
1008
- const remainingTokens = await ctx.redis.eval(
1240
+ const remainingTokens = await safeEval(
1241
+ ctx,
1009
1242
  slidingWindowLimitScript2,
1243
+ "limitHash",
1010
1244
  [currentKey, previousKey],
1011
1245
  [tokens, now, windowSize, incrementBy]
1012
1246
  );
@@ -1029,8 +1263,10 @@ var RegionRatelimit = class extends Ratelimit {
1029
1263
  const currentKey = [identifier, currentWindow].join(":");
1030
1264
  const previousWindow = currentWindow - 1;
1031
1265
  const previousKey = [identifier, previousWindow].join(":");
1032
- const usedTokens = await ctx.redis.eval(
1266
+ const usedTokens = await safeEval(
1267
+ ctx,
1033
1268
  slidingWindowRemainingTokensScript2,
1269
+ "getRemainingHash",
1034
1270
  [currentKey, previousKey],
1035
1271
  [now, windowSize]
1036
1272
  );
@@ -1041,7 +1277,13 @@ var RegionRatelimit = class extends Ratelimit {
1041
1277
  if (ctx.cache) {
1042
1278
  ctx.cache.pop(identifier);
1043
1279
  }
1044
- await ctx.redis.eval(resetScript, [pattern], [null]);
1280
+ await safeEval(
1281
+ ctx,
1282
+ resetScript,
1283
+ "resetHash",
1284
+ [pattern],
1285
+ [null]
1286
+ );
1045
1287
  }
1046
1288
  });
1047
1289
  }
@@ -1070,14 +1312,17 @@ var RegionRatelimit = class extends Ratelimit {
1070
1312
  limit: maxTokens,
1071
1313
  remaining: 0,
1072
1314
  reset: reset2,
1073
- pending: Promise.resolve()
1315
+ pending: Promise.resolve(),
1316
+ reason: "cacheBlock"
1074
1317
  };
1075
1318
  }
1076
1319
  }
1077
1320
  const now = Date.now();
1078
1321
  const incrementBy = rate ? Math.max(1, rate) : 1;
1079
- const [remaining, reset] = await ctx.redis.eval(
1322
+ const [remaining, reset] = await safeEval(
1323
+ ctx,
1080
1324
  tokenBucketLimitScript,
1325
+ "limitHash",
1081
1326
  [identifier],
1082
1327
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1083
1328
  );
@@ -1094,8 +1339,10 @@ var RegionRatelimit = class extends Ratelimit {
1094
1339
  };
1095
1340
  },
1096
1341
  async getRemaining(ctx, identifier) {
1097
- const remainingTokens = await ctx.redis.eval(
1342
+ const remainingTokens = await safeEval(
1343
+ ctx,
1098
1344
  tokenBucketRemainingTokensScript,
1345
+ "getRemainingHash",
1099
1346
  [identifier],
1100
1347
  [maxTokens]
1101
1348
  );
@@ -1106,7 +1353,13 @@ var RegionRatelimit = class extends Ratelimit {
1106
1353
  if (ctx.cache) {
1107
1354
  ctx.cache.pop(identifier);
1108
1355
  }
1109
- await ctx.redis.eval(resetScript, [pattern], [null]);
1356
+ await safeEval(
1357
+ ctx,
1358
+ resetScript,
1359
+ "resetHash",
1360
+ [pattern],
1361
+ [null]
1362
+ );
1110
1363
  }
1111
1364
  });
1112
1365
  }
@@ -1149,9 +1402,13 @@ var RegionRatelimit = class extends Ratelimit {
1149
1402
  if (hit) {
1150
1403
  const cachedTokensAfterUpdate = ctx.cache.incr(key);
1151
1404
  const success = cachedTokensAfterUpdate < tokens;
1152
- const pending = success ? ctx.redis.eval(cachedFixedWindowLimitScript, [key], [windowDuration, incrementBy]).then((t) => {
1153
- ctx.cache.set(key, t);
1154
- }) : Promise.resolve();
1405
+ const pending = success ? safeEval(
1406
+ ctx,
1407
+ cachedFixedWindowLimitScript,
1408
+ "limitHash",
1409
+ [key],
1410
+ [windowDuration, incrementBy]
1411
+ ) : Promise.resolve();
1155
1412
  return {
1156
1413
  success,
1157
1414
  limit: tokens,
@@ -1160,8 +1417,10 @@ var RegionRatelimit = class extends Ratelimit {
1160
1417
  pending
1161
1418
  };
1162
1419
  }
1163
- const usedTokensAfterUpdate = await ctx.redis.eval(
1420
+ const usedTokensAfterUpdate = await safeEval(
1421
+ ctx,
1164
1422
  cachedFixedWindowLimitScript,
1423
+ "limitHash",
1165
1424
  [key],
1166
1425
  [windowDuration, incrementBy]
1167
1426
  );
@@ -1186,20 +1445,30 @@ var RegionRatelimit = class extends Ratelimit {
1186
1445
  const cachedUsedTokens = ctx.cache.get(key) ?? 0;
1187
1446
  return Math.max(0, tokens - cachedUsedTokens);
1188
1447
  }
1189
- const usedTokens = await ctx.redis.eval(
1448
+ const usedTokens = await safeEval(
1449
+ ctx,
1190
1450
  cachedFixedWindowRemainingTokenScript,
1451
+ "getRemainingHash",
1191
1452
  [key],
1192
1453
  [null]
1193
1454
  );
1194
1455
  return Math.max(0, tokens - usedTokens);
1195
1456
  },
1196
1457
  async resetTokens(ctx, identifier) {
1197
- const pattern = [identifier, "*"].join(":");
1198
1458
  if (!ctx.cache) {
1199
1459
  throw new Error("This algorithm requires a cache");
1200
1460
  }
1201
- ctx.cache.pop(identifier);
1202
- await ctx.redis.eval(resetScript, [pattern], [null]);
1461
+ const bucket = Math.floor(Date.now() / windowDuration);
1462
+ const key = [identifier, bucket].join(":");
1463
+ ctx.cache.pop(key);
1464
+ const pattern = [identifier, "*"].join(":");
1465
+ await safeEval(
1466
+ ctx,
1467
+ resetScript,
1468
+ "resetHash",
1469
+ [pattern],
1470
+ [null]
1471
+ );
1203
1472
  }
1204
1473
  });
1205
1474
  }