@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.mjs CHANGED
@@ -97,6 +97,9 @@ var Cache = class {
97
97
  empty() {
98
98
  this.cache.clear();
99
99
  }
100
+ size() {
101
+ return this.cache.size;
102
+ }
100
103
  };
101
104
 
102
105
  // src/duration.ts
@@ -123,6 +126,37 @@ function ms(d) {
123
126
  }
124
127
  }
125
128
 
129
+ // src/hash.ts
130
+ var setHash = async (ctx, script, kind) => {
131
+ const regionContexts = "redis" in ctx ? [ctx] : ctx.regionContexts;
132
+ const hashSample = regionContexts[0].scriptHashes[kind];
133
+ if (!hashSample) {
134
+ await Promise.all(regionContexts.map(async (context) => {
135
+ context.scriptHashes[kind] = await context.redis.scriptLoad(script);
136
+ }));
137
+ }
138
+ ;
139
+ };
140
+ var safeEval = async (ctx, script, kind, keys, args) => {
141
+ if (!ctx.cacheScripts) {
142
+ return await ctx.redis.eval(script, keys, args);
143
+ }
144
+ ;
145
+ await setHash(ctx, script, kind);
146
+ try {
147
+ return await ctx.redis.evalsha(ctx.scriptHashes[kind], keys, args);
148
+ } catch (error) {
149
+ if (`${error}`.includes("NOSCRIPT")) {
150
+ console.log("Script with the expected hash was not found in redis db. It is probably flushed. Will load another scipt before continuing.");
151
+ ctx.scriptHashes[kind] = void 0;
152
+ await setHash(ctx, script, kind);
153
+ console.log(" New script successfully loaded.");
154
+ return await ctx.redis.evalsha(ctx.scriptHashes[kind], keys, args);
155
+ }
156
+ throw error;
157
+ }
158
+ };
159
+
126
160
  // src/lua-scripts/multi.ts
127
161
  var fixedWindowLimitScript = `
128
162
  local key = KEYS[1]
@@ -232,20 +266,71 @@ var resetScript = `
232
266
  until cursor == "0"
233
267
  `;
234
268
 
269
+ // src/deny-list.ts
270
+ var denyListCache = new Cache(/* @__PURE__ */ new Map());
271
+ var checkDenyListCache = (members) => {
272
+ return members.find(
273
+ (member) => denyListCache.isBlocked(member).blocked
274
+ );
275
+ };
276
+ var blockMember = (member) => {
277
+ if (denyListCache.size() > 1e3)
278
+ denyListCache.empty();
279
+ denyListCache.blockUntil(member, Date.now() + 6e4);
280
+ };
281
+ var checkDenyList = async (redis, prefix, members) => {
282
+ const deniedMembers = await redis.smismember(
283
+ [prefix, "denyList", "all"].join(":"),
284
+ members
285
+ );
286
+ let deniedMember = void 0;
287
+ deniedMembers.map((memberDenied, index) => {
288
+ if (memberDenied) {
289
+ blockMember(members[index]);
290
+ deniedMember = members[index];
291
+ }
292
+ });
293
+ return deniedMember;
294
+ };
295
+ var resolveResponses = ([ratelimitResponse, denyListResponse]) => {
296
+ if (denyListResponse) {
297
+ ratelimitResponse.success = false;
298
+ ratelimitResponse.remaining = 0;
299
+ ratelimitResponse.reason = "denyList";
300
+ ratelimitResponse.deniedValue = denyListResponse;
301
+ }
302
+ return ratelimitResponse;
303
+ };
304
+ var defaultDeniedResponse = (deniedValue) => {
305
+ return {
306
+ success: false,
307
+ limit: 0,
308
+ remaining: 0,
309
+ reset: 0,
310
+ pending: Promise.resolve(),
311
+ reason: "denyList",
312
+ deniedValue
313
+ };
314
+ };
315
+
235
316
  // src/ratelimit.ts
236
317
  var Ratelimit = class {
237
318
  limiter;
238
319
  ctx;
239
320
  prefix;
240
321
  timeout;
322
+ primaryRedis;
241
323
  analytics;
324
+ enableProtection;
242
325
  constructor(config) {
243
326
  this.ctx = config.ctx;
244
327
  this.limiter = config.limiter;
245
328
  this.timeout = config.timeout ?? 5e3;
246
329
  this.prefix = config.prefix ?? "@upstash/ratelimit";
330
+ this.enableProtection = config.enableProtection ?? false;
331
+ this.primaryRedis = "redis" in this.ctx ? this.ctx.redis : this.ctx.regionContexts[0].redis;
247
332
  this.analytics = config.analytics ? new Analytics({
248
- redis: Array.isArray(this.ctx.redis) ? this.ctx.redis[0] : this.ctx.redis,
333
+ redis: this.primaryRedis,
249
334
  prefix: this.prefix
250
335
  }) : void 0;
251
336
  if (config.ephemeralCache instanceof Map) {
@@ -291,43 +376,14 @@ var Ratelimit = class {
291
376
  * ```
