@upstash/ratelimit 1.0.1-canary → 1.0.3
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 +7 -352
- package/dist/index.d.mts +24 -3
- package/dist/index.d.ts +24 -3
- package/dist/index.js +284 -177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +284 -177
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -130,7 +130,7 @@ function ms(d) {
|
|
|
130
130
|
if (!match) {
|
|
131
131
|
throw new Error(`Unable to parse window size: ${d}`);
|
|
132
132
|
}
|
|
133
|
-
const time = parseInt(match[1]);
|
|
133
|
+
const time = Number.parseInt(match[1]);
|
|
134
134
|
const unit = match[2];
|
|
135
135
|
switch (unit) {
|
|
136
136
|
case "ms":
|
|
@@ -148,6 +148,59 @@ function ms(d) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// src/lua-scripts/multi.ts
|
|
152
|
+
var fixedWindowScript = `
|
|
153
|
+
local key = KEYS[1]
|
|
154
|
+
local id = ARGV[1]
|
|
155
|
+
local window = ARGV[2]
|
|
156
|
+
local incrementBy = tonumber(ARGV[3])
|
|
157
|
+
|
|
158
|
+
redis.call("HSET", key, id, incrementBy)
|
|
159
|
+
local fields = redis.call("HGETALL", key)
|
|
160
|
+
if #fields == 1 and tonumber(fields[1])==incrementBy then
|
|
161
|
+
-- The first time this key is set, and the value will be equal to incrementBy.
|
|
162
|
+
-- So we only need the expire command once
|
|
163
|
+
redis.call("PEXPIRE", key, window)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
return fields
|
|
167
|
+
`;
|
|
168
|
+
var slidingWindowScript = `
|
|
169
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
170
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
171
|
+
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
172
|
+
local now = ARGV[2] -- current timestamp in milliseconds
|
|
173
|
+
local window = ARGV[3] -- interval in milliseconds
|
|
174
|
+
local requestId = ARGV[4] -- uuid for this request
|
|
175
|
+
local incrementBy = tonumber(ARGV[5]) -- custom rate, default is 1
|
|
176
|
+
|
|
177
|
+
local currentFields = redis.call("HGETALL", currentKey)
|
|
178
|
+
local requestsInCurrentWindow = 0
|
|
179
|
+
for i = 2, #currentFields, 2 do
|
|
180
|
+
requestsInCurrentWindow = requestsInCurrentWindow + tonumber(currentFields[i])
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
local previousFields = redis.call("HGETALL", previousKey)
|
|
184
|
+
local requestsInPreviousWindow = 0
|
|
185
|
+
for i = 2, #previousFields, 2 do
|
|
186
|
+
requestsInPreviousWindow = requestsInPreviousWindow + tonumber(previousFields[i])
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
local percentageInCurrent = ( now % window) / window
|
|
190
|
+
if requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
|
|
191
|
+
return {currentFields, previousFields, false}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
redis.call("HSET", currentKey, requestId, incrementBy)
|
|
195
|
+
|
|
196
|
+
if requestsInCurrentWindow == 0 then
|
|
197
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
198
|
+
-- So we only need the expire command once
|
|
199
|
+
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
200
|
+
end
|
|
201
|
+
return {currentFields, previousFields, true}
|
|
202
|
+
`;
|
|
203
|
+
|
|
151
204
|
// src/ratelimit.ts
|
|
152
205
|
var Ratelimit = class {
|
|
153
206
|
limiter;
|
|
@@ -188,12 +241,29 @@ var Ratelimit = class {
|
|
|
188
241
|
* }
|
|
189
242
|
* return "Yes"
|
|
190
243
|
* ```
|
|
244
|
+
*
|
|
245
|
+
* @param req.rate - The rate at which tokens will be added or consumed from the token bucket. A higher rate allows for more requests to be processed. Defaults to 1 token per interval if not specified.
|
|
246
|
+
*
|
|
247
|
+
* Usage with `req.rate`
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* const ratelimit = new Ratelimit({
|
|
251
|
+
* redis: Redis.fromEnv(),
|
|
252
|
+
* limiter: Ratelimit.slidingWindow(100, "10 s")
|
|
253
|
+
* })
|
|
254
|
+
*
|
|
255
|
+
* const { success } = await ratelimit.limit(id, {rate: 10})
|
|
256
|
+
* if (!success){
|
|
257
|
+
* return "Nope"
|
|
258
|
+
* }
|
|
259
|
+
* return "Yes"
|
|
260
|
+
* ```
|
|
191
261
|
*/
|
|
192
262
|
limit = async (identifier, req) => {
|
|
193
263
|
const key = [this.prefix, identifier].join(":");
|
|
194
264
|
let timeoutId = null;
|
|
195
265
|
try {
|
|
196
|
-
const arr = [this.limiter(this.ctx, key)];
|
|
266
|
+
const arr = [this.limiter(this.ctx, key, req?.rate)];
|
|
197
267
|
if (this.timeout > 0) {
|
|
198
268
|
arr.push(
|
|
199
269
|
new Promise((resolve) => {
|
|
@@ -325,22 +395,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
325
395
|
*/
|
|
326
396
|
static fixedWindow(tokens, window) {
|
|
327
397
|
const windowDuration = ms(window);
|
|
328
|
-
|
|
329
|
-
local key = KEYS[1]
|
|
330
|
-
local id = ARGV[1]
|
|
331
|
-
local window = ARGV[2]
|
|
332
|
-
|
|
333
|
-
redis.call("SADD", key, id)
|
|
334
|
-
local members = redis.call("SMEMBERS", key)
|
|
335
|
-
if #members == 1 then
|
|
336
|
-
-- The first time this key is set, the value will be 1.
|
|
337
|
-
-- So we only need the expire command once
|
|
338
|
-
redis.call("PEXPIRE", key, window)
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
return members
|
|
342
|
-
`;
|
|
343
|
-
return async function(ctx, identifier) {
|
|
398
|
+
return async (ctx, identifier, rate) => {
|
|
344
399
|
if (ctx.cache) {
|
|
345
400
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
346
401
|
if (blocked) {
|
|
@@ -356,26 +411,60 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
356
411
|
const requestId = randomId();
|
|
357
412
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
358
413
|
const key = [identifier, bucket].join(":");
|
|
414
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
359
415
|
const dbs = ctx.redis.map((redis) => ({
|
|
360
416
|
redis,
|
|
361
|
-
request: redis.eval(
|
|
417
|
+
request: redis.eval(
|
|
418
|
+
fixedWindowScript,
|
|
419
|
+
[key],
|
|
420
|
+
[requestId, windowDuration, incrementBy]
|
|
421
|
+
)
|
|
362
422
|
}));
|
|
363
423
|
const firstResponse = await Promise.any(dbs.map((s) => s.request));
|
|
364
|
-
const usedTokens = firstResponse.
|
|
365
|
-
|
|
424
|
+
const usedTokens = firstResponse.reduce((accTokens, usedToken, index) => {
|
|
425
|
+
let parsedToken = 0;
|
|
426
|
+
if (index % 2) {
|
|
427
|
+
parsedToken = Number.parseInt(usedToken);
|
|
428
|
+
}
|
|
429
|
+
return accTokens + parsedToken;
|
|
430
|
+
}, 0);
|
|
431
|
+
const remaining = tokens - usedTokens;
|
|
366
432
|
async function sync() {
|
|
367
433
|
const individualIDs = await Promise.all(dbs.map((s) => s.request));
|
|
368
|
-
const allIDs = Array.from(
|
|
434
|
+
const allIDs = Array.from(
|
|
435
|
+
new Set(
|
|
436
|
+
individualIDs.flatMap((_) => _).reduce((acc, curr, index) => {
|
|
437
|
+
if (index % 2 === 0) {
|
|
438
|
+
acc.push(curr);
|
|
439
|
+
}
|
|
440
|
+
return acc;
|
|
441
|
+
}, [])
|
|
442
|
+
).values()
|
|
443
|
+
);
|
|
369
444
|
for (const db of dbs) {
|
|
370
|
-
const
|
|
371
|
-
|
|
445
|
+
const usedDbTokens = (await db.request).reduce((accTokens, usedToken, index) => {
|
|
446
|
+
let parsedToken = 0;
|
|
447
|
+
if (index % 2) {
|
|
448
|
+
parsedToken = Number.parseInt(usedToken);
|
|
449
|
+
}
|
|
450
|
+
return accTokens + parsedToken;
|
|
451
|
+
}, 0);
|
|
452
|
+
const dbIds = (await db.request).reduce((ids, currentId, index) => {
|
|
453
|
+
if (index % 2 === 0) {
|
|
454
|
+
ids.push(currentId);
|
|
455
|
+
}
|
|
456
|
+
return ids;
|
|
457
|
+
}, []);
|
|
458
|
+
if (usedDbTokens >= tokens) {
|
|
372
459
|
continue;
|
|
373
460
|
}
|
|
374
|
-
const diff = allIDs.filter((id) => !
|
|
461
|
+
const diff = allIDs.filter((id) => !dbIds.includes(id));
|
|
375
462
|
if (diff.length === 0) {
|
|
376
463
|
continue;
|
|
377
464
|
}
|
|
378
|
-
|
|
465
|
+
for (const requestId2 of diff) {
|
|
466
|
+
await db.redis.hset(key, { [requestId2]: incrementBy });
|
|
467
|
+
}
|
|
379
468
|
}
|
|
380
469
|
}
|
|
381
470
|
const success = remaining > 0;
|
|
@@ -410,69 +499,76 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
410
499
|
*/
|
|
411
500
|
static slidingWindow(tokens, window) {
|
|
412
501
|
const windowSize = ms(window);
|
|
413
|
-
const script = `
|
|
414
|
-
local currentKey = KEYS[1] -- identifier including prefixes
|
|
415
|
-
local previousKey = KEYS[2] -- key of the previous bucket
|
|
416
|
-
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
417
|
-
local now = ARGV[2] -- current timestamp in milliseconds
|
|
418
|
-
local window = ARGV[3] -- interval in milliseconds
|
|
419
|
-
local requestId = ARGV[4] -- uuid for this request
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
local currentMembers = redis.call("SMEMBERS", currentKey)
|
|
423
|
-
local requestsInCurrentWindow = #currentMembers
|
|
424
|
-
local previousMembers = redis.call("SMEMBERS", previousKey)
|
|
425
|
-
local requestsInPreviousWindow = #previousMembers
|
|
426
|
-
|
|
427
|
-
local percentageInCurrent = ( now % window) / window
|
|
428
|
-
if requestsInPreviousWindow * ( 1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then
|
|
429
|
-
return {currentMembers, previousMembers, false}
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
redis.call("SADD", currentKey, requestId)
|
|
433
|
-
table.insert(currentMembers, requestId)
|
|
434
|
-
if requestsInCurrentWindow == 0 then
|
|
435
|
-
-- The first time this key is set, the value will be 1.
|
|
436
|
-
-- So we only need the expire command once
|
|
437
|
-
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
438
|
-
end
|
|
439
|
-
return {currentMembers, previousMembers, true}
|
|
440
|
-
`;
|
|
441
502
|
const windowDuration = ms(window);
|
|
442
|
-
return async
|
|
503
|
+
return async (ctx, identifier, rate) => {
|
|
443
504
|
const requestId = randomId();
|
|
444
505
|
const now = Date.now();
|
|
445
506
|
const currentWindow = Math.floor(now / windowSize);
|
|
446
507
|
const currentKey = [identifier, currentWindow].join(":");
|
|
447
508
|
const previousWindow = currentWindow - 1;
|
|
448
509
|
const previousKey = [identifier, previousWindow].join(":");
|
|
510
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
449
511
|
const dbs = ctx.redis.map((redis) => ({
|
|
450
512
|
redis,
|
|
451
513
|
request: redis.eval(
|
|
452
|
-
|
|
514
|
+
slidingWindowScript,
|
|
453
515
|
[currentKey, previousKey],
|
|
454
|
-
[tokens, now, windowDuration, requestId]
|
|
516
|
+
[tokens, now, windowDuration, requestId, incrementBy]
|
|
455
517
|
// lua seems to return `1` for true and `null` for false
|
|
456
518
|
)
|
|
457
519
|
}));
|
|
458
520
|
const percentageInCurrent = now % windowDuration / windowDuration;
|
|
459
521
|
const [current, previous, success] = await Promise.any(dbs.map((s) => s.request));
|
|
460
|
-
const
|
|
461
|
-
|
|
522
|
+
const previousUsedTokens = previous.reduce((accTokens, usedToken, index) => {
|
|
523
|
+
let parsedToken = 0;
|
|
524
|
+
if (index % 2) {
|
|
525
|
+
parsedToken = Number.parseInt(usedToken);
|
|
526
|
+
}
|
|
527
|
+
return accTokens + parsedToken;
|
|
528
|
+
}, 0);
|
|
529
|
+
const currentUsedTokens = current.reduce((accTokens, usedToken, index) => {
|
|
530
|
+
let parsedToken = 0;
|
|
531
|
+
if (index % 2) {
|
|
532
|
+
parsedToken = Number.parseInt(usedToken);
|
|
533
|
+
}
|
|
534
|
+
return accTokens + parsedToken;
|
|
535
|
+
}, 0);
|
|
536
|
+
const previousPartialUsed = previousUsedTokens * (1 - percentageInCurrent);
|
|
537
|
+
const usedTokens = previousPartialUsed + currentUsedTokens;
|
|
462
538
|
const remaining = tokens - usedTokens;
|
|
463
539
|
async function sync() {
|
|
464
540
|
const res = await Promise.all(dbs.map((s) => s.request));
|
|
465
|
-
const allCurrentIds = res.flatMap(([current2]) => current2)
|
|
541
|
+
const allCurrentIds = res.flatMap(([current2]) => current2).reduce((accCurrentIds, curr, index) => {
|
|
542
|
+
if (index % 2 === 0) {
|
|
543
|
+
accCurrentIds.push(curr);
|
|
544
|
+
}
|
|
545
|
+
return accCurrentIds;
|
|
546
|
+
}, []);
|
|
466
547
|
for (const db of dbs) {
|
|
467
|
-
const [
|
|
468
|
-
|
|
548
|
+
const [_current, previous2, _success] = await db.request;
|
|
549
|
+
const dbIds = previous2.reduce((ids, currentId, index) => {
|
|
550
|
+
if (index % 2 === 0) {
|
|
551
|
+
ids.push(currentId);
|
|
552
|
+
}
|
|
553
|
+
return ids;
|
|
554
|
+
}, []);
|
|
555
|
+
const usedDbTokens = previous2.reduce((accTokens, usedToken, index) => {
|
|
556
|
+
let parsedToken = 0;
|
|
557
|
+
if (index % 2) {
|
|
558
|
+
parsedToken = Number.parseInt(usedToken);
|
|
559
|
+
}
|
|
560
|
+
return accTokens + parsedToken;
|
|
561
|
+
}, 0);
|
|
562
|
+
if (usedDbTokens >= tokens) {
|
|
469
563
|
continue;
|
|
470
564
|
}
|
|
471
|
-
const diff = allCurrentIds.filter((id) => !
|
|
565
|
+
const diff = allCurrentIds.filter((id) => !dbIds.includes(id));
|
|
472
566
|
if (diff.length === 0) {
|
|
473
567
|
continue;
|
|
474
568
|
}
|
|
475
|
-
|
|
569
|
+
for (const requestId2 of diff) {
|
|
570
|
+
await db.redis.hset(currentKey, { [requestId2]: incrementBy });
|
|
571
|
+
}
|
|
476
572
|
}
|
|
477
573
|
}
|
|
478
574
|
const reset = (currentWindow + 1) * windowDuration;
|
|
@@ -482,7 +578,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
482
578
|
return {
|
|
483
579
|
success: Boolean(success),
|
|
484
580
|
limit: tokens,
|
|
485
|
-
remaining,
|
|
581
|
+
remaining: Math.max(0, remaining),
|
|
486
582
|
reset,
|
|
487
583
|
pending: sync()
|
|
488
584
|
};
|
|
@@ -490,6 +586,107 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
490
586
|
}
|
|
491
587
|
};
|
|
492
588
|
|
|
589
|
+
// src/lua-scripts/single.ts
|
|
590
|
+
var fixedWindowScript2 = `
|
|
591
|
+
local key = KEYS[1]
|
|
592
|
+
local window = ARGV[1]
|
|
593
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
594
|
+
|
|
595
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
596
|
+
if r == incrementBy then
|
|
597
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
598
|
+
-- So we only need the expire command once
|
|
599
|
+
redis.call("PEXPIRE", key, window)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
return r
|
|
603
|
+
`;
|
|
604
|
+
var slidingWindowScript2 = `
|
|
605
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
606
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
607
|
+
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
608
|
+
local now = ARGV[2] -- current timestamp in milliseconds
|
|
609
|
+
local window = ARGV[3] -- interval in milliseconds
|
|
610
|
+
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
|
|
611
|
+
|
|
612
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
613
|
+
if requestsInCurrentWindow == false then
|
|
614
|
+
requestsInCurrentWindow = 0
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
618
|
+
if requestsInPreviousWindow == false then
|
|
619
|
+
requestsInPreviousWindow = 0
|
|
620
|
+
end
|
|
621
|
+
local percentageInCurrent = ( now % window ) / window
|
|
622
|
+
-- weighted requests to consider from the previous window
|
|
623
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
624
|
+
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
|
|
625
|
+
return -1
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
629
|
+
if newValue == incrementBy then
|
|
630
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
631
|
+
-- So we only need the expire command once
|
|
632
|
+
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
633
|
+
end
|
|
634
|
+
return tokens - ( newValue + requestsInPreviousWindow )
|
|
635
|
+
`;
|
|
636
|
+
var tokenBucketScript = `
|
|
637
|
+
local key = KEYS[1] -- identifier including prefixes
|
|
638
|
+
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
639
|
+
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
640
|
+
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
641
|
+
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
642
|
+
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
643
|
+
|
|
644
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
645
|
+
|
|
646
|
+
local refilledAt
|
|
647
|
+
local tokens
|
|
648
|
+
|
|
649
|
+
if bucket[1] == false then
|
|
650
|
+
refilledAt = now
|
|
651
|
+
tokens = maxTokens
|
|
652
|
+
else
|
|
653
|
+
refilledAt = tonumber(bucket[1])
|
|
654
|
+
tokens = tonumber(bucket[2])
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
if now >= refilledAt + interval then
|
|
658
|
+
local numRefills = math.floor((now - refilledAt) / interval)
|
|
659
|
+
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
|
|
660
|
+
|
|
661
|
+
refilledAt = refilledAt + numRefills * interval
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
if tokens == 0 then
|
|
665
|
+
return {-1, refilledAt + interval}
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
local remaining = tokens - incrementBy
|
|
669
|
+
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
|
|
670
|
+
|
|
671
|
+
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
672
|
+
redis.call("PEXPIRE", key, expireAt)
|
|
673
|
+
return {remaining, refilledAt + interval}
|
|
674
|
+
`;
|
|
675
|
+
var cachedFixedWindowScript = `
|
|
676
|
+
local key = KEYS[1]
|
|
677
|
+
local window = ARGV[1]
|
|
678
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
679
|
+
|
|
680
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
681
|
+
if r == incrementBy then
|
|
682
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
683
|
+
-- So we only need the expire command once
|
|
684
|
+
redis.call("PEXPIRE", key, window)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
return r
|
|
688
|
+
`;
|
|
689
|
+
|
|
493
690
|
// src/single.ts
|
|
494
691
|
var RegionRatelimit = class extends Ratelimit {
|
|
495
692
|
/**
|
|
@@ -527,20 +724,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
527
724
|
*/
|
|
528
725
|
static fixedWindow(tokens, window) {
|
|
529
726
|
const windowDuration = ms(window);
|
|
530
|
-
|
|
531
|
-
local key = KEYS[1]
|
|
532
|
-
local window = ARGV[1]
|
|
533
|
-
|
|
534
|
-
local r = redis.call("INCR", key)
|
|
535
|
-
if r == 1 then
|
|
536
|
-
-- The first time this key is set, the value will be 1.
|
|
537
|
-
-- So we only need the expire command once
|
|
538
|
-
redis.call("PEXPIRE", key, window)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
return r
|
|
542
|
-
`;
|
|
543
|
-
return async function(ctx, identifier) {
|
|
727
|
+
return async (ctx, identifier, rate) => {
|
|
544
728
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
545
729
|
const key = [identifier, bucket].join(":");
|
|
546
730
|
if (ctx.cache) {
|
|
@@ -555,12 +739,14 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
555
739
|
};
|
|
556
740
|
}
|
|
557
741
|
}
|
|
742
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
558
743
|
const usedTokensAfterUpdate = await ctx.redis.eval(
|
|
559
|
-
|
|
744
|
+
fixedWindowScript2,
|
|
560
745
|
[key],
|
|
561
|
-
[windowDuration]
|
|
746
|
+
[windowDuration, incrementBy]
|
|
562
747
|
);
|
|
563
748
|
const success = usedTokensAfterUpdate <= tokens;
|
|
749
|
+
const remainingTokens = Math.max(0, tokens - usedTokensAfterUpdate);
|
|
564
750
|
const reset = (bucket + 1) * windowDuration;
|
|
565
751
|
if (ctx.cache && !success) {
|
|
566
752
|
ctx.cache.blockUntil(identifier, reset);
|
|
@@ -568,7 +754,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
568
754
|
return {
|
|
569
755
|
success,
|
|
570
756
|
limit: tokens,
|
|
571
|
-
remaining:
|
|
757
|
+
remaining: remainingTokens,
|
|
572
758
|
reset,
|
|
573
759
|
pending: Promise.resolve()
|
|
574
760
|
};
|
|
@@ -591,39 +777,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
591
777
|
* @param window - The duration in which the user can max X requests.
|
|
592
778
|
*/
|
|
593
779
|
static slidingWindow(tokens, window) {
|
|
594
|
-
const script = `
|
|
595
|
-
local currentKey = KEYS[1] -- identifier including prefixes
|
|
596
|
-
local previousKey = KEYS[2] -- key of the previous bucket
|
|
597
|
-
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
598
|
-
local now = ARGV[2] -- current timestamp in milliseconds
|
|
599
|
-
local window = ARGV[3] -- interval in milliseconds
|
|
600
|
-
|
|
601
|
-
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
602
|
-
if requestsInCurrentWindow == false then
|
|
603
|
-
requestsInCurrentWindow = 0
|
|
604
|
-
end
|
|
605
|
-
|
|
606
|
-
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
607
|
-
if requestsInPreviousWindow == false then
|
|
608
|
-
requestsInPreviousWindow = 0
|
|
609
|
-
end
|
|
610
|
-
local percentageInCurrent = ( now % window ) / window
|
|
611
|
-
-- weighted requests to consider from the previous window
|
|
612
|
-
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
613
|
-
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
|
|
614
|
-
return -1
|
|
615
|
-
end
|
|
616
|
-
|
|
617
|
-
local newValue = redis.call("INCR", currentKey)
|
|
618
|
-
if newValue == 1 then
|
|
619
|
-
-- The first time this key is set, the value will be 1.
|
|
620
|
-
-- So we only need the expire command once
|
|
621
|
-
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
622
|
-
end
|
|
623
|
-
return tokens - ( newValue + requestsInPreviousWindow )
|
|
624
|
-
`;
|
|
625
780
|
const windowSize = ms(window);
|
|
626
|
-
return async
|
|
781
|
+
return async (ctx, identifier, rate) => {
|
|
627
782
|
const now = Date.now();
|
|
628
783
|
const currentWindow = Math.floor(now / windowSize);
|
|
629
784
|
const currentKey = [identifier, currentWindow].join(":");
|
|
@@ -641,12 +796,13 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
641
796
|
};
|
|
642
797
|
}
|
|
643
798
|
}
|
|
644
|
-
const
|
|
645
|
-
|
|
799
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
800
|
+
const remainingTokens = await ctx.redis.eval(
|
|
801
|
+
slidingWindowScript2,
|
|
646
802
|
[currentKey, previousKey],
|
|
647
|
-
[tokens, now, windowSize]
|
|
803
|
+
[tokens, now, windowSize, incrementBy]
|
|
648
804
|
);
|
|
649
|
-
const success =
|
|
805
|
+
const success = remainingTokens >= 0;
|
|
650
806
|
const reset = (currentWindow + 1) * windowSize;
|
|
651
807
|
if (ctx.cache && !success) {
|
|
652
808
|
ctx.cache.blockUntil(identifier, reset);
|
|
@@ -654,7 +810,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
654
810
|
return {
|
|
655
811
|
success,
|
|
656
812
|
limit: tokens,
|
|
657
|
-
remaining: Math.max(0,
|
|
813
|
+
remaining: Math.max(0, remainingTokens),
|
|
658
814
|
reset,
|
|
659
815
|
pending: Promise.resolve()
|
|
660
816
|
};
|
|
@@ -674,46 +830,8 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
674
830
|
* than `refillRate`
|
|
675
831
|
*/
|
|
676
832
|
static tokenBucket(refillRate, interval, maxTokens) {
|
|
677
|
-
const script = `
|
|
678
|
-
local key = KEYS[1] -- identifier including prefixes
|
|
679
|
-
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
680
|
-
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
681
|
-
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
682
|
-
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
683
|
-
|
|
684
|
-
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
685
|
-
|
|
686
|
-
local refilledAt
|
|
687
|
-
local tokens
|
|
688
|
-
|
|
689
|
-
if bucket[1] == false then
|
|
690
|
-
refilledAt = now
|
|
691
|
-
tokens = maxTokens
|
|
692
|
-
else
|
|
693
|
-
refilledAt = tonumber(bucket[1])
|
|
694
|
-
tokens = tonumber(bucket[2])
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
if now >= refilledAt + interval then
|
|
698
|
-
local numRefills = math.floor((now - refilledAt) / interval)
|
|
699
|
-
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
|
|
700
|
-
|
|
701
|
-
refilledAt = refilledAt + numRefills * interval
|
|
702
|
-
end
|
|
703
|
-
|
|
704
|
-
if tokens == 0 then
|
|
705
|
-
return {-1, refilledAt + interval}
|
|
706
|
-
end
|
|
707
|
-
|
|
708
|
-
local remaining = tokens - 1
|
|
709
|
-
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
|
|
710
|
-
|
|
711
|
-
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
712
|
-
redis.call("PEXPIRE", key, expireAt)
|
|
713
|
-
return {remaining, refilledAt + interval}
|
|
714
|
-
`;
|
|
715
833
|
const intervalDuration = ms(interval);
|
|
716
|
-
return async
|
|
834
|
+
return async (ctx, identifier, rate) => {
|
|
717
835
|
if (ctx.cache) {
|
|
718
836
|
const { blocked, reset: reset2 } = ctx.cache.isBlocked(identifier);
|
|
719
837
|
if (blocked) {
|
|
@@ -727,10 +845,11 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
727
845
|
}
|
|
728
846
|
}
|
|
729
847
|
const now = Date.now();
|
|
848
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
730
849
|
const [remaining, reset] = await ctx.redis.eval(
|
|
731
|
-
|
|
850
|
+
tokenBucketScript,
|
|
732
851
|
[identifier],
|
|
733
|
-
[maxTokens, intervalDuration, refillRate, now]
|
|
852
|
+
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
734
853
|
);
|
|
735
854
|
const success = remaining >= 0;
|
|
736
855
|
if (ctx.cache && !success) {
|
|
@@ -771,31 +890,19 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
771
890
|
*/
|
|
772
891
|
static cachedFixedWindow(tokens, window) {
|
|
773
892
|
const windowDuration = ms(window);
|
|
774
|
-
|
|
775
|
-
local key = KEYS[1]
|
|
776
|
-
local window = ARGV[1]
|
|
777
|
-
|
|
778
|
-
local r = redis.call("INCR", key)
|
|
779
|
-
if r == 1 then
|
|
780
|
-
-- The first time this key is set, the value will be 1.
|
|
781
|
-
-- So we only need the expire command once
|
|
782
|
-
redis.call("PEXPIRE", key, window)
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
return r
|
|
786
|
-
`;
|
|
787
|
-
return async function(ctx, identifier) {
|
|
893
|
+
return async (ctx, identifier, rate) => {
|
|
788
894
|
if (!ctx.cache) {
|
|
789
895
|
throw new Error("This algorithm requires a cache");
|
|
790
896
|
}
|
|
791
897
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
792
898
|
const key = [identifier, bucket].join(":");
|
|
793
899
|
const reset = (bucket + 1) * windowDuration;
|
|
900
|
+
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
794
901
|
const hit = typeof ctx.cache.get(key) === "number";
|
|
795
902
|
if (hit) {
|
|
796
903
|
const cachedTokensAfterUpdate = ctx.cache.incr(key);
|
|
797
904
|
const success = cachedTokensAfterUpdate < tokens;
|
|
798
|
-
const pending = success ? ctx.redis.eval(
|
|
905
|
+
const pending = success ? ctx.redis.eval(cachedFixedWindowScript, [key], [windowDuration, incrementBy]).then((t) => {
|
|
799
906
|
ctx.cache.set(key, t);
|
|
800
907
|
}) : Promise.resolve();
|
|
801
908
|
return {
|
|
@@ -807,9 +914,9 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
807
914
|
};
|
|
808
915
|
}
|
|
809
916
|
const usedTokensAfterUpdate = await ctx.redis.eval(
|
|
810
|
-
|
|
917
|
+
cachedFixedWindowScript,
|
|
811
918
|
[key],
|
|
812
|
-
[windowDuration]
|
|
919
|
+
[windowDuration, incrementBy]
|
|
813
920
|
);
|
|
814
921
|
ctx.cache.set(key, usedTokensAfterUpdate);
|
|
815
922
|
const remaining = tokens - usedTokensAfterUpdate;
|