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