292
377
  */
293
378
  limit = async (identifier, req) => {
294
- const key = [this.prefix, identifier].join(":");
295
379
  let timeoutId = null;
296
380
  try {
297
- const arr = [this.limiter().limit(this.ctx, key, req?.rate)];
298
- if (this.timeout > 0) {
299
- arr.push(
300
- new Promise((resolve) => {
301
- timeoutId = setTimeout(() => {
302
- resolve({
303
- success: true,
304
- limit: 0,
305
- remaining: 0,
306
- reset: 0,
307
- pending: Promise.resolve()
308
- });
309
- }, this.timeout);
310
- })
311
- );
312
- }
313
- const res = await Promise.race(arr);
314
- if (this.analytics) {
315
- try {
316
- const geo = req ? this.analytics.extractGeo(req) : void 0;
317
- const analyticsP = this.analytics.record({
318
- identifier,
319
- time: Date.now(),
320
- success: res.success,
321
- ...geo
322
- }).catch((err) => {
323
- console.warn("Failed to record analytics", err);
324
- });
325
- res.pending = Promise.all([res.pending, analyticsP]);
326
- } catch (err) {
327
- console.warn("Failed to record analytics", err);
328
- }
329
- }
330
- return res;
381
+ const response = this.getRatelimitResponse(identifier, req);
382
+ const { responseArray, newTimeoutId } = this.applyTimeout(response);
383
+ timeoutId = newTimeoutId;
384
+ const timedResponse = await Promise.race(responseArray);
385
+ const finalResponse = this.submitAnalytics(timedResponse, identifier, req);
386
+ return finalResponse;
331
387
  } finally {
332
388
  if (timeoutId) {
333
389
  clearTimeout(timeoutId);
@@ -386,6 +442,125 @@ var Ratelimit = class {
386
442
  const pattern = [this.prefix, identifier].join(":");
387
443
  return await this.limiter().getRemaining(this.ctx, pattern);
388
444
  };
445
+ /**
446
+ * Checks if the identifier or the values in req are in the deny list cache.
447
+ * If so, returns the default denied response.
448
+ *
449
+ * Otherwise, calls redis to check the rate limit and deny list. Returns after
450
+ * resolving the result. Resolving is overriding the rate limit result if
451
+ * the some value is in deny list.
452
+ *
453
+ * @param identifier identifier to block
454
+ * @param req options with ip, user agent, country, rate and geo info
455
+ * @returns rate limit response
456
+ */
457
+ getRatelimitResponse = async (identifier, req) => {
458
+ const key = this.getKey(identifier);
459
+ const definedMembers = this.getDefinedMembers(identifier, req);
460
+ const deniedMember = checkDenyListCache(definedMembers);
461
+ let result;
462
+ if (deniedMember) {
463
+ result = [defaultDeniedResponse(deniedMember), deniedMember];
464
+ } else {
465
+ result = await Promise.all([
466
+ this.limiter().limit(this.ctx, key, req?.rate),
467
+ checkDenyList(
468
+ this.primaryRedis,
469
+ this.prefix,
470
+ definedMembers
471
+ )
472
+ ]);
473
+ }
474
+ return resolveResponses(result);
475
+ };
476
+ /**
477
+ * Creates an array with the original response promise and a timeout promise
478
+ * if this.timeout > 0.
479
+ *
480
+ * @param response Ratelimit response promise
481
+ * @returns array with the response and timeout promise. also includes the timeout id
482
+ */
483
+ applyTimeout = (response) => {
484
+ let newTimeoutId = null;
485
+ const responseArray = [response];
486
+ if (this.timeout > 0) {
487
+ const timeoutResponse = new Promise((resolve) => {
488
+ newTimeoutId = setTimeout(() => {
489
+ resolve({
490
+ success: true,
491
+ limit: 0,
492
+ remaining: 0,
493
+ reset: 0,
494
+ pending: Promise.resolve(),
495
+ reason: "timeout"
496
+ });
497
+ }, this.timeout);
498
+ });
499
+ responseArray.push(timeoutResponse);
500
+ }
501
+ return {
502
+ responseArray,
503
+ newTimeoutId
504
+ };
505
+ };
506
+ /**
507
+ * submits analytics if this.analytics is set
508
+ *
509
+ * @param ratelimitResponse final rate limit response
510
+ * @param identifier identifier to submit
511
+ * @param req limit options
512
+ * @returns rate limit response after updating the .pending field
513
+ */
514
+ submitAnalytics = (ratelimitResponse, identifier, req) => {
515
+ if (this.analytics) {
516
+ try {
517
+ const geo = req ? this.analytics.extractGeo(req) : void 0;
518
+ const analyticsP = this.analytics.record({
519
+ identifier: ratelimitResponse.reason === "denyList" ? ratelimitResponse.deniedValue : identifier,
520
+ time: Date.now(),
521
+ success: ratelimitResponse.reason === "denyList" ? "denied" : ratelimitResponse.success,
522
+ ...geo
523
+ }).catch((err) => {
524
+ let errorMessage = "Failed to record analytics";
525
+ if (`${err}`.includes("WRONGTYPE")) {
526
+ errorMessage = `
527
+ Failed to record analytics. See the information below:
528
+
529
+ This can occur when you uprade to Ratelimit version 1.1.2
530
+ or later from an earlier version.
531
+
532
+ This occurs simply because the way we store analytics data
533
+ has changed. To avoid getting this error, disable analytics
534
+ for *an hour*, then simply enable it back.
535
+
536
+ `;
537
+ }
538
+ console.warn(errorMessage, err);
539
+ });
540
+ ratelimitResponse.pending = Promise.all([ratelimitResponse.pending, analyticsP]);
541
+ } catch (err) {
542
+ console.warn("Failed to record analytics", err);
543
+ }
544
+ ;
545
+ }
546
+ ;
547
+ return ratelimitResponse;
548
+ };
549
+ getKey = (identifier) => {
550
+ return [this.prefix, identifier].join(":");
551
+ };
552
+ /**
553
+ * returns a list of defined values from
554
+ * [identifier, req.ip, req.userAgent, req.country]
555
+ *
556
+ * @param identifier identifier
557
+ * @param req limit options
558
+ * @returns list of defined values
559
+ */
560
+ getDefinedMembers = (identifier, req) => {
561
+ const members = [identifier, req?.ip, req?.userAgent, req?.country];
562
+ return members.filter((item) => Boolean(item));
563
+ };
389
564
  };
390
565
 
391
566
  // src/multi.ts
@@ -409,7 +584,11 @@ var MultiRegionRatelimit = class extends Ratelimit {
409
584
  timeout: config.timeout,
410
585
  analytics: config.analytics,
411
586
  ctx: {
412
- redis: config.redis,
587
+ regionContexts: config.redis.map((redis) => ({
588
+ redis,
589
+ scriptHashes: {},
590
+ cacheScripts: config.cacheScripts ?? true
591
+ })),
413
592
  cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
414
593
  }
415
594
  });
@@ -444,7 +623,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
444
623
  limit: tokens,
445
624
  remaining: 0,
446
625
  reset: reset2,
447
- pending: Promise.resolve()
626
+ pending: Promise.resolve(),
627
+ reason: "cacheBlock"
448
628
  };
