@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.mjs
CHANGED
|
@@ -26,10 +26,10 @@ var Analytics = class {
|
|
|
26
26
|
* @returns
|
|
27
27
|
*/
|
|
28
28
|
extractGeo(req) {
|
|
29
|
-
if (
|
|
29
|
+
if (req.geo !== void 0) {
|
|
30
30
|
return req.geo;
|
|
31
31
|
}
|
|
32
|
-
if (
|
|
32
|
+
if (req.cf !== void 0) {
|
|
33
33
|
return req.cf;
|
|
34
34
|
}
|
|
35
35
|
return {};
|
|
@@ -118,54 +118,203 @@ function ms(d) {
|
|
|
118
118
|
const time = Number.parseInt(match[1]);
|
|
119
119
|
const unit = match[2];
|
|
120
120
|
switch (unit) {
|
|
121
|
-
case "ms":
|
|
121
|
+
case "ms": {
|
|
122
122
|
return time;
|
|
123
|
-
|
|
123
|
+
}
|
|
124
|
+
case "s": {
|
|
124
125
|
return time * 1e3;
|
|
125
|
-
|
|
126
|
+
}
|
|
127
|
+
case "m": {
|
|
126
128
|
return time * 1e3 * 60;
|
|
127
|
-
|
|
129
|
+
}
|
|
130
|
+
case "h": {
|
|
128
131
|
return time * 1e3 * 60 * 60;
|
|
129
|
-
|
|
132
|
+
}
|
|
133
|
+
case "d": {
|
|
130
134
|
return time * 1e3 * 60 * 60 * 24;
|
|
131
|
-
|
|
135
|
+
}
|
|
136
|
+
default: {
|
|
132
137
|
throw new Error(`Unable to parse window size: ${d}`);
|
|
138
|
+
}
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
// src/hash.ts
|
|
137
|
-
var
|
|
138
|
-
const regionContexts = "redis" in ctx ? [ctx] : ctx.regionContexts;
|
|
139
|
-
const hashSample = regionContexts[0].scriptHashes[kind];
|
|
140
|
-
if (!hashSample) {
|
|
141
|
-
await Promise.all(regionContexts.map(async (context) => {
|
|
142
|
-
context.scriptHashes[kind] = await context.redis.scriptLoad(script);
|
|
143
|
-
}));
|
|
144
|
-
}
|
|
145
|
-
;
|
|
146
|
-
};
|
|
147
|
-
var safeEval = async (ctx, script, kind, keys, args) => {
|
|
148
|
-
if (!ctx.cacheScripts) {
|
|
149
|
-
return await ctx.redis.eval(script, keys, args);
|
|
150
|
-
}
|
|
151
|
-
;
|
|
152
|
-
await setHash(ctx, script, kind);
|
|
143
|
+
var safeEval = async (ctx, script, keys, args) => {
|
|
153
144
|
try {
|
|
154
|
-
return await ctx.redis.evalsha(
|
|
145
|
+
return await ctx.redis.evalsha(script.hash, keys, args);
|
|
155
146
|
} catch (error) {
|
|
156
147
|
if (`${error}`.includes("NOSCRIPT")) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
148
|
+
const hash = await ctx.redis.scriptLoad(script.script);
|
|
149
|
+
if (hash !== script.hash) {
|
|
150
|
+
console.warn(
|
|
151
|
+
"Upstash Ratelimit: Expected hash and the hash received from Redis are different. Ratelimit will work as usual but performance will be reduced."
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return await ctx.redis.evalsha(hash, keys, args);
|
|
162
155
|
}
|
|
163
156
|
throw error;
|
|
164
157
|
}
|
|
165
158
|
};
|
|
166
159
|
|
|
167
|
-
// src/lua-scripts/
|
|
160
|
+
// src/lua-scripts/single.ts
|
|
168
161
|
var fixedWindowLimitScript = `
|
|
162
|
+
local key = KEYS[1]
|
|
163
|
+
local window = ARGV[1]
|
|
164
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
165
|
+
|
|
166
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
167
|
+
if r == tonumber(incrementBy) then
|
|
168
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
169
|
+
-- So we only need the expire command once
|
|
170
|
+
redis.call("PEXPIRE", key, window)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
return r
|
|
174
|
+
`;
|
|
175
|
+
var fixedWindowRemainingTokensScript = `
|
|
176
|
+
local key = KEYS[1]
|
|
177
|
+
local tokens = 0
|
|
178
|
+
|
|
179
|
+
local value = redis.call('GET', key)
|
|
180
|
+
if value then
|
|
181
|
+
tokens = value
|
|
182
|
+
end
|
|
183
|
+
return tokens
|
|
184
|
+
`;
|
|
185
|
+
var slidingWindowLimitScript = `
|
|
186
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
187
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
188
|
+
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
189
|
+
local now = ARGV[2] -- current timestamp in milliseconds
|
|
190
|
+
local window = ARGV[3] -- interval in milliseconds
|
|
191
|
+
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
|
|
192
|
+
|
|
193
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
194
|
+
if requestsInCurrentWindow == false then
|
|
195
|
+
requestsInCurrentWindow = 0
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
199
|
+
if requestsInPreviousWindow == false then
|
|
200
|
+
requestsInPreviousWindow = 0
|
|
201
|
+
end
|
|
202
|
+
local percentageInCurrent = ( now % window ) / window
|
|
203
|
+
-- weighted requests to consider from the previous window
|
|
204
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
205
|
+
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
|
|
206
|
+
return -1
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
210
|
+
if newValue == tonumber(incrementBy) then
|
|
211
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
212
|
+
-- So we only need the expire command once
|
|
213
|
+
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
214
|
+
end
|
|
215
|
+
return tokens - ( newValue + requestsInPreviousWindow )
|
|
216
|
+
`;
|
|
217
|
+
var slidingWindowRemainingTokensScript = `
|
|
218
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
219
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
220
|
+
local now = ARGV[1] -- current timestamp in milliseconds
|
|
221
|
+
local window = ARGV[2] -- interval in milliseconds
|
|
222
|
+
|
|
223
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
224
|
+
if requestsInCurrentWindow == false then
|
|
225
|
+
requestsInCurrentWindow = 0
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
229
|
+
if requestsInPreviousWindow == false then
|
|
230
|
+
requestsInPreviousWindow = 0
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
local percentageInCurrent = ( now % window ) / window
|
|
234
|
+
-- weighted requests to consider from the previous window
|
|
235
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
236
|
+
|
|
237
|
+
return requestsInPreviousWindow + requestsInCurrentWindow
|
|
238
|
+
`;
|
|
239
|
+
var tokenBucketLimitScript = `
|
|
240
|
+
local key = KEYS[1] -- identifier including prefixes
|
|
241
|
+
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
242
|
+
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
243
|
+
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
244
|
+
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
245
|
+
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
246
|
+
|
|
247
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
248
|
+
|
|
249
|
+
local refilledAt
|
|
250
|
+
local tokens
|
|
251
|
+
|
|
252
|
+
if bucket[1] == false then
|
|
253
|
+
refilledAt = now
|
|
254
|
+
tokens = maxTokens
|
|
255
|
+
else
|
|
256
|
+
refilledAt = tonumber(bucket[1])
|
|
257
|
+
tokens = tonumber(bucket[2])
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if now >= refilledAt + interval then
|
|
261
|
+
local numRefills = math.floor((now - refilledAt) / interval)
|
|
262
|
+
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
|
|
263
|
+
|
|
264
|
+
refilledAt = refilledAt + numRefills * interval
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if tokens == 0 then
|
|
268
|
+
return {-1, refilledAt + interval}
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
local remaining = tokens - incrementBy
|
|
272
|
+
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
|
|
273
|
+
|
|
274
|
+
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
275
|
+
redis.call("PEXPIRE", key, expireAt)
|
|
276
|
+
return {remaining, refilledAt + interval}
|
|
277
|
+
`;
|
|
278
|
+
var tokenBucketIdentifierNotFound = -1;
|
|
279
|
+
var tokenBucketRemainingTokensScript = `
|
|
280
|
+
local key = KEYS[1]
|
|
281
|
+
local maxTokens = tonumber(ARGV[1])
|
|
282
|
+
|
|
283
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
284
|
+
|
|
285
|
+
if bucket[1] == false then
|
|
286
|
+
return {maxTokens, ${tokenBucketIdentifierNotFound}}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
290
|
+
`;
|
|
291
|
+
var cachedFixedWindowLimitScript = `
|
|
292
|
+
local key = KEYS[1]
|
|
293
|
+
local window = ARGV[1]
|
|
294
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
295
|
+
|
|
296
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
297
|
+
if r == incrementBy then
|
|
298
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
299
|
+
-- So we only need the expire command once
|
|
300
|
+
redis.call("PEXPIRE", key, window)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
return r
|
|
304
|
+
`;
|
|
305
|
+
var cachedFixedWindowRemainingTokenScript = `
|
|
306
|
+
local key = KEYS[1]
|
|
307
|
+
local tokens = 0
|
|
308
|
+
|
|
309
|
+
local value = redis.call('GET', key)
|
|
310
|
+
if value then
|
|
311
|
+
tokens = value
|
|
312
|
+
end
|
|
313
|
+
return tokens
|
|
314
|
+
`;
|
|
315
|
+
|
|
316
|
+
// src/lua-scripts/multi.ts
|
|
317
|
+
var fixedWindowLimitScript2 = `
|
|
169
318
|
local key = KEYS[1]
|
|
170
319
|
local id = ARGV[1]
|
|
171
320
|
local window = ARGV[2]
|
|
@@ -181,7 +330,7 @@ var fixedWindowLimitScript = `
|
|
|
181
330
|
|
|
182
331
|
return fields
|
|
183
332
|
`;
|
|
184
|
-
var
|
|
333
|
+
var fixedWindowRemainingTokensScript2 = `
|
|
185
334
|
local key = KEYS[1]
|
|
186
335
|
local tokens = 0
|
|
187
336
|
|
|
@@ -189,7 +338,7 @@ var fixedWindowRemainingTokensScript = `
|
|
|
189
338
|
|
|
190
339
|
return fields
|
|
191
340
|
`;
|
|
192
|
-
var
|
|
341
|
+
var slidingWindowLimitScript2 = `
|
|
193
342
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
194
343
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
195
344
|
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
@@ -224,7 +373,7 @@ var slidingWindowLimitScript = `
|
|
|
224
373
|
end
|
|
225
374
|
return {currentFields, previousFields, true}
|
|
226
375
|
`;
|
|
227
|
-
var
|
|
376
|
+
var slidingWindowRemainingTokensScript2 = `
|
|
228
377
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
229
378
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
230
379
|
local now = ARGV[1] -- current timestamp in milliseconds
|
|
@@ -273,6 +422,78 @@ var resetScript = `
|
|
|
273
422
|
until cursor == "0"
|
|
274
423
|
`;
|
|
275
424
|
|
|
425
|
+
// src/lua-scripts/hash.ts
|
|
426
|
+
var SCRIPTS = {
|
|
427
|
+
singleRegion: {
|
|
428
|
+
fixedWindow: {
|
|
429
|
+
limit: {
|
|
430
|
+
script: fixedWindowLimitScript,
|
|
431
|
+
hash: "b13943e359636db027ad280f1def143f02158c13"
|
|
432
|
+
},
|
|
433
|
+
getRemaining: {
|
|
434
|
+
script: fixedWindowRemainingTokensScript,
|
|
435
|
+
hash: "8c4c341934502aee132643ffbe58ead3450e5208"
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
slidingWindow: {
|
|
439
|
+
limit: {
|
|
440
|
+
script: slidingWindowLimitScript,
|
|
441
|
+
hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
|
|
442
|
+
},
|
|
443
|
+
getRemaining: {
|
|
444
|
+
script: slidingWindowRemainingTokensScript,
|
|
445
|
+
hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
tokenBucket: {
|
|
449
|
+
limit: {
|
|
450
|
+
script: tokenBucketLimitScript,
|
|
451
|
+
hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
|
|
452
|
+
},
|
|
453
|
+
getRemaining: {
|
|
454
|
+
script: tokenBucketRemainingTokensScript,
|
|
455
|
+
hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
cachedFixedWindow: {
|
|
459
|
+
limit: {
|
|
460
|
+
script: cachedFixedWindowLimitScript,
|
|
461
|
+
hash: "c26b12703dd137939b9a69a3a9b18e906a2d940f"
|
|
462
|
+
},
|
|
463
|
+
getRemaining: {
|
|
464
|
+
script: cachedFixedWindowRemainingTokenScript,
|
|
465
|
+
hash: "8e8f222ccae68b595ee6e3f3bf2199629a62b91a"
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
multiRegion: {
|
|
470
|
+
fixedWindow: {
|
|
471
|
+
limit: {
|
|
472
|
+
script: fixedWindowLimitScript2,
|
|
473
|
+
hash: "a8c14f3835aa87bd70e5e2116081b81664abcf5c"
|
|
474
|
+
},
|
|
475
|
+
getRemaining: {
|
|
476
|
+
script: fixedWindowRemainingTokensScript2,
|
|
477
|
+
hash: "8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd"
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
slidingWindow: {
|
|
481
|
+
limit: {
|
|
482
|
+
script: slidingWindowLimitScript2,
|
|
483
|
+
hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
|
|
484
|
+
},
|
|
485
|
+
getRemaining: {
|
|
486
|
+
script: slidingWindowRemainingTokensScript2,
|
|
487
|
+
hash: "558c9306b7ec54abb50747fe0b17e5d44bd24868"
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
var RESET_SCRIPT = {
|
|
493
|
+
script: resetScript,
|
|
494
|
+
hash: "54bd274ddc59fb3be0f42deee2f64322a10e2b50"
|
|
495
|
+
};
|
|
496
|
+
|
|
276
497
|
// src/types.ts
|
|
277
498
|
var DenyListExtension = "denyList";
|
|
278
499
|
var IpDenyListKey = "ipDenyList";
|
|
@@ -356,7 +577,7 @@ var updateIpDenyList = async (redis, prefix, threshold, ttl) => {
|
|
|
356
577
|
const transaction = redis.multi();
|
|
357
578
|
transaction.sdiffstore(allDenyLists, allDenyLists, ipDenyList);
|
|
358
579
|
transaction.del(ipDenyList);
|
|
359
|
-
transaction.sadd(ipDenyList, ...allIps);
|
|
580
|
+
transaction.sadd(ipDenyList, allIps.at(0), ...allIps.slice(1));
|
|
360
581
|
transaction.sdiffstore(ipDenyList, ipDenyList, allDenyLists);
|
|
361
582
|
transaction.sunionstore(allDenyLists, allDenyLists, ipDenyList);
|
|
362
583
|
transaction.set(statusKey, "valid", { px: ttl ?? getIpListTTL() });
|
|
@@ -458,7 +679,7 @@ var Ratelimit = class {
|
|
|
458
679
|
}) : void 0;
|
|
459
680
|
if (config.ephemeralCache instanceof Map) {
|
|
460
681
|
this.ctx.cache = new Cache(config.ephemeralCache);
|
|
461
|
-
} else if (
|
|
682
|
+
} else if (config.ephemeralCache === void 0) {
|
|
462
683
|
this.ctx.cache = new Cache(/* @__PURE__ */ new Map());
|
|
463
684
|
}
|
|
464
685
|
}
|
|
@@ -589,15 +810,10 @@ var Ratelimit = class {
|
|
|
589
810
|
const key = this.getKey(identifier);
|
|
590
811
|
const definedMembers = this.getDefinedMembers(identifier, req);
|
|
591
812
|
const deniedValue = checkDenyListCache(definedMembers);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
result = await Promise.all([
|
|
597
|
-
this.limiter().limit(this.ctx, key, req?.rate),
|
|
598
|
-
this.enableProtection ? checkDenyList(this.primaryRedis, this.prefix, definedMembers) : { deniedValue: void 0, invalidIpDenyList: false }
|
|
599
|
-
]);
|
|
600
|
-
}
|
|
813
|
+
const result = deniedValue ? [defaultDeniedResponse(deniedValue), { deniedValue, invalidIpDenyList: false }] : await Promise.all([
|
|
814
|
+
this.limiter().limit(this.ctx, key, req?.rate),
|
|
815
|
+
this.enableProtection ? checkDenyList(this.primaryRedis, this.prefix, definedMembers) : { deniedValue: void 0, invalidIpDenyList: false }
|
|
816
|
+
]);
|
|
601
817
|
return resolveLimitPayload(this.primaryRedis, this.prefix, result, this.denyListThreshold);
|
|
602
818
|
};
|
|
603
819
|
/**
|
|
@@ -647,9 +863,9 @@ var Ratelimit = class {
|
|
|
647
863
|
time: Date.now(),
|
|
648
864
|
success: ratelimitResponse.reason === "denyList" ? "denied" : ratelimitResponse.success,
|
|
649
865
|
...geo
|
|
650
|
-
}).catch((
|
|
866
|
+
}).catch((error) => {
|
|
651
867
|
let errorMessage = "Failed to record analytics";
|
|
652
|
-
if (`${
|
|
868
|
+
if (`${error}`.includes("WRONGTYPE")) {
|
|
653
869
|
errorMessage = `
|
|
654
870
|
Failed to record analytics. See the information below:
|
|
655
871
|
|
|
@@ -662,11 +878,11 @@ var Ratelimit = class {
|
|
|
662
878
|
|
|
663
879
|
`;
|
|
664
880
|
}
|
|
665
|
-
console.warn(errorMessage,
|
|
881
|
+
console.warn(errorMessage, error);
|
|
666
882
|
});
|
|
667
883
|
ratelimitResponse.pending = Promise.all([ratelimitResponse.pending, analyticsP]);
|
|
668
|
-
} catch (
|
|
669
|
-
console.warn("Failed to record analytics",
|
|
884
|
+
} catch (error) {
|
|
885
|
+
console.warn("Failed to record analytics", error);
|
|
670
886
|
}
|
|
671
887
|
;
|
|
672
888
|
}
|
|
@@ -712,9 +928,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
712
928
|
analytics: config.analytics,
|
|
713
929
|
ctx: {
|
|
714
930
|
regionContexts: config.redis.map((redis) => ({
|
|
715
|
-
redis
|
|
716
|
-
scriptHashes: {},
|
|
717
|
-
cacheScripts: config.cacheScripts ?? true
|
|
931
|
+
redis
|
|
718
932
|
})),
|
|
719
933
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
720
934
|
}
|
|
@@ -763,8 +977,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
763
977
|
redis: regionContext.redis,
|
|
764
978
|
request: safeEval(
|
|
765
979
|
regionContext,
|
|
766
|
-
|
|
767
|
-
"limitHash",
|
|
980
|
+
SCRIPTS.multiRegion.fixedWindow.limit,
|
|
768
981
|
[key],
|
|
769
982
|
[requestId, windowDuration, incrementBy]
|
|
770
983
|
)
|
|
@@ -780,18 +993,17 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
780
993
|
const remaining = tokens - usedTokens;
|
|
781
994
|
async function sync() {
|
|
782
995
|
const individualIDs = await Promise.all(dbs.map((s) => s.request));
|
|
783
|
-
const allIDs =
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
).values()
|
|
792
|
-
);
|
|
996
|
+
const allIDs = [...new Set(
|
|
997
|
+
individualIDs.flat().reduce((acc, curr, index) => {
|
|
998
|
+
if (index % 2 === 0) {
|
|
999
|
+
acc.push(curr);
|
|
1000
|
+
}
|
|
1001
|
+
return acc;
|
|
1002
|
+
}, [])
|
|
1003
|
+
).values()];
|
|
793
1004
|
for (const db of dbs) {
|
|
794
|
-
const
|
|
1005
|
+
const usedDbTokensRequest = await db.request;
|
|
1006
|
+
const usedDbTokens = usedDbTokensRequest.reduce(
|
|
795
1007
|
(accTokens, usedToken, index) => {
|
|
796
1008
|
let parsedToken = 0;
|
|
797
1009
|
if (index % 2) {
|
|
@@ -801,7 +1013,8 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
801
1013
|
},
|
|
802
1014
|
0
|
|
803
1015
|
);
|
|
804
|
-
const
|
|
1016
|
+
const dbIdsRequest = await db.request;
|
|
1017
|
+
const dbIds = dbIdsRequest.reduce((ids, currentId, index) => {
|
|
805
1018
|
if (index % 2 === 0) {
|
|
806
1019
|
ids.push(currentId);
|
|
807
1020
|
}
|
|
@@ -839,8 +1052,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
839
1052
|
redis: regionContext.redis,
|
|
840
1053
|
request: safeEval(
|
|
841
1054
|
regionContext,
|
|
842
|
-
|
|
843
|
-
"getRemainingHash",
|
|
1055
|
+
SCRIPTS.multiRegion.fixedWindow.getRemaining,
|
|
844
1056
|
[key],
|
|
845
1057
|
[null]
|
|
846
1058
|
)
|
|
@@ -866,8 +1078,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
866
1078
|
await Promise.all(ctx.regionContexts.map((regionContext) => {
|
|
867
1079
|
safeEval(
|
|
868
1080
|
regionContext,
|
|
869
|
-
|
|
870
|
-
"resetHash",
|
|
1081
|
+
RESET_SCRIPT,
|
|
871
1082
|
[pattern],
|
|
872
1083
|
[null]
|
|
873
1084
|
);
|
|
@@ -920,8 +1131,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
920
1131
|
redis: regionContext.redis,
|
|
921
1132
|
request: safeEval(
|
|
922
1133
|
regionContext,
|
|
923
|
-
|
|
924
|
-
"limitHash",
|
|
1134
|
+
SCRIPTS.multiRegion.slidingWindow.limit,
|
|
925
1135
|
[currentKey, previousKey],
|
|
926
1136
|
[tokens, now, windowDuration, requestId, incrementBy]
|
|
927
1137
|
// lua seems to return `1` for true and `null` for false
|
|
@@ -951,16 +1161,14 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
951
1161
|
const remaining = tokens - usedTokens;
|
|
952
1162
|
async function sync() {
|
|
953
1163
|
const res = await Promise.all(dbs.map((s) => s.request));
|
|
954
|
-
const allCurrentIds =
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
).values()
|
|
963
|
-
);
|
|
1164
|
+
const allCurrentIds = [...new Set(
|
|
1165
|
+
res.flatMap(([current2]) => current2).reduce((acc, curr, index) => {
|
|
1166
|
+
if (index % 2 === 0) {
|
|
1167
|
+
acc.push(curr);
|
|
1168
|
+
}
|
|
1169
|
+
return acc;
|
|
1170
|
+
}, [])
|
|
1171
|
+
).values()];
|
|
964
1172
|
for (const db of dbs) {
|
|
965
1173
|
const [current2, _previous, _success] = await db.request;
|
|
966
1174
|
const dbIds = current2.reduce((ids, currentId, index) => {
|
|
@@ -1010,8 +1218,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1010
1218
|
redis: regionContext.redis,
|
|
1011
1219
|
request: safeEval(
|
|
1012
1220
|
regionContext,
|
|
1013
|
-
|
|
1014
|
-
"getRemainingHash",
|
|
1221
|
+
SCRIPTS.multiRegion.slidingWindow.getRemaining,
|
|
1015
1222
|
[currentKey, previousKey],
|
|
1016
1223
|
[now, windowSize]
|
|
1017
1224
|
// lua seems to return `1` for true and `null` for false
|
|
@@ -1031,8 +1238,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1031
1238
|
await Promise.all(ctx.regionContexts.map((regionContext) => {
|
|
1032
1239
|
safeEval(
|
|
1033
1240
|
regionContext,
|
|
1034
|
-
|
|
1035
|
-
"resetHash",
|
|
1241
|
+
RESET_SCRIPT,
|
|
1036
1242
|
[pattern],
|
|
1037
1243
|
[null]
|
|
1038
1244
|
);
|
|
@@ -1042,162 +1248,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1042
1248
|
}
|
|
1043
1249
|
};
|
|
1044
1250
|
|
|
1045
|
-
// src/lua-scripts/single.ts
|
|
1046
|
-
var fixedWindowLimitScript2 = `
|
|
1047
|
-
local key = KEYS[1]
|
|
1048
|
-
local window = ARGV[1]
|
|
1049
|
-
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
1050
|
-
|
|
1051
|
-
local r = redis.call("INCRBY", key, incrementBy)
|
|
1052
|
-
if r == tonumber(incrementBy) then
|
|
1053
|
-
-- The first time this key is set, the value will be equal to incrementBy.
|
|
1054
|
-
-- So we only need the expire command once
|
|
1055
|
-
redis.call("PEXPIRE", key, window)
|
|
1056
|
-
end
|
|
1057
|
-
|
|
1058
|
-
return r
|
|
1059
|
-
`;
|
|
1060
|
-
var fixedWindowRemainingTokensScript2 = `
|
|
1061
|
-
local key = KEYS[1]
|
|
1062
|
-
local tokens = 0
|
|
1063
|
-
|
|
1064
|
-
local value = redis.call('GET', key)
|
|
1065
|
-
if value then
|
|
1066
|
-
tokens = value
|
|
1067
|
-
end
|
|
1068
|
-
return tokens
|
|
1069
|
-
`;
|
|
1070
|
-
var slidingWindowLimitScript2 = `
|
|
1071
|
-
local currentKey = KEYS[1] -- identifier including prefixes
|
|
1072
|
-
local previousKey = KEYS[2] -- key of the previous bucket
|
|
1073
|
-
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
1074
|
-
local now = ARGV[2] -- current timestamp in milliseconds
|
|
1075
|
-
local window = ARGV[3] -- interval in milliseconds
|
|
1076
|
-
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
|
|
1077
|
-
|
|
1078
|
-
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
1079
|
-
if requestsInCurrentWindow == false then
|
|
1080
|
-
requestsInCurrentWindow = 0
|
|
1081
|
-
end
|
|
1082
|
-
|
|
1083
|
-
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
1084
|
-
if requestsInPreviousWindow == false then
|
|
1085
|
-
requestsInPreviousWindow = 0
|
|
1086
|
-
end
|
|
1087
|
-
local percentageInCurrent = ( now % window ) / window
|
|
1088
|
-
-- weighted requests to consider from the previous window
|
|
1089
|
-
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
1090
|
-
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
|
|
1091
|
-
return -1
|
|
1092
|
-
end
|
|
1093
|
-
|
|
1094
|
-
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
1095
|
-
if newValue == tonumber(incrementBy) then
|
|
1096
|
-
-- The first time this key is set, the value will be equal to incrementBy.
|
|
1097
|
-
-- So we only need the expire command once
|
|
1098
|
-
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
1099
|
-
end
|
|
1100
|
-
return tokens - ( newValue + requestsInPreviousWindow )
|
|
1101
|
-
`;
|
|
1102
|
-
var slidingWindowRemainingTokensScript2 = `
|
|
1103
|
-
local currentKey = KEYS[1] -- identifier including prefixes
|
|
1104
|
-
local previousKey = KEYS[2] -- key of the previous bucket
|
|
1105
|
-
local now = ARGV[1] -- current timestamp in milliseconds
|
|
1106
|
-
local window = ARGV[2] -- interval in milliseconds
|
|
1107
|
-
|
|
1108
|
-
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
1109
|
-
if requestsInCurrentWindow == false then
|
|
1110
|
-
requestsInCurrentWindow = 0
|
|
1111
|
-
end
|
|
1112
|
-
|
|
1113
|
-
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
1114
|
-
if requestsInPreviousWindow == false then
|
|
1115
|
-
requestsInPreviousWindow = 0
|
|
1116
|
-
end
|
|
1117
|
-
|
|
1118
|
-
local percentageInCurrent = ( now % window ) / window
|
|
1119
|
-
-- weighted requests to consider from the previous window
|
|
1120
|
-
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
1121
|
-
|
|
1122
|
-
return requestsInPreviousWindow + requestsInCurrentWindow
|
|
1123
|
-
`;
|
|
1124
|
-
var tokenBucketLimitScript = `
|
|
1125
|
-
local key = KEYS[1] -- identifier including prefixes
|
|
1126
|
-
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
1127
|
-
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
1128
|
-
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
1129
|
-
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
1130
|
-
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
1131
|
-
|
|
1132
|
-
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
1133
|
-
|
|
1134
|
-
local refilledAt
|
|
1135
|
-
local tokens
|
|
1136
|
-
|
|
1137
|
-
if bucket[1] == false then
|
|
1138
|
-
refilledAt = now
|
|
1139
|
-
tokens = maxTokens
|
|
1140
|
-
else
|
|
1141
|
-
refilledAt = tonumber(bucket[1])
|
|
1142
|
-
tokens = tonumber(bucket[2])
|
|
1143
|
-
end
|
|
1144
|
-
|
|
1145
|
-
if now >= refilledAt + interval then
|
|
1146
|
-
local numRefills = math.floor((now - refilledAt) / interval)
|
|
1147
|
-
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
|
|
1148
|
-
|
|
1149
|
-
refilledAt = refilledAt + numRefills * interval
|
|
1150
|
-
end
|
|
1151
|
-
|
|
1152
|
-
if tokens == 0 then
|
|
1153
|
-
return {-1, refilledAt + interval}
|
|
1154
|
-
end
|
|
1155
|
-
|
|
1156
|
-
local remaining = tokens - incrementBy
|
|
1157
|
-
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
|
|
1158
|
-
|
|
1159
|
-
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
1160
|
-
redis.call("PEXPIRE", key, expireAt)
|
|
1161
|
-
return {remaining, refilledAt + interval}
|
|
1162
|
-
`;
|
|
1163
|
-
var tokenBucketIdentifierNotFound = -1;
|
|
1164
|
-
var tokenBucketRemainingTokensScript = `
|
|
1165
|
-
local key = KEYS[1]
|
|
1166
|
-
local maxTokens = tonumber(ARGV[1])
|
|
1167
|
-
|
|
1168
|
-
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
1169
|
-
|
|
1170
|
-
if bucket[1] == false then
|
|
1171
|
-
return {maxTokens, ${tokenBucketIdentifierNotFound}}
|
|
1172
|
-
end
|
|
1173
|
-
|
|
1174
|
-
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
1175
|
-
`;
|
|
1176
|
-
var cachedFixedWindowLimitScript = `
|
|
1177
|
-
local key = KEYS[1]
|
|
1178
|
-
local window = ARGV[1]
|
|
1179
|
-
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
1180
|
-
|
|
1181
|
-
local r = redis.call("INCRBY", key, incrementBy)
|
|
1182
|
-
if r == incrementBy then
|
|
1183
|
-
-- The first time this key is set, the value will be equal to incrementBy.
|
|
1184
|
-
-- So we only need the expire command once
|
|
1185
|
-
redis.call("PEXPIRE", key, window)
|
|
1186
|
-
end
|
|
1187
|
-
|
|
1188
|
-
return r
|
|
1189
|
-
`;
|
|
1190
|
-
var cachedFixedWindowRemainingTokenScript = `
|
|
1191
|
-
local key = KEYS[1]
|
|
1192
|
-
local tokens = 0
|
|
1193
|
-
|
|
1194
|
-
local value = redis.call('GET', key)
|
|
1195
|
-
if value then
|
|
1196
|
-
tokens = value
|
|
1197
|
-
end
|
|
1198
|
-
return tokens
|
|
1199
|
-
`;
|
|
1200
|
-
|
|
1201
1251
|
// src/single.ts
|
|
1202
1252
|
var RegionRatelimit = class extends Ratelimit {
|
|
1203
1253
|
/**
|
|
@@ -1210,9 +1260,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1210
1260
|
timeout: config.timeout,
|
|
1211
1261
|
analytics: config.analytics,
|
|
1212
1262
|
ctx: {
|
|
1213
|
-
redis: config.redis
|
|
1214
|
-
scriptHashes: {},
|
|
1215
|
-
cacheScripts: config.cacheScripts ?? true
|
|
1263
|
+
redis: config.redis
|
|
1216
1264
|
},
|
|
1217
1265
|
ephemeralCache: config.ephemeralCache,
|
|
1218
1266
|
enableProtection: config.enableProtection,
|
|
@@ -1259,8 +1307,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1259
1307
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1260
1308
|
const usedTokensAfterUpdate = await safeEval(
|
|
1261
1309
|
ctx,
|
|
1262
|
-
|
|
1263
|
-
"limitHash",
|
|
1310
|
+
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1264
1311
|
[key],
|
|
1265
1312
|
[windowDuration, incrementBy]
|
|
1266
1313
|
);
|
|
@@ -1283,8 +1330,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1283
1330
|
const key = [identifier, bucket].join(":");
|
|
1284
1331
|
const usedTokens = await safeEval(
|
|
1285
1332
|
ctx,
|
|
1286
|
-
|
|
1287
|
-
"getRemainingHash",
|
|
1333
|
+
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1288
1334
|
[key],
|
|
1289
1335
|
[null]
|
|
1290
1336
|
);
|
|
@@ -1300,8 +1346,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1300
1346
|
}
|
|
1301
1347
|
await safeEval(
|
|
1302
1348
|
ctx,
|
|
1303
|
-
|
|
1304
|
-
"resetHash",
|
|
1349
|
+
RESET_SCRIPT,
|
|
1305
1350
|
[pattern],
|
|
1306
1351
|
[null]
|
|
1307
1352
|
);
|
|
@@ -1349,8 +1394,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1349
1394
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1350
1395
|
const remainingTokens = await safeEval(
|
|
1351
1396
|
ctx,
|
|
1352
|
-
|
|
1353
|
-
"limitHash",
|
|
1397
|
+
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1354
1398
|
[currentKey, previousKey],
|
|
1355
1399
|
[tokens, now, windowSize, incrementBy]
|
|
1356
1400
|
);
|
|
@@ -1375,8 +1419,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1375
1419
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1376
1420
|
const usedTokens = await safeEval(
|
|
1377
1421
|
ctx,
|
|
1378
|
-
|
|
1379
|
-
"getRemainingHash",
|
|
1422
|
+
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1380
1423
|
[currentKey, previousKey],
|
|
1381
1424
|
[now, windowSize]
|
|
1382
1425
|
);
|
|
@@ -1392,8 +1435,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1392
1435
|
}
|
|
1393
1436
|
await safeEval(
|
|
1394
1437
|
ctx,
|
|
1395
|
-
|
|
1396
|
-
"resetHash",
|
|
1438
|
+
RESET_SCRIPT,
|
|
1397
1439
|
[pattern],
|
|
1398
1440
|
[null]
|
|
1399
1441
|
);
|
|
@@ -1434,8 +1476,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1434
1476
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1435
1477
|
const [remaining, reset] = await safeEval(
|
|
1436
1478
|
ctx,
|
|
1437
|
-
|
|
1438
|
-
"limitHash",
|
|
1479
|
+
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1439
1480
|
[identifier],
|
|
1440
1481
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1441
1482
|
);
|
|
@@ -1454,8 +1495,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1454
1495
|
async getRemaining(ctx, identifier) {
|
|
1455
1496
|
const [remainingTokens, refilledAt] = await safeEval(
|
|
1456
1497
|
ctx,
|
|
1457
|
-
|
|
1458
|
-
"getRemainingHash",
|
|
1498
|
+
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1459
1499
|
[identifier],
|
|
1460
1500
|
[maxTokens]
|
|
1461
1501
|
);
|
|
@@ -1473,8 +1513,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1473
1513
|
}
|
|
1474
1514
|
await safeEval(
|
|
1475
1515
|
ctx,
|
|
1476
|
-
|
|
1477
|
-
"resetHash",
|
|
1516
|
+
RESET_SCRIPT,
|
|
1478
1517
|
[pattern],
|
|
1479
1518
|
[null]
|
|
1480
1519
|
);
|
|
@@ -1522,8 +1561,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1522
1561
|
const success = cachedTokensAfterUpdate < tokens;
|
|
1523
1562
|
const pending = success ? safeEval(
|
|
1524
1563
|
ctx,
|
|
1525
|
-
|
|
1526
|
-
"limitHash",
|
|
1564
|
+
SCRIPTS.singleRegion.cachedFixedWindow.limit,
|
|
1527
1565
|
[key],
|
|
1528
1566
|
[windowDuration, incrementBy]
|
|
1529
1567
|
) : Promise.resolve();
|
|
@@ -1537,8 +1575,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1537
1575
|
}
|
|
1538
1576
|
const usedTokensAfterUpdate = await safeEval(
|
|
1539
1577
|
ctx,
|
|
1540
|
-
|
|
1541
|
-
"limitHash",
|
|
1578
|
+
SCRIPTS.singleRegion.cachedFixedWindow.limit,
|
|
1542
1579
|
[key],
|
|
1543
1580
|
[windowDuration, incrementBy]
|
|
1544
1581
|
);
|
|
@@ -1568,8 +1605,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1568
1605
|
}
|
|
1569
1606
|
const usedTokens = await safeEval(
|
|
1570
1607
|
ctx,
|
|
1571
|
-
|
|
1572
|
-
"getRemainingHash",
|
|
1608
|
+
SCRIPTS.singleRegion.cachedFixedWindow.getRemaining,
|
|
1573
1609
|
[key],
|
|
1574
1610
|
[null]
|
|
1575
1611
|
);
|
|
@@ -1588,8 +1624,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1588
1624
|
const pattern = [identifier, "*"].join(":");
|
|
1589
1625
|
await safeEval(
|
|
1590
1626
|
ctx,
|
|
1591
|
-
|
|
1592
|
-
"resetHash",
|
|
1627
|
+
RESET_SCRIPT,
|
|
1593
1628
|
[pattern],
|
|
1594
1629
|
[null]
|
|
1595
1630
|
);
|