@upstash/ratelimit 1.0.3 → 1.1.0-canary-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -15
- package/dist/index.d.mts +17 -13
- package/dist/index.d.ts +17 -13
- package/dist/index.js +561 -291
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +561 -291
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -122,6 +122,12 @@ var Cache = class {
|
|
|
122
122
|
this.cache.set(key, value);
|
|
123
123
|
return value;
|
|
124
124
|
}
|
|
125
|
+
pop(key) {
|
|
126
|
+
this.cache.delete(key);
|
|
127
|
+
}
|
|
128
|
+
empty() {
|
|
129
|
+
this.cache.clear();
|
|
130
|
+
}
|
|
125
131
|
};
|
|
126
132
|
|
|
127
133
|
// src/duration.ts
|
|
@@ -149,7 +155,7 @@ function ms(d) {
|
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
// src/lua-scripts/multi.ts
|
|
152
|
-
var
|
|
158
|
+
var fixedWindowLimitScript = `
|
|
153
159
|
local key = KEYS[1]
|
|
154
160
|
local id = ARGV[1]
|
|
155
161
|
local window = ARGV[2]
|
|
@@ -165,7 +171,15 @@ var fixedWindowScript = `
|
|
|
165
171
|
|
|
166
172
|
return fields
|
|
167
173
|
`;
|
|
168
|
-
var
|
|
174
|
+
var fixedWindowRemainingTokensScript = `
|
|
175
|
+
local key = KEYS[1]
|
|
176
|
+
local tokens = 0
|
|
177
|
+
|
|
178
|
+
local fields = redis.call("HGETALL", key)
|
|
179
|
+
|
|
180
|
+
return fields
|
|
181
|
+
`;
|
|
182
|
+
var slidingWindowLimitScript = `
|
|
169
183
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
170
184
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
171
185
|
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
@@ -200,6 +214,54 @@ var slidingWindowScript = `
|
|
|
200
214
|
end
|
|
201
215
|
return {currentFields, previousFields, true}
|
|
202
216
|
`;
|
|
217
|
+
var slidingWindowRemainingTokensScript = `
|
|
218
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
219
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
220
|
+
local now = ARGV[1] -- current timestamp in milliseconds
|
|
221
|
+
local window = ARGV[2] -- interval in milliseconds
|
|
222
|
+
|
|
223
|
+
local currentFields = redis.call("HGETALL", currentKey)
|
|
224
|
+
local requestsInCurrentWindow = 0
|
|
225
|
+
for i = 2, #currentFields, 2 do
|
|
226
|
+
requestsInCurrentWindow = requestsInCurrentWindow + tonumber(currentFields[i])
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
local previousFields = redis.call("HGETALL", previousKey)
|
|
230
|
+
local requestsInPreviousWindow = 0
|
|
231
|
+
for i = 2, #previousFields, 2 do
|
|
232
|
+
requestsInPreviousWindow = requestsInPreviousWindow + tonumber(previousFields[i])
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
local percentageInCurrent = ( now % window) / window
|
|
236
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
237
|
+
|
|
238
|
+
return requestsInCurrentWindow + requestsInPreviousWindow
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
// src/lua-scripts/reset.ts
|
|
242
|
+
var resetScript = `
|
|
243
|
+
local pattern = KEYS[1]
|
|
244
|
+
|
|
245
|
+
-- Initialize cursor to start from 0
|
|
246
|
+
local cursor = "0"
|
|
247
|
+
|
|
248
|
+
repeat
|
|
249
|
+
-- Scan for keys matching the pattern
|
|
250
|
+
local scan_result = redis.call('SCAN', cursor, 'MATCH', pattern)
|
|
251
|
+
|
|
252
|
+
-- Extract cursor for the next iteration
|
|
253
|
+
cursor = scan_result[1]
|
|
254
|
+
|
|
255
|
+
-- Extract keys from the scan result
|
|
256
|
+
local keys = scan_result[2]
|
|
257
|
+
|
|
258
|
+
for i=1, #keys do
|
|
259
|
+
redis.call('DEL', keys[i])
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
-- Continue scanning until cursor is 0 (end of keyspace)
|
|
263
|
+
until cursor == "0"
|
|
264
|
+
`;
|
|
203
265
|
|
|
204
266
|
// src/ratelimit.ts
|
|
205
267
|
var Ratelimit = class {
|
|
@@ -263,7 +325,7 @@ var Ratelimit = class {
|
|
|
263
325
|
const key = [this.prefix, identifier].join(":");
|
|
264
326
|
let timeoutId = null;
|
|
265
327
|
try {
|
|
266
|
-
const arr = [this.limiter(this.ctx, key, req?.rate)];
|
|
328
|
+
const arr = [this.limiter().limit(this.ctx, key, req?.rate)];
|
|
267
329
|
if (this.timeout > 0) {
|
|
268
330
|
arr.push(
|
|
269
331
|
new Promise((resolve) => {
|
|
@@ -347,6 +409,14 @@ var Ratelimit = class {
|
|
|
347
409
|
}
|
|
348
410
|
return res;
|
|
349
411
|
};
|
|
412
|
+
resetUsedTokens = async (identifier) => {
|
|
413
|
+
const pattern = [this.prefix, identifier].join(":");
|
|
414
|
+
await this.limiter().resetTokens(this.ctx, pattern);
|
|
415
|
+
};
|
|
416
|
+
getRemaining = async (identifier) => {
|
|
417
|
+
const pattern = [this.prefix, identifier].join(":");
|
|
418
|
+
return await this.limiter().getRemaining(this.ctx, pattern);
|
|
419
|
+
};
|
|
350
420
|
};
|
|
351
421
|
|
|
352
422
|
// src/multi.ts
|
|
@@ -395,91 +465,122 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
395
465
|
*/
|
|
396
466
|
static fixedWindow(tokens, window) {
|
|
397
467
|
const windowDuration = ms(window);
|
|
398
|
-
return
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const requestId = randomId();
|
|
412
|
-
const bucket = Math.floor(Date.now() / windowDuration);
|
|
413
|
-
const key = [identifier, bucket].join(":");
|
|
414
|
-
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
415
|
-
const dbs = ctx.redis.map((redis) => ({
|
|
416
|
-
redis,
|
|
417
|
-
request: redis.eval(
|
|
418
|
-
fixedWindowScript,
|
|
419
|
-
[key],
|
|
420
|
-
[requestId, windowDuration, incrementBy]
|
|
421
|
-
)
|
|
422
|
-
}));
|
|
423
|
-
const firstResponse = await Promise.any(dbs.map((s) => s.request));
|
|
424
|
-
const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
|
|
425
|
-
let parsedToken = 0;
|
|
426
|
-
if (index % 2) {
|
|
427
|
-
parsedToken = Number.parseInt(usedToken);
|
|
468
|
+
return () => ({
|
|
469
|
+
async limit(ctx, identifier, rate) {
|
|
470
|
+
if (ctx.cache) {
|
|
471
|
+
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
472
|
+
if (blocked) {
|
|
473
|
+
return {
|
|
474
|
+
success: false,
|
|
475
|
+
limit: tokens,
|
|
476
|
+
remaining: 0,
|
|
477
|
+
reset: reset2,
|
|
478
|
+
pending: Promise.resolve()
|
|
479
|
+
};
|
|
480
|
+
}
|
|
428
481
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
482
|
+
const requestId = randomId();
|
|
483
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
484
|
+
const key = [identifier, bucket].join(":");
|
|
485
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
486
|
+
const dbs = ctx.redis.map((redis) => ({
|
|
487
|
+
redis,
|
|
488
|
+
request: redis.eval(
|
|
489
|
+
fixedWindowLimitScript,
|
|
490
|
+
[key],
|
|
491
|
+
[requestId, windowDuration, incrementBy]
|
|
492
|
+
)
|
|
493
|
+
}));
|
|
494
|
+
const firstResponse = await Promise.any(dbs.map((s) => s.request));
|
|
495
|
+
const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
|
|
496
|
+
let parsedToken = 0;
|
|
497
|
+
if (index % 2) {
|
|
498
|
+
parsedToken = Number.parseInt(usedToken);
|
|
499
|
+
}
|
|
500
|
+
return accTokens + parsedToken;
|
|
501
|
+
}, 0);
|
|
502
|
+
const remaining = tokens - usedTokens;
|
|
503
|
+
async function sync() {
|
|
504
|
+
const individualIDs = await Promise.all(dbs.map((s) => s.request));
|
|
505
|
+
const allIDs = Array.from(
|
|
506
|
+
new Set(
|
|
507
|
+
individualIDs.flatMap((_) => _).reduce((acc, curr, index) => {
|
|
508
|
+
if (index % 2 === 0) {
|
|
509
|
+
acc.push(curr);
|
|
510
|
+
}
|
|
511
|
+
return acc;
|
|
512
|
+
}, [])
|
|
513
|
+
).values()
|
|
514
|
+
);
|
|
515
|
+
for (const db of dbs) {
|
|
516
|
+
const usedDbTokens = (await db.request).reduce(
|
|
517
|
+
(accTokens, usedToken, index) => {
|
|
518
|
+
let parsedToken = 0;
|
|
519
|
+
if (index % 2) {
|
|
520
|
+
parsedToken = Number.parseInt(usedToken);
|
|
521
|
+
}
|
|
522
|
+
return accTokens + parsedToken;
|
|
523
|
+
},
|
|
524
|
+
0
|
|
525
|
+
);
|
|
526
|
+
const dbIds = (await db.request).reduce((ids, currentId, index) => {
|
|
437
527
|
if (index % 2 === 0) {
|
|
438
|
-
|
|
528
|
+
ids.push(currentId);
|
|
439
529
|
}
|
|
440
|
-
return
|
|
441
|
-
}, [])
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
for (const db of dbs) {
|
|
445
|
-
const usedDbTokens = (await db.request).reduce((accTokens, usedToken, index) => {
|
|
446
|
-
let parsedToken = 0;
|
|
447
|
-
if (index % 2) {
|
|
448
|
-
parsedToken = Number.parseInt(usedToken);
|
|
530
|
+
return ids;
|
|
531
|
+
}, []);
|
|
532
|
+
if (usedDbTokens >= tokens) {
|
|
533
|
+
continue;
|
|
449
534
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
535
|
+
const diff = allIDs.filter((id) => !dbIds.includes(id));
|
|
536
|
+
if (diff.length === 0) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
for (const requestId2 of diff) {
|
|
540
|
+
await db.redis.hset(key, { [requestId2]: incrementBy });
|
|
455
541
|
}
|
|
456
|
-
return ids;
|
|
457
|
-
}, []);
|
|
458
|
-
if (usedDbTokens >= tokens) {
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
const diff = allIDs.filter((id) => !dbIds.includes(id));
|
|
462
|
-
if (diff.length === 0) {
|
|
463
|
-
continue;
|
|
464
542
|
}
|
|
465
|
-
|
|
466
|
-
|
|
543
|
+
}
|
|
544
|
+
const success = remaining > 0;
|
|
545
|
+
const reset = (bucket + 1) * windowDuration;
|
|
546
|
+
if (ctx.cache && !success) {
|
|
547
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
success,
|
|
551
|
+
limit: tokens,
|
|
552
|
+
remaining,
|
|
553
|
+
reset,
|
|
554
|
+
pending: sync()
|
|
555
|
+
};
|
|
556
|
+
},
|
|
557
|
+
async getRemaining(ctx, identifier) {
|
|
558
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
559
|
+
const key = [identifier, bucket].join(":");
|
|
560
|
+
const dbs = ctx.redis.map((redis) => ({
|
|
561
|
+
redis,
|
|
562
|
+
request: redis.eval(fixedWindowRemainingTokensScript, [key], [null])
|
|
563
|
+
}));
|
|
564
|
+
const firstResponse = await Promise.any(dbs.map((s) => s.request));
|
|
565
|
+
const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
|
|
566
|
+
let parsedToken = 0;
|
|
567
|
+
if (index % 2) {
|
|
568
|
+
parsedToken = Number.parseInt(usedToken);
|
|
467
569
|
}
|
|
570
|
+
return accTokens + parsedToken;
|
|
571
|
+
}, 0);
|
|
572
|
+
return Math.max(0, tokens - usedTokens);
|
|
573
|
+
},
|
|
574
|
+
async resetTokens(ctx, identifier) {
|
|
575
|
+
const pattern = [identifier, "*"].join(":");
|
|
576
|
+
if (ctx.cache) {
|
|
577
|
+
ctx.cache.pop(identifier);
|
|
578
|
+
}
|
|
579
|
+
for (const db of ctx.redis) {
|
|
580
|
+
await db.eval(resetScript, [pattern], [null]);
|
|
468
581
|
}
|
|
469
582
|
}
|
|
470
|
-
|
|
471
|
-
const reset = (bucket + 1) * windowDuration;
|
|
472
|
-
if (ctx.cache && !success) {
|
|
473
|
-
ctx.cache.blockUntil(identifier, reset);
|
|
474
|
-
}
|
|
475
|
-
return {
|
|
476
|
-
success,
|
|
477
|
-
limit: tokens,
|
|
478
|
-
remaining,
|
|
479
|
-
reset,
|
|
480
|
-
pending: sync()
|
|
481
|
-
};
|
|
482
|
-
};
|
|
583
|
+
});
|
|
483
584
|
}
|
|
484
585
|
/**
|
|
485
586
|
* Combined approach of `slidingLogs` and `fixedWindow` with lower storage
|
|
@@ -500,100 +601,129 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
500
601
|
static slidingWindow(tokens, window) {
|
|
501
602
|
const windowSize = ms(window);
|
|
502
603
|
const windowDuration = ms(window);
|
|
503
|
-
return
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
redis
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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;
|
|
538
|
-
const remaining = tokens - usedTokens;
|
|
539
|
-
async function sync() {
|
|
540
|
-
const res = await Promise.all(dbs.map((s) => s.request));
|
|
541
|
-
const allCurrentIds = res.flatMap(([current2]) => current2).reduce((accCurrentIds, curr, index) => {
|
|
542
|
-
if (index % 2 === 0) {
|
|
543
|
-
accCurrentIds.push(curr);
|
|
604
|
+
return () => ({
|
|
605
|
+
async limit(ctx, identifier, rate) {
|
|
606
|
+
const requestId = randomId();
|
|
607
|
+
const now = Date.now();
|
|
608
|
+
const currentWindow = Math.floor(now / windowSize);
|
|
609
|
+
const currentKey = [identifier, currentWindow].join(":");
|
|
610
|
+
const previousWindow = currentWindow - 1;
|
|
611
|
+
const previousKey = [identifier, previousWindow].join(":");
|
|
612
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
613
|
+
const dbs = ctx.redis.map((redis) => ({
|
|
614
|
+
redis,
|
|
615
|
+
request: redis.eval(
|
|
616
|
+
slidingWindowLimitScript,
|
|
617
|
+
[currentKey, previousKey],
|
|
618
|
+
[tokens, now, windowDuration, requestId, incrementBy]
|
|
619
|
+
// lua seems to return `1` for true and `null` for false
|
|
620
|
+
)
|
|
621
|
+
}));
|
|
622
|
+
const percentageInCurrent = now % windowDuration / windowDuration;
|
|
623
|
+
const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
|
|
624
|
+
const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
|
|
625
|
+
let parsedToken = 0;
|
|
626
|
+
if (index % 2) {
|
|
627
|
+
parsedToken = Number.parseInt(usedToken);
|
|
544
628
|
}
|
|
545
|
-
return
|
|
546
|
-
},
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
629
|
+
return accTokens + parsedToken;
|
|
630
|
+
}, 0);
|
|
631
|
+
const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
|
|
632
|
+
let parsedToken = 0;
|
|
633
|
+
if (index % 2) {
|
|
634
|
+
parsedToken = Number.parseInt(usedToken);
|
|
635
|
+
}
|
|
636
|
+
return accTokens + parsedToken;
|
|
637
|
+
}, 0);
|
|
638
|
+
const previousPartialUsed = previousUsedTokens * (1 - percentageInCurrent);
|
|
639
|
+
const usedTokens = previousPartialUsed + currentUsedTokens;
|
|
640
|
+
const remaining = tokens - usedTokens;
|
|
641
|
+
async function sync() {
|
|
642
|
+
const res = await Promise.all(dbs.map((s) => s.request));
|
|
643
|
+
const allCurrentIds = res.flatMap(([current2]) => current2).reduce((accCurrentIds, curr, index) => {
|
|
550
644
|
if (index % 2 === 0) {
|
|
551
|
-
|
|
645
|
+
accCurrentIds.push(curr);
|
|
552
646
|
}
|
|
553
|
-
return
|
|
647
|
+
return accCurrentIds;
|
|
554
648
|
}, []);
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
649
|
+
for (const db of dbs) {
|
|
650
|
+
const [_current, previous2, _success] = await db.request;
|
|
651
|
+
const dbIds = previous2.reduce((ids, currentId, index) => {
|
|
652
|
+
if (index % 2 === 0) {
|
|
653
|
+
ids.push(currentId);
|
|
654
|
+
}
|
|
655
|
+
return ids;
|
|
656
|
+
}, []);
|
|
657
|
+
const usedDbTokens = previous2.reduce((accTokens, usedToken, index) => {
|
|
658
|
+
let parsedToken = 0;
|
|
659
|
+
if (index % 2) {
|
|
660
|
+
parsedToken = Number.parseInt(usedToken);
|
|
661
|
+
}
|
|
662
|
+
return accTokens + parsedToken;
|
|
663
|
+
}, 0);
|
|
664
|
+
if (usedDbTokens >= tokens) {
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const diff = allCurrentIds.filter((id) => !dbIds.includes(id));
|
|
668
|
+
if (diff.length === 0) {
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
for (const requestId2 of diff) {
|
|
672
|
+
await db.redis.hset(currentKey, { [requestId2]: incrementBy });
|
|
559
673
|
}
|
|
560
|
-
return accTokens + parsedToken;
|
|
561
|
-
}, 0);
|
|
562
|
-
if (usedDbTokens >= tokens) {
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
const diff = allCurrentIds.filter((id) => !dbIds.includes(id));
|
|
566
|
-
if (diff.length === 0) {
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
for (const requestId2 of diff) {
|
|
570
|
-
await db.redis.hset(currentKey, { [requestId2]: incrementBy });
|
|
571
674
|
}
|
|
572
675
|
}
|
|
676
|
+
const reset = (currentWindow + 1) * windowDuration;
|
|
677
|
+
if (ctx.cache && !success) {
|
|
678
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
success: Boolean(success),
|
|
682
|
+
limit: tokens,
|
|
683
|
+
remaining: Math.max(0, remaining),
|
|
684
|
+
reset,
|
|
685
|
+
pending: sync()
|
|
686
|
+
};
|
|
687
|
+
},
|
|
688
|
+
async getRemaining(ctx, identifier) {
|
|
689
|
+
const now = Date.now();
|
|
690
|
+
const currentWindow = Math.floor(now / windowSize);
|
|
691
|
+
const currentKey = [identifier, currentWindow].join(":");
|
|
692
|
+
const previousWindow = currentWindow - 1;
|
|
693
|
+
const previousKey = [identifier, previousWindow].join(":");
|
|
694
|
+
const dbs = ctx.redis.map((redis) => ({
|
|
695
|
+
redis,
|
|
696
|
+
request: redis.eval(
|
|
697
|
+
slidingWindowRemainingTokensScript,
|
|
698
|
+
[currentKey, previousKey],
|
|
699
|
+
[now, windowSize]
|
|
700
|
+
// lua seems to return `1` for true and `null` for false
|
|
701
|
+
)
|
|
702
|
+
}));
|
|
703
|
+
const usedTokens = await Promise.any(dbs.map((s) => s.request));
|
|
704
|
+
return Math.max(0, tokens - usedTokens);
|
|
705
|
+
},
|
|
706
|
+
async resetTokens(ctx, identifier) {
|
|
707
|
+
const pattern = [identifier, "*"].join(":");
|
|
708
|
+
if (ctx.cache) {
|
|
709
|
+
ctx.cache.pop(identifier);
|
|
710
|
+
}
|
|
711
|
+
for (const db of ctx.redis) {
|
|
712
|
+
await db.eval(resetScript, [pattern], [null]);
|
|
713
|
+
}
|
|
573
714
|
}
|
|
574
|
-
|
|
575
|
-
if (ctx.cache && !success) {
|
|
576
|
-
ctx.cache.blockUntil(identifier, reset);
|
|
577
|
-
}
|
|
578
|
-
return {
|
|
579
|
-
success: Boolean(success),
|
|
580
|
-
limit: tokens,
|
|
581
|
-
remaining: Math.max(0, remaining),
|
|
582
|
-
reset,
|
|
583
|
-
pending: sync()
|
|
584
|
-
};
|
|
585
|
-
};
|
|
715
|
+
});
|
|
586
716
|
}
|
|
587
717
|
};
|
|
588
718
|
|
|
589
719
|
// src/lua-scripts/single.ts
|
|
590
|
-
var
|
|
720
|
+
var fixedWindowLimitScript2 = `
|
|
591
721
|
local key = KEYS[1]
|
|
592
722
|
local window = ARGV[1]
|
|
593
723
|
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
594
724
|
|
|
595
725
|
local r = redis.call("INCRBY", key, incrementBy)
|
|
596
|
-
if r == incrementBy then
|
|
726
|
+
if r == tonumber(incrementBy) then
|
|
597
727
|
-- The first time this key is set, the value will be equal to incrementBy.
|
|
598
728
|
-- So we only need the expire command once
|
|
599
729
|
redis.call("PEXPIRE", key, window)
|
|
@@ -601,7 +731,17 @@ var fixedWindowScript2 = `
|
|
|
601
731
|
|
|
602
732
|
return r
|
|
603
733
|
`;
|
|
604
|
-
var
|
|
734
|
+
var fixedWindowRemainingTokensScript2 = `
|
|
735
|
+
local key = KEYS[1]
|
|
736
|
+
local tokens = 0
|
|
737
|
+
|
|
738
|
+
local value = redis.call('GET', key)
|
|
739
|
+
if value then
|
|
740
|
+
tokens = value
|
|
741
|
+
end
|
|
742
|
+
return tokens
|
|
743
|
+
`;
|
|
744
|
+
var slidingWindowLimitScript2 = `
|
|
605
745
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
606
746
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
607
747
|
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
@@ -626,14 +766,36 @@ var slidingWindowScript2 = `
|
|
|
626
766
|
end
|
|
627
767
|
|
|
628
768
|
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
629
|
-
if newValue == incrementBy then
|
|
769
|
+
if newValue == tonumber(incrementBy) then
|
|
630
770
|
-- The first time this key is set, the value will be equal to incrementBy.
|
|
631
771
|
-- So we only need the expire command once
|
|
632
772
|
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
633
773
|
end
|
|
634
774
|
return tokens - ( newValue + requestsInPreviousWindow )
|
|
635
775
|
`;
|
|
636
|
-
var
|
|
776
|
+
var slidingWindowRemainingTokensScript2 = `
|
|
777
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
778
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
779
|
+
local now = ARGV[1] -- current timestamp in milliseconds
|
|
780
|
+
local window = ARGV[2] -- interval in milliseconds
|
|
781
|
+
|
|
782
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
783
|
+
if requestsInCurrentWindow == false then
|
|
784
|
+
requestsInCurrentWindow = 0
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
788
|
+
if requestsInPreviousWindow == false then
|
|
789
|
+
requestsInPreviousWindow = 0
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
local percentageInCurrent = ( now % window ) / window
|
|
793
|
+
-- weighted requests to consider from the previous window
|
|
794
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
795
|
+
|
|
796
|
+
return requestsInPreviousWindow + requestsInCurrentWindow
|
|
797
|
+
`;
|
|
798
|
+
var tokenBucketLimitScript = `
|
|
637
799
|
local key = KEYS[1] -- identifier including prefixes
|
|
638
800
|
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
639
801
|
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
@@ -672,7 +834,19 @@ var tokenBucketScript = `
|
|
|
672
834
|
redis.call("PEXPIRE", key, expireAt)
|
|
673
835
|
return {remaining, refilledAt + interval}
|
|
674
836
|
`;
|
|
675
|
-
var
|
|
837
|
+
var tokenBucketRemainingTokensScript = `
|
|
838
|
+
local key = KEYS[1]
|
|
839
|
+
local maxTokens = tonumber(ARGV[1])
|
|
840
|
+
|
|
841
|
+
local bucket = redis.call("HMGET", key, "tokens")
|
|
842
|
+
|
|
843
|
+
if bucket[1] == false then
|
|
844
|
+
return maxTokens
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
return tonumber(bucket[1])
|
|
848
|
+
`;
|
|
849
|
+
var cachedFixedWindowLimitScript = `
|
|
676
850
|
local key = KEYS[1]
|
|
677
851
|
local window = ARGV[1]
|
|
678
852
|
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
@@ -686,6 +860,16 @@ var cachedFixedWindowScript = `
|
|
|
686
860
|
|
|
687
861
|
return r
|
|
688
862
|
`;
|
|
863
|
+
var cachedFixedWindowRemainingTokenScript = `
|
|
864
|
+
local key = KEYS[1]
|
|
865
|
+
local tokens = 0
|
|
866
|
+
|
|
867
|
+
local value = redis.call('GET', key)
|
|
868
|
+
if value then
|
|
869
|
+
tokens = value
|
|
870
|
+
end
|
|
871
|
+
return tokens
|
|
872
|
+
`;
|
|
689
873
|
|
|
690
874
|
// src/single.ts
|
|
691
875
|
var RegionRatelimit = class extends Ratelimit {
|
|
@@ -724,41 +908,60 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
724
908
|
*/
|
|
725
909
|
static fixedWindow(tokens, window) {
|
|
726
910
|
const windowDuration = ms(window);
|
|
727
|
-
return
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
911
|
+
return () => ({
|
|
912
|
+
async limit(ctx, identifier, rate) {
|
|
913
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
914
|
+
const key = [identifier, bucket].join(":");
|
|
915
|
+
if (ctx.cache) {
|
|
916
|
+
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
917
|
+
if (blocked) {
|
|
918
|
+
return {
|
|
919
|
+
success: false,
|
|
920
|
+
limit: tokens,
|
|
921
|
+
remaining: 0,
|
|
922
|
+
reset: reset2,
|
|
923
|
+
pending: Promise.resolve()
|
|
924
|
+
};
|
|
925
|
+
}
|
|
740
926
|
}
|
|
927
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
928
|
+
const usedTokensAfterUpdate = await ctx.redis.eval(
|
|
929
|
+
fixedWindowLimitScript2,
|
|
930
|
+
[key],
|
|
931
|
+
[windowDuration, incrementBy]
|
|
932
|
+
);
|
|
933
|
+
const success = usedTokensAfterUpdate <= tokens;
|
|
934
|
+
const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
|
|
935
|
+
const reset = (bucket + 1) * windowDuration;
|
|
936
|
+
if (ctx.cache && !success) {
|
|
937
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
success,
|
|
941
|
+
limit: tokens,
|
|
942
|
+
remaining: remainingTokens,
|
|
943
|
+
reset,
|
|
944
|
+
pending: Promise.resolve()
|
|
945
|
+
};
|
|
946
|
+
},
|
|
947
|
+
async getRemaining(ctx, identifier) {
|
|
948
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
949
|
+
const key = [identifier, bucket].join(":");
|
|
950
|
+
const usedTokens = await ctx.redis.eval(
|
|
951
|
+
fixedWindowRemainingTokensScript2,
|
|
952
|
+
[key],
|
|
953
|
+
[null]
|
|
954
|
+
);
|
|
955
|
+
return Math.max(0, tokens - usedTokens);
|
|
956
|
+
},
|
|
957
|
+
async resetTokens(ctx, identifier) {
|
|
958
|
+
const pattern = [identifier, "*"].join(":");
|
|
959
|
+
if (ctx.cache) {
|
|
960
|
+
ctx.cache.pop(identifier);
|
|
961
|
+
}
|
|
962
|
+
await ctx.redis.eval(resetScript, [pattern], [null]);
|
|
741
963
|
}
|
|
742
|
-
|
|
743
|
-
const usedTokensAfterUpdate = await ctx.redis.eval(
|
|
744
|
-
fixedWindowScript2,
|
|
745
|
-
[key],
|
|
746
|
-
[windowDuration, incrementBy]
|
|
747
|
-
);
|
|
748
|
-
const success = usedTokensAfterUpdate <= tokens;
|
|
749
|
-
const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
|
|
750
|
-
const reset = (bucket + 1) * windowDuration;
|
|
751
|
-
if (ctx.cache && !success) {
|
|
752
|
-
ctx.cache.blockUntil(identifier, reset);
|
|
753
|
-
}
|
|
754
|
-
return {
|
|
755
|
-
success,
|
|
756
|
-
limit: tokens,
|
|
757
|
-
remaining: remainingTokens,
|
|
758
|
-
reset,
|
|
759
|
-
pending: Promise.resolve()
|
|
760
|
-
};
|
|
761
|
-
};
|
|
964
|
+
});
|
|
762
965
|
}
|
|
763
966
|
/**
|
|
764
967
|
* Combined approach of `slidingLogs` and `fixedWindow` with lower storage
|
|
@@ -778,43 +981,65 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
778
981
|
*/
|
|
779
982
|
static slidingWindow(tokens, window) {
|
|
780
983
|
const windowSize = ms(window);
|
|
781
|
-
return
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
984
|
+
return () => ({
|
|
985
|
+
async limit(ctx, identifier, rate) {
|
|
986
|
+
const now = Date.now();
|
|
987
|
+
const currentWindow = Math.floor(now / windowSize);
|
|
988
|
+
const currentKey = [identifier, currentWindow].join(":");
|
|
989
|
+
const previousWindow = currentWindow - 1;
|
|
990
|
+
const previousKey = [identifier, previousWindow].join(":");
|
|
991
|
+
if (ctx.cache) {
|
|
992
|
+
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
993
|
+
if (blocked) {
|
|
994
|
+
return {
|
|
995
|
+
success: false,
|
|
996
|
+
limit: tokens,
|
|
997
|
+
remaining: 0,
|
|
998
|
+
reset: reset2,
|
|
999
|
+
pending: Promise.resolve()
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
797
1002
|
}
|
|
1003
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1004
|
+
const remainingTokens = await ctx.redis.eval(
|
|
1005
|
+
slidingWindowLimitScript2,
|
|
1006
|
+
[currentKey, previousKey],
|
|
1007
|
+
[tokens, now, windowSize, incrementBy]
|
|
1008
|
+
);
|
|
1009
|
+
const success = remainingTokens >= 0;
|
|
1010
|
+
const reset = (currentWindow + 1) * windowSize;
|
|
1011
|
+
if (ctx.cache && !success) {
|
|
1012
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
success,
|
|
1016
|
+
limit: tokens,
|
|
1017
|
+
remaining: Math.max(0, remainingTokens),
|
|
1018
|
+
reset,
|
|
1019
|
+
pending: Promise.resolve()
|
|
1020
|
+
};
|
|
1021
|
+
},
|
|
1022
|
+
async getRemaining(ctx, identifier) {
|
|
1023
|
+
const now = Date.now();
|
|
1024
|
+
const currentWindow = Math.floor(now / windowSize);
|
|
1025
|
+
const currentKey = [identifier, currentWindow].join(":");
|
|
1026
|
+
const previousWindow = currentWindow - 1;
|
|
1027
|
+
const previousKey = [identifier, previousWindow].join(":");
|
|
1028
|
+
const usedTokens = await ctx.redis.eval(
|
|
1029
|
+
slidingWindowRemainingTokensScript2,
|
|
1030
|
+
[currentKey, previousKey],
|
|
1031
|
+
[now, windowSize]
|
|
1032
|
+
);
|
|
1033
|
+
return Math.max(0, tokens - usedTokens);
|
|
1034
|
+
},
|
|
1035
|
+
async resetTokens(ctx, identifier) {
|
|
1036
|
+
const pattern = [identifier, "*"].join(":");
|
|
1037
|
+
if (ctx.cache) {
|
|
1038
|
+
ctx.cache.pop(identifier);
|
|
1039
|
+
}
|
|
1040
|
+
await ctx.redis.eval(resetScript, [pattern], [null]);
|
|
798
1041
|
}
|
|
799
|
-
|
|
800
|
-
const remainingTokens = await ctx.redis.eval(
|
|
801
|
-
slidingWindowScript2,
|
|
802
|
-
[currentKey, previousKey],
|
|
803
|
-
[tokens, now, windowSize, incrementBy]
|
|
804
|
-
);
|
|
805
|
-
const success = remainingTokens >= 0;
|
|
806
|
-
const reset = (currentWindow + 1) * windowSize;
|
|
807
|
-
if (ctx.cache && !success) {
|
|
808
|
-
ctx.cache.blockUntil(identifier, reset);
|
|
809
|
-
}
|
|
810
|
-
return {
|
|
811
|
-
success,
|
|
812
|
-
limit: tokens,
|
|
813
|
-
remaining: Math.max(0, remainingTokens),
|
|
814
|
-
reset,
|
|
815
|
-
pending: Promise.resolve()
|
|
816
|
-
};
|
|
817
|
-
};
|
|
1042
|
+
});
|
|
818
1043
|
}
|
|
819
1044
|
/**
|
|
820
1045
|
* You have a bucket filled with `{maxTokens}` tokens that refills constantly
|
|
@@ -831,38 +1056,55 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
831
1056
|
*/
|
|
832
1057
|
static tokenBucket(refillRate, interval, maxTokens) {
|
|
833
1058
|
const intervalDuration = ms(interval);
|
|
834
|
-
return
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1059
|
+
return () => ({
|
|
1060
|
+
async limit(ctx, identifier, rate) {
|
|
1061
|
+
if (ctx.cache) {
|
|
1062
|
+
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
1063
|
+
if (blocked) {
|
|
1064
|
+
return {
|
|
1065
|
+
success: false,
|
|
1066
|
+
limit: maxTokens,
|
|
1067
|
+
remaining: 0,
|
|
1068
|
+
reset: reset2,
|
|
1069
|
+
pending: Promise.resolve()
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
845
1072
|
}
|
|
1073
|
+
const now = Date.now();
|
|
1074
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1075
|
+
const [remaining, reset] = await ctx.redis.eval(
|
|
1076
|
+
tokenBucketLimitScript,
|
|
1077
|
+
[identifier],
|
|
1078
|
+
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1079
|
+
);
|
|
1080
|
+
const success = remaining >= 0;
|
|
1081
|
+
if (ctx.cache && !success) {
|
|
1082
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
success,
|
|
1086
|
+
limit: maxTokens,
|
|
1087
|
+
remaining,
|
|
1088
|
+
reset,
|
|
1089
|
+
pending: Promise.resolve()
|
|
1090
|
+
};
|
|
1091
|
+
},
|
|
1092
|
+
async getRemaining(ctx, identifier) {
|
|
1093
|
+
const remainingTokens = await ctx.redis.eval(
|
|
1094
|
+
tokenBucketRemainingTokensScript,
|
|
1095
|
+
[identifier],
|
|
1096
|
+
[maxTokens]
|
|
1097
|
+
);
|
|
1098
|
+
return remainingTokens;
|
|
1099
|
+
},
|
|
1100
|
+
async resetTokens(ctx, identifier) {
|
|
1101
|
+
const pattern = identifier;
|
|
1102
|
+
if (ctx.cache) {
|
|
1103
|
+
ctx.cache.pop(identifier);
|
|
1104
|
+
}
|
|
1105
|
+
await ctx.redis.eval(resetScript, [pattern], [null]);
|
|
846
1106
|
}
|
|
847
|
-
|
|
848
|
-
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
849
|
-
const [remaining, reset] = await ctx.redis.eval(
|
|
850
|
-
tokenBucketScript,
|
|
851
|
-
[identifier],
|
|
852
|
-
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
853
|
-
);
|
|
854
|
-
const success = remaining >= 0;
|
|
855
|
-
if (ctx.cache && !success) {
|
|
856
|
-
ctx.cache.blockUntil(identifier, reset);
|
|
857
|
-
}
|
|
858
|
-
return {
|
|
859
|
-
success,
|
|
860
|
-
limit: maxTokens,
|
|
861
|
-
remaining,
|
|
862
|
-
reset,
|
|
863
|
-
pending: Promise.resolve()
|
|
864
|
-
};
|
|
865
|
-
};
|
|
1107
|
+
});
|
|
866
1108
|
}
|
|
867
1109
|
/**
|
|
868
1110
|
* cachedFixedWindow first uses the local cache to decide if a request may pass and then updates
|
|
@@ -890,44 +1132,72 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
890
1132
|
*/
|
|
891
1133
|
static cachedFixedWindow(tokens, window) {
|
|
892
1134
|
const windowDuration = ms(window);
|
|
893
|
-
return
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
ctx.
|
|
907
|
-
|
|
1135
|
+
return () => ({
|
|
1136
|
+
async limit(ctx, identifier, rate) {
|
|
1137
|
+
if (!ctx.cache) {
|
|
1138
|
+
throw new Error("This algorithm requires a cache");
|
|
1139
|
+
}
|
|
1140
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1141
|
+
const key = [identifier, bucket].join(":");
|
|
1142
|
+
const reset = (bucket + 1) * windowDuration;
|
|
1143
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1144
|
+
const hit = typeof ctx.cache.get(key) === "number";
|
|
1145
|
+
if (hit) {
|
|
1146
|
+
const cachedTokensAfterUpdate = ctx.cache.incr(key);
|
|
1147
|
+
const success = cachedTokensAfterUpdate < tokens;
|
|
1148
|
+
const pending = success ? ctx.redis.eval(cachedFixedWindowLimitScript, [key], [windowDuration, incrementBy]).then((t) => {
|
|
1149
|
+
ctx.cache.set(key, t);
|
|
1150
|
+
}) : Promise.resolve();
|
|
1151
|
+
return {
|
|
1152
|
+
success,
|
|
1153
|
+
limit: tokens,
|
|
1154
|
+
remaining: tokens - cachedTokensAfterUpdate,
|
|
1155
|
+
reset,
|
|
1156
|
+
pending
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
const usedTokensAfterUpdate = await ctx.redis.eval(
|
|
1160
|
+
cachedFixedWindowLimitScript,
|
|
1161
|
+
[key],
|
|
1162
|
+
[windowDuration, incrementBy]
|
|
1163
|
+
);
|
|
1164
|
+
ctx.cache.set(key, usedTokensAfterUpdate);
|
|
1165
|
+
const remaining = tokens - usedTokensAfterUpdate;
|
|
908
1166
|
return {
|
|
909
|
-
success,
|
|
1167
|
+
success: remaining >= 0,
|
|
910
1168
|
limit: tokens,
|
|
911
|
-
remaining
|
|
1169
|
+
remaining,
|
|
912
1170
|
reset,
|
|
913
|
-
pending
|
|
1171
|
+
pending: Promise.resolve()
|
|
914
1172
|
};
|
|
1173
|
+
},
|
|
1174
|
+
async getRemaining(ctx, identifier) {
|
|
1175
|
+
if (!ctx.cache) {
|
|
1176
|
+
throw new Error("This algorithm requires a cache");
|
|
1177
|
+
}
|
|
1178
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
1179
|
+
const key = [identifier, bucket].join(":");
|
|
1180
|
+
const hit = typeof ctx.cache.get(key) === "number";
|
|
1181
|
+
if (hit) {
|
|
1182
|
+
const cachedUsedTokens = ctx.cache.get(key) ?? 0;
|
|
1183
|
+
return Math.max(0, tokens - cachedUsedTokens);
|
|
1184
|
+
}
|
|
1185
|
+
const usedTokens = await ctx.redis.eval(
|
|
1186
|
+
cachedFixedWindowRemainingTokenScript,
|
|
1187
|
+
[key],
|
|
1188
|
+
[null]
|
|
1189
|
+
);
|
|
1190
|
+
return Math.max(0, tokens - usedTokens);
|
|
1191
|
+
},
|
|
1192
|
+
async resetTokens(ctx, identifier) {
|
|
1193
|
+
const pattern = [identifier, "*"].join(":");
|
|
1194
|
+
if (!ctx.cache) {
|
|
1195
|
+
throw new Error("This algorithm requires a cache");
|
|
1196
|
+
}
|
|
1197
|
+
ctx.cache.pop(identifier);
|
|
1198
|
+
await ctx.redis.eval(resetScript, [pattern], [null]);
|
|
915
1199
|
}
|
|
916
|
-
|
|
917
|
-
cachedFixedWindowScript,
|
|
918
|
-
[key],
|
|
919
|
-
[windowDuration, incrementBy]
|
|
920
|
-
);
|
|
921
|
-
ctx.cache.set(key, usedTokensAfterUpdate);
|
|
922
|
-
const remaining = tokens - usedTokensAfterUpdate;
|
|
923
|
-
return {
|
|
924
|
-
success: remaining >= 0,
|
|
925
|
-
limit: tokens,
|
|
926
|
-
remaining,
|
|
927
|
-
reset,
|
|
928
|
-
pending: Promise.resolve()
|
|
929
|
-
};
|
|
930
|
-
};
|
|
1200
|
+
});
|
|
931
1201
|
}
|
|
932
1202
|
};
|
|
933
1203
|
// Annotate the CommonJS export names for ESM import in node:
|