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