449
629
  }
450
630
  }
@@ -452,10 +632,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
452
632
  const bucket = Math.floor(Date.now() / windowDuration);
453
633
  const key = [identifier, bucket].join(":");
454
634
  const incrementBy = rate ? Math.max(1, rate) : 1;
455
- const dbs = ctx.redis.map((redis) => ({
456
- redis,
457
- request: redis.eval(
635
+ const dbs = ctx.regionContexts.map((regionContext) => ({
636
+ redis: regionContext.redis,
637
+ request: safeEval(
638
+ regionContext,
458
639
  fixedWindowLimitScript,
640
+ "limitHash",
459
641
  [key],
460
642
  [requestId, windowDuration, incrementBy]
461
643
  )
@@ -526,9 +708,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
526
708
  async getRemaining(ctx, identifier) {
527
709
  const bucket = Math.floor(Date.now() / windowDuration);
528
710
  const key = [identifier, bucket].join(":");
529
- const dbs = ctx.redis.map((redis) => ({
530
- redis,
531
- request: redis.eval(fixedWindowRemainingTokensScript, [key], [null])
711
+ const dbs = ctx.regionContexts.map((regionContext) => ({
712
+ redis: regionContext.redis,
713
+ request: safeEval(
714
+ regionContext,
715
+ fixedWindowRemainingTokensScript,
716
+ "getRemainingHash",
717
+ [key],
718
+ [null]
719
+ )
532
720
  }));
533
721
  const firstResponse = await Promise.any(dbs.map((s) => s.request));
534
722
  const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
@@ -545,9 +733,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
545
733
  if (ctx.cache) {
546
734
  ctx.cache.pop(identifier);
547
735
  }
548
- for (const db of ctx.redis) {
549
- await db.eval(resetScript, [pattern], [null]);
550
- }
736
+ await Promise.all(ctx.regionContexts.map((regionContext) => {
737
+ safeEval(
738
+ regionContext,
739
+ resetScript,
740
+ "resetHash",
741
+ [pattern],
742
+ [null]
743
+ );
744
+ }));
551
745
  }
552
746
  });
553
747
  }
@@ -572,6 +766,19 @@ var MultiRegionRatelimit = class extends Ratelimit {
572
766
  const windowDuration = ms(window);
573
767
  return () => ({
574
768
  async limit(ctx, identifier, rate) {
769
+ if (ctx.cache) {
770
+ const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
771
+ if (blocked) {
772
+ return {
773
+ success: false,
774
+ limit: tokens,
775
+ remaining: 0,
776
+ reset: reset2,
777
+ pending: Promise.resolve(),
778
+ reason: "cacheBlock"
779
+ };
780
+ }
781
+ }
575
782
  const requestId = randomId();
576
783
  const now = Date.now();
577
784
  const currentWindow = Math.floor(now / windowSize);
@@ -579,10 +786,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
579
786
  const previousWindow = currentWindow - 1;
580
787
  const previousKey = [identifier, previousWindow].join(":");
581
788
  const incrementBy = rate ? Math.max(1, rate) : 1;
582
- const dbs = ctx.redis.map((redis) => ({
583
- redis,
584
- request: redis.eval(
789
+ const dbs = ctx.regionContexts.map((regionContext) => ({
790
+ redis: regionContext.redis,
791
+ request: safeEval(
792
+ regionContext,
585
793
  slidingWindowLimitScript,
794
+ "limitHash",
586
795
  [currentKey, previousKey],
587
796
  [tokens, now, windowDuration, requestId, incrementBy]
588
797
  // lua seems to return `1` for true and `null` for false
@@ -667,10 +876,12 @@ var MultiRegionRatelimit = class extends Ratelimit {
667
876
  const currentKey = [identifier, currentWindow].join(":");
668
877
  const previousWindow = currentWindow - 1;
669
878
  const previousKey = [identifier, previousWindow].join(":");
670
- const dbs = ctx.redis.map((redis) => ({
671
- redis,
672
- request: redis.eval(
879
+ const dbs = ctx.regionContexts.map((regionContext) => ({
880
+ redis: regionContext.redis,
881
+ request: safeEval(
882
+ regionContext,
673
883
  slidingWindowRemainingTokensScript,
884
+ "getRemainingHash",
674
885
  [currentKey, previousKey],
675
886
  [now, windowSize]
676
887
  // lua seems to return `1` for true and `null` for false
@@ -684,9 +895,15 @@ var MultiRegionRatelimit = class extends Ratelimit {
684
895
  if (ctx.cache) {
685
896
  ctx.cache.pop(identifier);
686
897
  }
687
- for (const db of ctx.redis) {
688
- await db.eval(resetScript, [pattern], [null]);
689
- }
898
+ await Promise.all(ctx.regionContexts.map((regionContext) => {
899
+ safeEval(
900
+ regionContext,
901
+ resetScript,
902
+ "resetHash",
903
+ [pattern],
904
+ [null]
905
+ );
906
+ }));
690
907
  }
691
908
  });
692
909
  }
@@ -859,9 +1076,12 @@ var RegionRatelimit = class extends Ratelimit {
859
1076
  timeout: config.timeout,
860
1077
  analytics: config.analytics,
861
1078
  ctx: {
862
- redis: config.redis
1079
+ redis: config.redis,
1080
+ scriptHashes: {},
1081
+ cacheScripts: config.cacheScripts ?? true
863
1082
  },
864
- ephemeralCache: config.ephemeralCache
1083
+ ephemeralCache: config.ephemeralCache,
1084
+ enableProtection: config.enableProtection
865
1085
  });
866
1086
  }
867
1087
  /**
@@ -896,13 +1116,16 @@ var RegionRatelimit = class extends Ratelimit {
896
1116
  limit: tokens,
897
1117
  remaining: 0,
898
1118
  reset: reset2,
899
- pending: Promise.resolve()
1119
+ pending: Promise.resolve(),
1120
+ reason: "cacheBlock"
900
1121
  };
901
1122
  }
902
1123
  }
903
1124
  const incrementBy = rate ? Math.max(1, rate) : 1;
904
- const usedTokensAfterUpdate = await ctx.redis.eval(
1125
+ const usedTokensAfterUpdate = await safeEval(
1126
+ ctx,
905
1127
  fixedWindowLimitScript2,
1128
+ "limitHash",
906
1129
  [key],
907
1130
  [windowDuration, incrementBy]
908
1131
  );
@@ -923,8 +1146,10 @@ var RegionRatelimit = class extends Ratelimit {
923
1146
  async getRemaining(ctx, identifier) {
924
1147
  const bucket = Math.floor(Date.now() / windowDuration);
925
1148
  const key = [identifier, bucket].join(":");
926
- const usedTokens = await ctx.redis.eval(
1149
+ const usedTokens = await safeEval(
1150
+ ctx,
927
1151
  fixedWindowRemainingTokensScript2,
1152
+ "getRemainingHash",
928
1153
  [key],
929
1154
  [null]
930
1155
  );
@@ -935,7 +1160,13 @@ var RegionRatelimit = class extends Ratelimit {
935
1160
  if (ctx.cache) {
936
1161
  ctx.cache.pop(identifier);
937
1162
  }
938
- await ctx.redis.eval(resetScript, [pattern], [null]);
1163
+ await safeEval(
1164
+ ctx,
1165
+ resetScript,
1166
+ "resetHash",
1167
+ [pattern],
1168
+ [null]
1169
+ );
939
1170
  }
940
1171
  });
941
1172
  }
@@ -972,13 +1203,16 @@ var RegionRatelimit = class extends Ratelimit {
972
1203
  limit: tokens,
973
1204
  remaining: 0,
974
1205
  reset: reset2,
975
- pending: Promise.resolve()
1206
+ pending: Promise.resolve(),
1207
+ reason: "cacheBlock"
976
1208
  };
977
1209
  }
978
1210
  }
979
1211
  const incrementBy = rate ? Math.max(1, rate) : 1;
980
- const remainingTokens = await ctx.redis.eval(
1212
+ const remainingTokens = await safeEval(
1213
+ ctx,
981
1214
  slidingWindowLimitScript2,
1215
+ "limitHash",
982
1216
  [currentKey, previousKey],
983
1217
  [tokens, now, windowSize, incrementBy]
984
1218
  );
@@ -1001,8 +1235,10 @@ var RegionRatelimit = class extends Ratelimit {
1001
1235
  const currentKey = [identifier, currentWindow].join(":");
1002
1236
  const previousWindow = currentWindow - 1;
1003
1237
  const previousKey = [identifier, previousWindow].join(":");
1004
- const usedTokens = await ctx.redis.eval(
1238
+ const usedTokens = await safeEval(
1239
+ ctx,
1005
1240
  slidingWindowRemainingTokensScript2,
1241
+ "getRemainingHash",
1006
1242
  [currentKey, previousKey],
1007
1243
  [now, windowSize]
1008
1244
  );
@@ -1013,7 +1249,13 @@ var RegionRatelimit = class extends Ratelimit {
1013
1249
  if (ctx.cache) {
1014
1250
  ctx.cache.pop(identifier);
1015
1251
  }
1016
- await ctx.redis.eval(resetScript, [pattern], [null]);
1252
+ await safeEval(
1253
+ ctx,
1254
+ resetScript,
1255
+ "resetHash",
1256
+ [pattern],
1257
+ [null]
1258
+ );
1017
1259
  }
1018
1260
  });
1019
1261
  }
@@ -1042,14 +1284,17 @@ var RegionRatelimit = class extends Ratelimit {
1042
1284
  limit: maxTokens,
1043
1285
  remaining: 0,
1044
1286
  reset: reset2,
1045
- pending: Promise.resolve()
1287
+ pending: Promise.resolve(),
1288
+ reason: "cacheBlock"
1046
1289
  };
1047
1290
  }
1048
1291
  }
1049
1292
  const now = Date.now();
1050
1293
  const incrementBy = rate ? Math.max(1, rate) : 1;
1051
- const [remaining, reset] = await ctx.redis.eval(
1294
+ const [remaining, reset] = await safeEval(
1295
+ ctx,
1052
1296
  tokenBucketLimitScript,
1297
+ "limitHash",
1053
1298
  [identifier],
1054
1299
  [maxTokens, intervalDuration, refillRate, now, incrementBy]
1055
1300
  );
@@ -1066,8 +1311,10 @@ var RegionRatelimit = class extends Ratelimit {
1066
1311
  };
1067
1312
  },
1068
1313
  async getRemaining(ctx, identifier) {
1069
- const remainingTokens = await ctx.redis.eval(
1314
+ const remainingTokens = await safeEval(
1315
+ ctx,
1070
1316
  tokenBucketRemainingTokensScript,
1317
+ "getRemainingHash",
1071
1318
  [identifier],
1072
1319
  [maxTokens]
1073
1320
  );
@@ -1078,7 +1325,13 @@ var RegionRatelimit = class extends Ratelimit {
1078
1325
  if (ctx.cache) {
1079
1326
  ctx.cache.pop(identifier);
1080
1327
  }
1081
- await ctx.redis.eval(resetScript, [pattern], [null]);
1328
+ await safeEval(
1329
+ ctx,
1330
+ resetScript,
1331
+ "resetHash",
1332
+ [pattern],
1333
+ [null]
1334
+ );
1082
1335
  }
1083
1336
  });
1084
1337
  }
@@ -1121,9 +1374,13 @@ var RegionRatelimit = class extends Ratelimit {
1121
1374
  if (hit) {
1122
1375
  const cachedTokensAfterUpdate = ctx.cache.incr(key);
1123
1376
  const success = cachedTokensAfterUpdate < tokens;
1124
- const pending = success ? ctx.redis.eval(cachedFixedWindowLimitScript, [key], [windowDuration, incrementBy]).then((t) => {
1125
- ctx.cache.set(key, t);
1126
- }) : Promise.resolve();
1377
+ const pending = success ? safeEval(
1378
+ ctx,
1379
+ cachedFixedWindowLimitScript,
1380
+ "limitHash",
1381
+ [key],
1382
+ [windowDuration, incrementBy]
1383
+ ) : Promise.resolve();
1127
1384
  return {
1128
1385
  success,
1129
1386
  limit: tokens,
@@ -1132,8 +1389,10 @@ var RegionRatelimit = class extends Ratelimit {
1132
1389
  pending
1133
1390
  };
1134
1391
  }
1135
- const usedTokensAfterUpdate = await ctx.redis.eval(
1392
+ const usedTokensAfterUpdate = await safeEval(
1393
+ ctx,
1136
1394
  cachedFixedWindowLimitScript,
1395
+ "limitHash",
1137
1396
  [key],
1138
1397
  [windowDuration, incrementBy]
1139
1398
  );
@@ -1158,20 +1417,30 @@ var RegionRatelimit = class extends Ratelimit {
1158
1417
  const cachedUsedTokens = ctx.cache.get(key) ?? 0;
1159
1418
  return Math.max(0, tokens - cachedUsedTokens);
1160
1419
  }
1161
- const usedTokens = await ctx.redis.eval(
1420
+ const usedTokens = await safeEval(
1421
+ ctx,
1162
1422
  cachedFixedWindowRemainingTokenScript,
1423
+ "getRemainingHash",
1163
1424
  [key],
1164
1425
  [null]
1165
1426
  );
1166
1427
  return Math.max(0, tokens - usedTokens);
1167
1428
  },
1168
1429
  async resetTokens(ctx, identifier) {
1169
- const pattern = [identifier, "*"].join(":");
1170
1430
  if (!ctx.cache) {
1171
1431
  throw new Error("This algorithm requires a cache");
1172
1432
  }
1173
- ctx.cache.pop(identifier);
1174
- await ctx.redis.eval(resetScript, [pattern], [null]);
1433
+ const bucket = Math.floor(Date.now() / windowDuration);
1434
+ const key = [identifier, bucket].join(":");
1435
+ ctx.cache.pop(key);
1436
+ const pattern = [identifier, "*"].join(":");
1437
+ await safeEval(
1438
+ ctx,
1439
+ resetScript,
1440
+ "resetHash",
1441
+ [pattern],
1442
+ [null]
1443
+ );
1175
1444
  }
1176
1445
  });
1177
1446
  }