@upstash/ratelimit 2.0.2 → 2.0.4
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/dist/index.d.mts +18 -23
- package/dist/index.d.ts +18 -23
- package/dist/index.js +307 -272
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -272
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -49,10 +49,10 @@ var Analytics = class {
|
|
|
49
49
|
* @returns
|
|
50
50
|
*/
|
|
51
51
|
extractGeo(req) {
|
|
52
|
-
if (
|
|
52
|
+
if (req.geo !== void 0) {
|
|
53
53
|
return req.geo;
|
|
54
54
|
}
|
|
55
|
-
if (
|
|
55
|
+
if (req.cf !== void 0) {
|
|
56
56
|
return req.cf;
|
|
57
57
|
}
|
|
58
58
|
return {};
|
|
@@ -141,54 +141,203 @@ function ms(d) {
|
|
|
141
141
|
const time = Number.parseInt(match[1]);
|
|
142
142
|
const unit = match[2];
|
|
143
143
|
switch (unit) {
|
|
144
|
-
case "ms":
|
|
144
|
+
case "ms": {
|
|
145
145
|
return time;
|
|
146
|
-
|
|
146
|
+
}
|
|
147
|
+
case "s": {
|
|
147
148
|
return time * 1e3;
|
|
148
|
-
|
|
149
|
+
}
|
|
150
|
+
case "m": {
|
|
149
151
|
return time * 1e3 * 60;
|
|
150
|
-
|
|
152
|
+
}
|
|
153
|
+
case "h": {
|
|
151
154
|
return time * 1e3 * 60 * 60;
|
|
152
|
-
|
|
155
|
+
}
|
|
156
|
+
case "d": {
|
|
153
157
|
return time * 1e3 * 60 * 60 * 24;
|
|
154
|
-
|
|
158
|
+
}
|
|
159
|
+
default: {
|
|
155
160
|
throw new Error(`Unable to parse window size: ${d}`);
|
|
161
|
+
}
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
// src/hash.ts
|
|
160
|
-
var
|
|
161
|
-
const regionContexts = "redis" in ctx ? [ctx] : ctx.regionContexts;
|
|
162
|
-
const hashSample = regionContexts[0].scriptHashes[kind];
|
|
163
|
-
if (!hashSample) {
|
|
164
|
-
await Promise.all(regionContexts.map(async (context) => {
|
|
165
|
-
context.scriptHashes[kind] = await context.redis.scriptLoad(script);
|
|
166
|
-
}));
|
|
167
|
-
}
|
|
168
|
-
;
|
|
169
|
-
};
|
|
170
|
-
var safeEval = async (ctx, script, kind, keys, args) => {
|
|
171
|
-
if (!ctx.cacheScripts) {
|
|
172
|
-
return await ctx.redis.eval(script, keys, args);
|
|
173
|
-
}
|
|
174
|
-
;
|
|
175
|
-
await setHash(ctx, script, kind);
|
|
166
|
+
var safeEval = async (ctx, script, keys, args) => {
|
|
176
167
|
try {
|
|
177
|
-
return await ctx.redis.evalsha(
|
|
168
|
+
return await ctx.redis.evalsha(script.hash, keys, args);
|
|
178
169
|
} catch (error) {
|
|
179
170
|
if (`${error}`.includes("NOSCRIPT")) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
171
|
+
const hash = await ctx.redis.scriptLoad(script.script);
|
|
172
|
+
if (hash !== script.hash) {
|
|
173
|
+
console.warn(
|
|
174
|
+
"Upstash Ratelimit: Expected hash and the hash received from Redis are different. Ratelimit will work as usual but performance will be reduced."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return await ctx.redis.evalsha(hash, keys, args);
|
|
185
178
|
}
|
|
186
179
|
throw error;
|
|
187
180
|
}
|
|
188
181
|
};
|
|
189
182
|
|
|
190
|
-
// src/lua-scripts/
|
|
183
|
+
// src/lua-scripts/single.ts
|
|
191
184
|
var fixedWindowLimitScript = `
|
|
185
|
+
local key = KEYS[1]
|
|
186
|
+
local window = ARGV[1]
|
|
187
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
188
|
+
|
|
189
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
190
|
+
if r == tonumber(incrementBy) then
|
|
191
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
192
|
+
-- So we only need the expire command once
|
|
193
|
+
redis.call("PEXPIRE", key, window)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
return r
|
|
197
|
+
`;
|
|
198
|
+
var fixedWindowRemainingTokensScript = `
|
|
199
|
+
local key = KEYS[1]
|
|
200
|
+
local tokens = 0
|
|
201
|
+
|
|
202
|
+
local value = redis.call('GET', key)
|
|
203
|
+
if value then
|
|
204
|
+
tokens = value
|
|
205
|
+
end
|
|
206
|
+
return tokens
|
|
207
|
+
`;
|
|
208
|
+
var slidingWindowLimitScript = `
|
|
209
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
210
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
211
|
+
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
212
|
+
local now = ARGV[2] -- current timestamp in milliseconds
|
|
213
|
+
local window = ARGV[3] -- interval in milliseconds
|
|
214
|
+
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
|
|
215
|
+
|
|
216
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
217
|
+
if requestsInCurrentWindow == false then
|
|
218
|
+
requestsInCurrentWindow = 0
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
222
|
+
if requestsInPreviousWindow == false then
|
|
223
|
+
requestsInPreviousWindow = 0
|
|
224
|
+
end
|
|
225
|
+
local percentageInCurrent = ( now % window ) / window
|
|
226
|
+
-- weighted requests to consider from the previous window
|
|
227
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
228
|
+
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
|
|
229
|
+
return -1
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
233
|
+
if newValue == tonumber(incrementBy) then
|
|
234
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
235
|
+
-- So we only need the expire command once
|
|
236
|
+
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
237
|
+
end
|
|
238
|
+
return tokens - ( newValue + requestsInPreviousWindow )
|
|
239
|
+
`;
|
|
240
|
+
var slidingWindowRemainingTokensScript = `
|
|
241
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
242
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
243
|
+
local now = ARGV[1] -- current timestamp in milliseconds
|
|
244
|
+
local window = ARGV[2] -- interval in milliseconds
|
|
245
|
+
|
|
246
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
247
|
+
if requestsInCurrentWindow == false then
|
|
248
|
+
requestsInCurrentWindow = 0
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
252
|
+
if requestsInPreviousWindow == false then
|
|
253
|
+
requestsInPreviousWindow = 0
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
local percentageInCurrent = ( now % window ) / window
|
|
257
|
+
-- weighted requests to consider from the previous window
|
|
258
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
259
|
+
|
|
260
|
+
return requestsInPreviousWindow + requestsInCurrentWindow
|
|
261
|
+
`;
|
|
262
|
+
var tokenBucketLimitScript = `
|
|
263
|
+
local key = KEYS[1] -- identifier including prefixes
|
|
264
|
+
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
265
|
+
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
266
|
+
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
267
|
+
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
268
|
+
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
269
|
+
|
|
270
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
271
|
+
|
|
272
|
+
local refilledAt
|
|
273
|
+
local tokens
|
|
274
|
+
|
|
275
|
+
if bucket[1] == false then
|
|
276
|
+
refilledAt = now
|
|
277
|
+
tokens = maxTokens
|
|
278
|
+
else
|
|
279
|
+
refilledAt = tonumber(bucket[1])
|
|
280
|
+
tokens = tonumber(bucket[2])
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
if now >= refilledAt + interval then
|
|
284
|
+
local numRefills = math.floor((now - refilledAt) / interval)
|
|
285
|
+
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
|
|
286
|
+
|
|
287
|
+
refilledAt = refilledAt + numRefills * interval
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
if tokens == 0 then
|
|
291
|
+
return {-1, refilledAt + interval}
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
local remaining = tokens - incrementBy
|
|
295
|
+
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
|
|
296
|
+
|
|
297
|
+
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
298
|
+
redis.call("PEXPIRE", key, expireAt)
|
|
299
|
+
return {remaining, refilledAt + interval}
|
|
300
|
+
`;
|
|
301
|
+
var tokenBucketIdentifierNotFound = -1;
|
|
302
|
+
var tokenBucketRemainingTokensScript = `
|
|
303
|
+
local key = KEYS[1]
|
|
304
|
+
local maxTokens = tonumber(ARGV[1])
|
|
305
|
+
|
|
306
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
307
|
+
|
|
308
|
+
if bucket[1] == false then
|
|
309
|
+
return {maxTokens, ${tokenBucketIdentifierNotFound}}
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
313
|
+
`;
|
|
314
|
+
var cachedFixedWindowLimitScript = `
|
|
315
|
+
local key = KEYS[1]
|
|
316
|
+
local window = ARGV[1]
|
|
317
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
318
|
+
|
|
319
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
320
|
+
if r == incrementBy then
|
|
321
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
322
|
+
-- So we only need the expire command once
|
|
323
|
+
redis.call("PEXPIRE", key, window)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
return r
|
|
327
|
+
`;
|
|
328
|
+
var cachedFixedWindowRemainingTokenScript = `
|
|
329
|
+
local key = KEYS[1]
|
|
330
|
+
local tokens = 0
|
|
331
|
+
|
|
332
|
+
local value = redis.call('GET', key)
|
|
333
|
+
if value then
|
|
334
|
+
tokens = value
|
|
335
|
+
end
|
|
336
|
+
return tokens
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
// src/lua-scripts/multi.ts
|
|
340
|
+
var fixedWindowLimitScript2 = `
|
|
192
341
|
local key = KEYS[1]
|
|
193
342
|
local id = ARGV[1]
|
|
194
343
|
local window = ARGV[2]
|
|
@@ -204,7 +353,7 @@ var fixedWindowLimitScript = `
|
|
|
204
353
|
|
|
205
354
|
return fields
|
|
206
355
|
`;
|
|
207
|
-
var
|
|
356
|
+
var fixedWindowRemainingTokensScript2 = `
|
|
208
357
|
local key = KEYS[1]
|
|
209
358
|
local tokens = 0
|
|
210
359
|
|
|
@@ -212,7 +361,7 @@ var fixedWindowRemainingTokensScript = `
|
|
|
212
361
|
|
|
213
362
|
return fields
|
|
214
363
|
`;
|
|
215
|
-
var
|
|
364
|
+
var slidingWindowLimitScript2 = `
|
|
216
365
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
217
366
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
218
367
|
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
@@ -247,7 +396,7 @@ var slidingWindowLimitScript = `
|
|
|
247
396
|
end
|
|
248
397
|
return {currentFields, previousFields, true}
|
|
249
398
|
`;
|
|
250
|
-
var
|
|
399
|
+
var slidingWindowRemainingTokensScript2 = `
|
|
251
400
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
252
401
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
253
402
|
local now = ARGV[1] -- current timestamp in milliseconds
|
|
@@ -296,6 +445,78 @@ var resetScript = `
|
|
|
296
445
|
until cursor == "0"
|
|
297
446
|
`;
|
|
298
447
|
|
|
448
|
+
// src/lua-scripts/hash.ts
|
|
449
|
+
var SCRIPTS = {
|
|
450
|
+
singleRegion: {
|
|
451
|
+
fixedWindow: {
|
|
452
|
+
limit: {
|
|
453
|
+
script: fixedWindowLimitScript,
|
|
454
|
+
hash: "b13943e359636db027ad280f1def143f02158c13"
|
|
455
|
+
},
|
|
456
|
+
getRemaining: {
|
|
457
|
+
script: fixedWindowRemainingTokensScript,
|
|
458
|
+
hash: "8c4c341934502aee132643ffbe58ead3450e5208"
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
slidingWindow: {
|
|
462
|
+
limit: {
|
|
463
|
+
script: slidingWindowLimitScript,
|
|
464
|
+
hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
|
|
465
|
+
},
|
|
466
|
+
getRemaining: {
|
|
467
|
+
script: slidingWindowRemainingTokensScript,
|
|
468
|
+
hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
tokenBucket: {
|
|
472
|
+
limit: {
|
|
473
|
+
script: tokenBucketLimitScript,
|
|
474
|
+
hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
|
|
475
|
+
},
|
|
476
|
+
getRemaining: {
|
|
477
|
+
script: tokenBucketRemainingTokensScript,
|
|
478
|
+
hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
cachedFixedWindow: {
|
|
482
|
+
limit: {
|
|
483
|
+
script: cachedFixedWindowLimitScript,
|
|
484
|
+
hash: "c26b12703dd137939b9a69a3a9b18e906a2d940f"
|
|
485
|
+
},
|
|
486
|
+
getRemaining: {
|
|
487
|
+
script: cachedFixedWindowRemainingTokenScript,
|
|
488
|
+
hash: "8e8f222ccae68b595ee6e3f3bf2199629a62b91a"
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
multiRegion: {
|
|
493
|
+
fixedWindow: {
|
|
494
|
+
limit: {
|
|
495
|
+
script: fixedWindowLimitScript2,
|
|
496
|
+
hash: "a8c14f3835aa87bd70e5e2116081b81664abcf5c"
|
|
497
|
+
},
|
|
498
|
+
getRemaining: {
|
|
499
|
+
script: fixedWindowRemainingTokensScript2,
|
|
500
|
+
hash: "8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd"
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
slidingWindow: {
|
|
504
|
+
limit: {
|
|
505
|
+
script: slidingWindowLimitScript2,
|
|
506
|
+
hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
|
|
507
|
+
},
|
|
508
|
+
getRemaining: {
|
|
509
|
+
script: slidingWindowRemainingTokensScript2,
|
|
510
|
+
hash: "558c9306b7ec54abb50747fe0b17e5d44bd24868"
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
var RESET_SCRIPT = {
|
|
516
|
+
script: resetScript,
|
|
517
|
+
hash: "54bd274ddc59fb3be0f42deee2f64322a10e2b50"
|
|
518
|
+
};
|
|
519
|
+
|
|
299
520
|
// src/types.ts
|
|
300
521
|
var DenyListExtension = "denyList";
|
|
301
522
|
var IpDenyListKey = "ipDenyList";
|
|
@@ -379,7 +600,7 @@ var updateIpDenyList = async (redis, prefix, threshold, ttl) => {
|
|
|
379
600
|
const transaction = redis.multi();
|
|
380
601
|
transaction.sdiffstore(allDenyLists, allDenyLists, ipDenyList);
|
|
381
602
|
transaction.del(ipDenyList);
|
|
382
|
-
transaction.sadd(ipDenyList, ...allIps);
|
|
603
|
+
transaction.sadd(ipDenyList, allIps.at(0), ...allIps.slice(1));
|
|
383
604
|
transaction.sdiffstore(ipDenyList, ipDenyList, allDenyLists);
|
|
384
605
|
transaction.sunionstore(allDenyLists, allDenyLists, ipDenyList);
|
|
385
606
|
transaction.set(statusKey, "valid", { px: ttl ?? getIpListTTL() });
|
|
@@ -481,7 +702,7 @@ var Ratelimit = class {
|
|
|
481
702
|
}) : void 0;
|
|
482
703
|
if (config.ephemeralCache instanceof Map) {
|
|
483
704
|
this.ctx.cache = new Cache(config.ephemeralCache);
|
|
484
|
-
} else if (
|
|
705
|
+
} else if (config.ephemeralCache === void 0) {
|
|
485
706
|
this.ctx.cache = new Cache(/* @__PURE__ */ new Map());
|
|
486
707
|
}
|
|
487
708
|
}
|
|
@@ -612,15 +833,10 @@ var Ratelimit = class {
|
|
|
612
833
|
const key = this.getKey(identifier);
|
|
613
834
|
const definedMembers = this.getDefinedMembers(identifier, req);
|
|
614
835
|
const deniedValue = checkDenyListCache(definedMembers);
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
result = await Promise.all([
|
|
620
|
-
this.limiter().limit(this.ctx, key, req?.rate),
|
|
621
|
-
this.enableProtection ? checkDenyList(this.primaryRedis, this.prefix, definedMembers) : { deniedValue: void 0, invalidIpDenyList: false }
|
|
622
|
-
]);
|
|
623
|
-
}
|
|
836
|
+
const result = deniedValue ? [defaultDeniedResponse(deniedValue), { deniedValue, invalidIpDenyList: false }] : await Promise.all([
|
|
837
|
+
this.limiter().limit(this.ctx, key, req?.rate),
|
|
838
|
+
this.enableProtection ? checkDenyList(this.primaryRedis, this.prefix, definedMembers) : { deniedValue: void 0, invalidIpDenyList: false }
|
|
839
|
+
]);
|
|
624
840
|
return resolveLimitPayload(this.primaryRedis, this.prefix, result, this.denyListThreshold);
|
|
625
841
|
};
|
|
626
842
|
/**
|
|
@@ -670,9 +886,9 @@ var Ratelimit = class {
|
|
|
670
886
|
time: Date.now(),
|
|
671
887
|
success: ratelimitResponse.reason === "denyList" ? "denied" : ratelimitResponse.success,
|
|
672
888
|
...geo
|
|
673
|
-
}).catch((
|
|
889
|
+
}).catch((error) => {
|
|
674
890
|
let errorMessage = "Failed to record analytics";
|
|
675
|
-
if (`${
|
|
891
|
+
if (`${error}`.includes("WRONGTYPE")) {
|
|
676
892
|
errorMessage = `
|
|
677
893
|
Failed to record analytics. See the information below:
|
|
678
894
|
|
|
@@ -685,11 +901,11 @@ var Ratelimit = class {
|
|
|
685
901
|
|
|
686
902
|
`;
|
|
687
903
|
}
|
|
688
|
-
console.warn(errorMessage,
|
|
904
|
+
console.warn(errorMessage, error);
|
|
689
905
|
});
|
|
690
906
|
ratelimitResponse.pending = Promise.all([ratelimitResponse.pending, analyticsP]);
|
|
691
|
-
} catch (
|
|
692
|
-
console.warn("Failed to record analytics",
|
|
907
|
+
} catch (error) {
|
|
908
|
+
console.warn("Failed to record analytics", error);
|
|
693
909
|
}
|
|
694
910
|
;
|
|
695
911
|
}
|
|
@@ -735,9 +951,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
735
951
|
analytics: config.analytics,
|
|
736
952
|
ctx: {
|
|
737
953
|
regionContexts: config.redis.map((redis) => ({
|
|
738
|
-
redis
|
|
739
|
-
scriptHashes: {},
|
|
740
|
-
cacheScripts: config.cacheScripts ?? true
|
|
954
|
+
redis
|
|
741
955
|
})),
|
|
742
956
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
743
957
|
}
|
|
@@ -786,8 +1000,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
786
1000
|
redis: regionContext.redis,
|
|
787
1001
|
request: safeEval(
|
|
788
1002
|
regionContext,
|
|
789
|
-
|
|
790
|
-
"limitHash",
|
|
1003
|
+
SCRIPTS.multiRegion.fixedWindow.limit,
|
|
791
1004
|
[key],
|
|
792
1005
|
[requestId, windowDuration, incrementBy]
|
|
793
1006
|
)
|
|
@@ -803,18 +1016,17 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
803
1016
|
const remaining = tokens - usedTokens;
|
|
804
1017
|
async function sync() {
|
|
805
1018
|
const individualIDs = await Promise.all(dbs.map((s) => s.request));
|
|
806
|
-
const allIDs =
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
).values()
|
|
815
|
-
);
|
|
1019
|
+
const allIDs = [...new Set(
|
|
1020
|
+
individualIDs.flat().reduce((acc, curr, index) => {
|
|
1021
|
+
if (index % 2 === 0) {
|
|
1022
|
+
acc.push(curr);
|
|
1023
|
+
}
|
|
1024
|
+
return acc;
|
|
1025
|
+
}, [])
|
|
1026
|
+
).values()];
|
|
816
1027
|
for (const db of dbs) {
|
|
817
|
-
const
|
|
1028
|
+
const usedDbTokensRequest = await db.request;
|
|
1029
|
+
const usedDbTokens = usedDbTokensRequest.reduce(
|
|
818
1030
|
(accTokens, usedToken, index) => {
|
|
819
1031
|
let parsedToken = 0;
|
|
820
1032
|
if (index % 2) {
|
|
@@ -824,7 +1036,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
824
1036
|
},
|
|
825
1037
|
0
|
|
826
1038
|
);
|
|
827
|
-
const
|
|
1039
|
+
const dbIdsRequest = await db.request;
|
|
1040
|
+
const dbIds = dbIdsRequest.reduce((ids, currentId, index) => {
|
|
828
1041
|
if (index % 2 === 0) {
|
|
829
1042
|
ids.push(currentId);
|
|
830
1043
|
}
|
|
@@ -862,8 +1075,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
862
1075
|
redis: regionContext.redis,
|
|
863
1076
|
request: safeEval(
|
|
864
1077
|
regionContext,
|
|
865
|
-
|
|
866
|
-
"getRemainingHash",
|
|
1078
|
+
SCRIPTS.multiRegion.fixedWindow.getRemaining,
|
|
867
1079
|
[key],
|
|
868
1080
|
[null]
|
|
869
1081
|
)
|
|
@@ -889,8 +1101,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
889
1101
|
await Promise.all(ctx.regionContexts.map((regionContext) => {
|
|
890
1102
|
safeEval(
|
|
891
1103
|
regionContext,
|
|
892
|
-
|
|
893
|
-
"resetHash",
|
|
1104
|
+
RESET_SCRIPT,
|
|
894
1105
|
[pattern],
|
|
895
1106
|
[null]
|
|
896
1107
|
);
|
|
@@ -943,8 +1154,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
943
1154
|
redis: regionContext.redis,
|
|
944
1155
|
request: safeEval(
|
|
945
1156
|
regionContext,
|
|
946
|
-
|
|
947
|
-
"limitHash",
|
|
1157
|
+
SCRIPTS.multiRegion.slidingWindow.limit,
|
|
948
1158
|
[currentKey, previousKey],
|
|
949
1159
|
[tokens, now, windowDuration, requestId, incrementBy]
|
|
950
1160
|
// lua seems to return `1` for true and `null` for false
|
|
@@ -974,16 +1184,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
974
1184
|
const remaining = tokens - usedTokens;
|
|
975
1185
|
async function sync() {
|
|
976
1186
|
const res = await Promise.all(dbs.map((s) => s.request));
|
|
977
|
-
const allCurrentIds =
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
).values()
|
|
986
|
-
);
|
|
1187
|
+
const allCurrentIds = [...new Set(
|
|
1188
|
+
res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
|
|
1189
|
+
if (index % 2 === 0) {
|
|
1190
|
+
acc.push(curr);
|
|
1191
|
+
}
|
|
1192
|
+
return acc;
|
|
1193
|
+
}, [])
|
|
1194
|
+
).values()];
|
|
987
1195
|
for (const db of dbs) {
|
|
988
1196
|
const [current2, _previous, _success] = await db.request;
|
|
989
1197
|
const dbIds = current2.reduce((ids, currentId, index) => {
|
|
@@ -1033,8 +1241,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1033
1241
|
redis: regionContext.redis,
|
|
1034
1242
|
request: safeEval(
|
|
1035
1243
|
regionContext,
|
|
1036
|
-
|
|
1037
|
-
"getRemainingHash",
|
|
1244
|
+
SCRIPTS.multiRegion.slidingWindow.getRemaining,
|
|
1038
1245
|
[currentKey, previousKey],
|
|
1039
1246
|
[now, windowSize]
|
|
1040
1247
|
// lua seems to return `1` for true and `null` for false
|
|
@@ -1054,8 +1261,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1054
1261
|
await Promise.all(ctx.regionContexts.map((regionContext) => {
|
|
1055
1262
|
safeEval(
|
|
1056
1263
|
regionContext,
|
|
1057
|
-
|
|
1058
|
-
"resetHash",
|
|
1264
|
+
RESET_SCRIPT,
|
|
1059
1265
|
[pattern],
|
|
1060
1266
|
[null]
|
|
1061
1267
|
);
|
|
@@ -1065,162 +1271,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1065
1271
|
}
|
|
1066
1272
|
};
|
|
1067
1273
|
|
|
1068
|
-
// src/lua-scripts/single.ts
|
|
1069
|
-
var fixedWindowLimitScript2 = `
|
|
1070
|
-
local key = KEYS[1]
|
|
1071
|
-
local window = ARGV[1]
|
|
1072
|
-
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
1073
|
-
|
|
1074
|
-
local r = redis.call("INCRBY", key, incrementBy)
|
|
1075
|
-
if r == tonumber(incrementBy) then
|
|
1076
|
-
-- The first time this key is set, the value will be equal to incrementBy.
|
|
1077
|
-
-- So we only need the expire command once
|
|
1078
|
-
redis.call("PEXPIRE", key, window)
|
|
1079
|
-
end
|
|
1080
|
-
|
|
1081
|
-
return r
|
|
1082
|
-
`;
|
|
1083
|
-
var fixedWindowRemainingTokensScript2 = `
|
|
1084
|
-
local key = KEYS[1]
|
|
1085
|
-
local tokens = 0
|
|
1086
|
-
|
|
1087
|
-
local value = redis.call('GET', key)
|
|
1088
|
-
if value then
|
|
1089
|
-
tokens = value
|
|
1090
|
-
end
|
|
1091
|
-
return tokens
|
|
1092
|
-
`;
|
|
1093
|
-
var slidingWindowLimitScript2 = `
|
|
1094
|
-
local currentKey = KEYS[1] -- identifier including prefixes
|
|
1095
|
-
local previousKey = KEYS[2] -- key of the previous bucket
|
|
1096
|
-
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
1097
|
-
local now = ARGV[2] -- current timestamp in milliseconds
|
|
1098
|
-
local window = ARGV[3] -- interval in milliseconds
|
|
1099
|
-
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
|
|
1100
|
-
|
|
1101
|
-
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
1102
|
-
if requestsInCurrentWindow == false then
|
|
1103
|
-
requestsInCurrentWindow = 0
|
|
1104
|
-
end
|
|
1105
|
-
|
|
1106
|
-
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
1107
|
-
if requestsInPreviousWindow == false then
|
|
1108
|
-
requestsInPreviousWindow = 0
|
|
1109
|
-
end
|
|
1110
|
-
local percentageInCurrent = ( now % window ) / window
|
|
1111
|
-
-- weighted requests to consider from the previous window
|
|
1112
|
-
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
1113
|
-
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
|
|
1114
|
-
return -1
|
|
1115
|
-
end
|
|
1116
|
-
|
|
1117
|
-
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
1118
|
-
if newValue == tonumber(incrementBy) then
|
|
1119
|
-
-- The first time this key is set, the value will be equal to incrementBy.
|
|
1120
|
-
-- So we only need the expire command once
|
|
1121
|
-
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
1122
|
-
end
|
|
1123
|
-
return tokens - ( newValue + requestsInPreviousWindow )
|
|
1124
|
-
`;
|
|
1125
|
-
var slidingWindowRemainingTokensScript2 = `
|
|
1126
|
-
local currentKey = KEYS[1] -- identifier including prefixes
|
|
1127
|
-
local previousKey = KEYS[2] -- key of the previous bucket
|
|
1128
|
-
local now = ARGV[1] -- current timestamp in milliseconds
|
|
1129
|
-
local window = ARGV[2] -- interval in milliseconds
|
|
1130
|
-
|
|
1131
|
-
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
1132
|
-
if requestsInCurrentWindow == false then
|
|
1133
|
-
requestsInCurrentWindow = 0
|
|
1134
|
-
end
|
|
1135
|
-
|
|
1136
|
-
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
1137
|
-
if requestsInPreviousWindow == false then
|
|
1138
|
-
requestsInPreviousWindow = 0
|
|
1139
|
-
end
|
|
1140
|
-
|
|
1141
|
-
local percentageInCurrent = ( now % window ) / window
|
|
1142
|
-
-- weighted requests to consider from the previous window
|
|
1143
|
-
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
1144
|
-
|
|
1145
|
-
return requestsInPreviousWindow + requestsInCurrentWindow
|
|
1146
|
-
`;
|
|
1147
|
-
var tokenBucketLimitScript = `
|
|
1148
|
-
local key = KEYS[1] -- identifier including prefixes
|
|
1149
|
-
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
1150
|
-
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
1151
|
-
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
1152
|
-
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
1153
|
-
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
1154
|
-
|
|
1155
|
-
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
1156
|
-
|
|
1157
|
-
local refilledAt
|
|
1158
|
-
local tokens
|
|
1159
|
-
|
|
1160
|
-
if bucket[1] == false then
|
|
1161
|
-
refilledAt = now
|
|
1162
|
-
tokens = maxTokens
|
|
1163
|
-
else
|
|
1164
|
-
refilledAt = tonumber(bucket[1])
|
|
1165
|
-
tokens = tonumber(bucket[2])
|
|
1166
|
-
end
|
|
1167
|
-
|
|
1168
|
-
if now >= refilledAt + interval then
|
|
1169
|
-
local numRefills = math.floor((now - refilledAt) / interval)
|
|
1170
|
-
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
|
|
1171
|
-
|
|
1172
|
-
refilledAt = refilledAt + numRefills * interval
|
|
1173
|
-
end
|
|
1174
|
-
|
|
1175
|
-
if tokens == 0 then
|
|
1176
|
-
return {-1, refilledAt + interval}
|
|
1177
|
-
end
|
|
1178
|
-
|
|
1179
|
-
local remaining = tokens - incrementBy
|
|
1180
|
-
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
|
|
1181
|
-
|
|
1182
|
-
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
1183
|
-
redis.call("PEXPIRE", key, expireAt)
|
|
1184
|
-
return {remaining, refilledAt + interval}
|
|
1185
|
-
`;
|
|
1186
|
-
var tokenBucketIdentifierNotFound = -1;
|
|
1187
|
-
var tokenBucketRemainingTokensScript = `
|
|
1188
|
-
local key = KEYS[1]
|
|
1189
|
-
local maxTokens = tonumber(ARGV[1])
|
|
1190
|
-
|
|
1191
|
-
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
1192
|
-
|
|
1193
|
-
if bucket[1] == false then
|
|
1194
|
-
return {maxTokens, ${tokenBucketIdentifierNotFound}}
|
|
1195
|
-
end
|
|
1196
|
-
|
|
1197
|
-
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
1198
|
-
`;
|
|
1199
|
-
var cachedFixedWindowLimitScript = `
|
|
1200
|
-
local key = KEYS[1]
|
|
1201
|
-
local window = ARGV[1]
|
|
1202
|
-
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
1203
|
-
|
|
1204
|
-
local r = redis.call("INCRBY", key, incrementBy)
|
|
1205
|
-
if r == incrementBy then
|
|
1206
|
-
-- The first time this key is set, the value will be equal to incrementBy.
|
|
1207
|
-
-- So we only need the expire command once
|
|
1208
|
-
redis.call("PEXPIRE", key, window)
|
|
1209
|
-
end
|
|
1210
|
-
|
|
1211
|
-
return r
|
|
1212
|
-
`;
|
|
1213
|
-
var cachedFixedWindowRemainingTokenScript = `
|
|
1214
|
-
local key = KEYS[1]
|
|
1215
|
-
local tokens = 0
|
|
1216
|
-
|
|
1217
|
-
local value = redis.call('GET', key)
|
|
1218
|
-
if value then
|
|
1219
|
-
tokens = value
|
|
1220
|
-
end
|
|
1221
|
-
return tokens
|
|
1222
|
-
`;
|
|
1223
|
-
|
|
1224
1274
|
// src/single.ts
|
|
1225
1275
|
var RegionRatelimit = class extends Ratelimit {
|
|
1226
1276
|
/**
|
|
@@ -1233,9 +1283,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1233
1283
|
timeout: config.timeout,
|
|
1234
1284
|
analytics: config.analytics,
|
|
1235
1285
|
ctx: {
|
|
1236
|
-
redis: config.redis
|
|
1237
|
-
scriptHashes: {},
|
|
1238
|
-
cacheScripts: config.cacheScripts ?? true
|
|
1286
|
+
redis: config.redis
|
|
1239
1287
|
},
|
|
1240
1288
|
ephemeralCache: config.ephemeralCache,
|
|
1241
1289
|
enableProtection: config.enableProtection,
|
|
@@ -1282,8 +1330,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1282
1330
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1283
1331
|
const usedTokensAfterUpdate = await safeEval(
|
|
1284
1332
|
ctx,
|
|
1285
|
-
|
|
1286
|
-
"limitHash",
|
|
1333
|
+
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1287
1334
|
[key],
|
|
1288
1335
|
[windowDuration, incrementBy]
|
|
1289
1336
|
);
|
|
@@ -1306,8 +1353,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1306
1353
|
const key = [identifier, bucket].join(":");
|
|
1307
1354
|
const usedTokens = await safeEval(
|
|
1308
1355
|
ctx,
|
|
1309
|
-
|
|
1310
|
-
"getRemainingHash",
|
|
1356
|
+
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1311
1357
|
[key],
|
|
1312
1358
|
[null]
|
|
1313
1359
|
);
|
|
@@ -1323,8 +1369,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1323
1369
|
}
|
|
1324
1370
|
await safeEval(
|
|
1325
1371
|
ctx,
|
|
1326
|
-
|
|
1327
|
-
"resetHash",
|
|
1372
|
+
RESET_SCRIPT,
|
|
1328
1373
|
[pattern],
|
|
1329
1374
|
[null]
|
|
1330
1375
|
);
|
|
@@ -1372,8 +1417,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1372
1417
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1373
1418
|
const remainingTokens = await safeEval(
|
|
1374
1419
|
ctx,
|
|
1375
|
-
|
|
1376
|
-
"limitHash",
|
|
1420
|
+
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1377
1421
|
[currentKey, previousKey],
|
|
1378
1422
|
[tokens, now, windowSize, incrementBy]
|
|
1379
1423
|
);
|
|
@@ -1398,8 +1442,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1398
1442
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1399
1443
|
const usedTokens = await safeEval(
|
|
1400
1444
|
ctx,
|
|
1401
|
-
|
|
1402
|
-
"getRemainingHash",
|
|
1445
|
+
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1403
1446
|
[currentKey, previousKey],
|
|
1404
1447
|
[now, windowSize]
|
|
1405
1448
|
);
|
|
@@ -1415,8 +1458,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1415
1458
|
}
|
|
1416
1459
|
await safeEval(
|
|
1417
1460
|
ctx,
|
|
1418
|
-
|
|
1419
|
-
"resetHash",
|
|
1461
|
+
RESET_SCRIPT,
|
|
1420
1462
|
[pattern],
|
|
1421
1463
|
[null]
|
|
1422
1464
|
);
|
|
@@ -1457,8 +1499,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1457
1499
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1458
1500
|
const [remaining, reset] = await safeEval(
|
|
1459
1501
|
ctx,
|
|
1460
|
-
|
|
1461
|
-
"limitHash",
|
|
1502
|
+
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1462
1503
|
[identifier],
|
|
1463
1504
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1464
1505
|
);
|
|
@@ -1477,8 +1518,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1477
1518
|
async getRemaining(ctx, identifier) {
|
|
1478
1519
|
const [remainingTokens, refilledAt] = await safeEval(
|
|
1479
1520
|
ctx,
|
|
1480
|
-
|
|
1481
|
-
"getRemainingHash",
|
|
1521
|
+
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1482
1522
|
[identifier],
|
|
1483
1523
|
[maxTokens]
|
|
1484
1524
|
);
|
|
@@ -1496,8 +1536,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1496
1536
|
}
|
|
1497
1537
|
await safeEval(
|
|
1498
1538
|
ctx,
|
|
1499
|
-
|
|
1500
|
-
"resetHash",
|
|
1539
|
+
RESET_SCRIPT,
|
|
1501
1540
|
[pattern],
|
|
1502
1541
|
[null]
|
|
1503
1542
|
);
|
|
@@ -1545,8 +1584,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1545
1584
|
const success = cachedTokensAfterUpdate < tokens;
|
|
1546
1585
|
const pending = success ? safeEval(
|
|
1547
1586
|
ctx,
|
|
1548
|
-
|
|
1549
|
-
"limitHash",
|
|
1587
|
+
SCRIPTS.singleRegion.cachedFixedWindow.limit,
|
|
1550
1588
|
[key],
|
|
1551
1589
|
[windowDuration, incrementBy]
|
|
1552
1590
|
) : Promise.resolve();
|
|
@@ -1560,8 +1598,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1560
1598
|
}
|
|
1561
1599
|
const usedTokensAfterUpdate = await safeEval(
|
|
1562
1600
|
ctx,
|
|
1563
|
-
|
|
1564
|
-
"limitHash",
|
|
1601
|
+
SCRIPTS.singleRegion.cachedFixedWindow.limit,
|
|
1565
1602
|
[key],
|
|
1566
1603
|
[windowDuration, incrementBy]
|
|
1567
1604
|
);
|
|
@@ -1591,8 +1628,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1591
1628
|
}
|
|
1592
1629
|
const usedTokens = await safeEval(
|
|
1593
1630
|
ctx,
|
|
1594
|
-
|
|
1595
|
-
"getRemainingHash",
|
|
1631
|
+
SCRIPTS.singleRegion.cachedFixedWindow.getRemaining,
|
|
1596
1632
|
[key],
|
|
1597
1633
|
[null]
|
|
1598
1634
|
);
|
|
@@ -1611,8 +1647,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1611
1647
|
const pattern = [identifier, "*"].join(":");
|
|
1612
1648
|
await safeEval(
|
|
1613
1649
|
ctx,
|
|
1614
|
-
|
|
1615
|
-
"resetHash",
|
|
1650
|
+
RESET_SCRIPT,
|
|
1616
1651
|
[pattern],
|
|
1617
1652
|
[null]
|
|
1618
1653
|
);
|