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