@upstash/ratelimit 2.0.2 → 2.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/dist/index.d.mts +6 -8
- package/dist/index.d.ts +6 -8
- package/dist/index.js +262 -226
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +262 -226
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -157,38 +157,181 @@ function ms(d) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// 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);
|
|
160
|
+
var safeEval = async (ctx, script, keys, args) => {
|
|
176
161
|
try {
|
|
177
|
-
return await ctx.redis.evalsha(
|
|
162
|
+
return await ctx.redis.evalsha(script.hash, keys, args);
|
|
178
163
|
} catch (error) {
|
|
179
164
|
if (`${error}`.includes("NOSCRIPT")) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
165
|
+
const hash = await ctx.redis.scriptLoad(script.script);
|
|
166
|
+
if (hash !== script.hash) {
|
|
167
|
+
console.warn(
|
|
168
|
+
"Upstash Ratelimit: Expected hash and the hash received from Redis are different. Ratelimit will work as usual but performance will be reduced."
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return await ctx.redis.evalsha(hash, keys, args);
|
|
185
172
|
}
|
|
186
173
|
throw error;
|
|
187
174
|
}
|
|
188
175
|
};
|
|
189
176
|
|
|
190
|
-
// src/lua-scripts/
|
|
177
|
+
// src/lua-scripts/single.ts
|
|
191
178
|
var fixedWindowLimitScript = `
|
|
179
|
+
local key = KEYS[1]
|
|
180
|
+
local window = ARGV[1]
|
|
181
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
182
|
+
|
|
183
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
184
|
+
if r == tonumber(incrementBy) then
|
|
185
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
186
|
+
-- So we only need the expire command once
|
|
187
|
+
redis.call("PEXPIRE", key, window)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
return r
|
|
191
|
+
`;
|
|
192
|
+
var fixedWindowRemainingTokensScript = `
|
|
193
|
+
local key = KEYS[1]
|
|
194
|
+
local tokens = 0
|
|
195
|
+
|
|
196
|
+
local value = redis.call('GET', key)
|
|
197
|
+
if value then
|
|
198
|
+
tokens = value
|
|
199
|
+
end
|
|
200
|
+
return tokens
|
|
201
|
+
`;
|
|
202
|
+
var slidingWindowLimitScript = `
|
|
203
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
204
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
205
|
+
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
206
|
+
local now = ARGV[2] -- current timestamp in milliseconds
|
|
207
|
+
local window = ARGV[3] -- interval in milliseconds
|
|
208
|
+
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
|
|
209
|
+
|
|
210
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
211
|
+
if requestsInCurrentWindow == false then
|
|
212
|
+
requestsInCurrentWindow = 0
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
216
|
+
if requestsInPreviousWindow == false then
|
|
217
|
+
requestsInPreviousWindow = 0
|
|
218
|
+
end
|
|
219
|
+
local percentageInCurrent = ( now % window ) / window
|
|
220
|
+
-- weighted requests to consider from the previous window
|
|
221
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
222
|
+
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
|
|
223
|
+
return -1
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
227
|
+
if newValue == tonumber(incrementBy) then
|
|
228
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
229
|
+
-- So we only need the expire command once
|
|
230
|
+
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
231
|
+
end
|
|
232
|
+
return tokens - ( newValue + requestsInPreviousWindow )
|
|
233
|
+
`;
|
|
234
|
+
var slidingWindowRemainingTokensScript = `
|
|
235
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
236
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
237
|
+
local now = ARGV[1] -- current timestamp in milliseconds
|
|
238
|
+
local window = ARGV[2] -- interval in milliseconds
|
|
239
|
+
|
|
240
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
241
|
+
if requestsInCurrentWindow == false then
|
|
242
|
+
requestsInCurrentWindow = 0
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
246
|
+
if requestsInPreviousWindow == false then
|
|
247
|
+
requestsInPreviousWindow = 0
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
local percentageInCurrent = ( now % window ) / window
|
|
251
|
+
-- weighted requests to consider from the previous window
|
|
252
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
253
|
+
|
|
254
|
+
return requestsInPreviousWindow + requestsInCurrentWindow
|
|
255
|
+
`;
|
|
256
|
+
var tokenBucketLimitScript = `
|
|
257
|
+
local key = KEYS[1] -- identifier including prefixes
|
|
258
|
+
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
259
|
+
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
260
|
+
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
261
|
+
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
262
|
+
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
263
|
+
|
|
264
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
265
|
+
|
|
266
|
+
local refilledAt
|
|
267
|
+
local tokens
|
|
268
|
+
|
|
269
|
+
if bucket[1] == false then
|
|
270
|
+
refilledAt = now
|
|
271
|
+
tokens = maxTokens
|
|
272
|
+
else
|
|
273
|
+
refilledAt = tonumber(bucket[1])
|
|
274
|
+
tokens = tonumber(bucket[2])
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
if now >= refilledAt + interval then
|
|
278
|
+
local numRefills = math.floor((now - refilledAt) / interval)
|
|
279
|
+
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
|
|
280
|
+
|
|
281
|
+
refilledAt = refilledAt + numRefills * interval
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
if tokens == 0 then
|
|
285
|
+
return {-1, refilledAt + interval}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
local remaining = tokens - incrementBy
|
|
289
|
+
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
|
|
290
|
+
|
|
291
|
+
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
292
|
+
redis.call("PEXPIRE", key, expireAt)
|
|
293
|
+
return {remaining, refilledAt + interval}
|
|
294
|
+
`;
|
|
295
|
+
var tokenBucketIdentifierNotFound = -1;
|
|
296
|
+
var tokenBucketRemainingTokensScript = `
|
|
297
|
+
local key = KEYS[1]
|
|
298
|
+
local maxTokens = tonumber(ARGV[1])
|
|
299
|
+
|
|
300
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
301
|
+
|
|
302
|
+
if bucket[1] == false then
|
|
303
|
+
return {maxTokens, ${tokenBucketIdentifierNotFound}}
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
307
|
+
`;
|
|
308
|
+
var cachedFixedWindowLimitScript = `
|
|
309
|
+
local key = KEYS[1]
|
|
310
|
+
local window = ARGV[1]
|
|
311
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
312
|
+
|
|
313
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
314
|
+
if r == incrementBy then
|
|
315
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
316
|
+
-- So we only need the expire command once
|
|
317
|
+
redis.call("PEXPIRE", key, window)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
return r
|
|
321
|
+
`;
|
|
322
|
+
var cachedFixedWindowRemainingTokenScript = `
|
|
323
|
+
local key = KEYS[1]
|
|
324
|
+
local tokens = 0
|
|
325
|
+
|
|
326
|
+
local value = redis.call('GET', key)
|
|
327
|
+
if value then
|
|
328
|
+
tokens = value
|
|
329
|
+
end
|
|
330
|
+
return tokens
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
// src/lua-scripts/multi.ts
|
|
334
|
+
var fixedWindowLimitScript2 = `
|
|
192
335
|
local key = KEYS[1]
|
|
193
336
|
local id = ARGV[1]
|
|
194
337
|
local window = ARGV[2]
|
|
@@ -204,7 +347,7 @@ var fixedWindowLimitScript = `
|
|
|
204
347
|
|
|
205
348
|
return fields
|
|
206
349
|
`;
|
|
207
|
-
var
|
|
350
|
+
var fixedWindowRemainingTokensScript2 = `
|
|
208
351
|
local key = KEYS[1]
|
|
209
352
|
local tokens = 0
|
|
210
353
|
|
|
@@ -212,7 +355,7 @@ var fixedWindowRemainingTokensScript = `
|
|
|
212
355
|
|
|
213
356
|
return fields
|
|
214
357
|
`;
|
|
215
|
-
var
|
|
358
|
+
var slidingWindowLimitScript2 = `
|
|
216
359
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
217
360
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
218
361
|
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
@@ -247,7 +390,7 @@ var slidingWindowLimitScript = `
|
|
|
247
390
|
end
|
|
248
391
|
return {currentFields, previousFields, true}
|
|
249
392
|
`;
|
|
250
|
-
var
|
|
393
|
+
var slidingWindowRemainingTokensScript2 = `
|
|
251
394
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
252
395
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
253
396
|
local now = ARGV[1] -- current timestamp in milliseconds
|
|
@@ -296,6 +439,78 @@ var resetScript = `
|
|
|
296
439
|
until cursor == "0"
|
|
297
440
|
`;
|
|
298
441
|
|
|
442
|
+
// src/lua-scripts/hash.ts
|
|
443
|
+
var SCRIPTS = {
|
|
444
|
+
singleRegion: {
|
|
445
|
+
fixedWindow: {
|
|
446
|
+
limit: {
|
|
447
|
+
script: fixedWindowLimitScript,
|
|
448
|
+
hash: "b13943e359636db027ad280f1def143f02158c13"
|
|
449
|
+
},
|
|
450
|
+
getRemaining: {
|
|
451
|
+
script: fixedWindowRemainingTokensScript,
|
|
452
|
+
hash: "8c4c341934502aee132643ffbe58ead3450e5208"
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
slidingWindow: {
|
|
456
|
+
limit: {
|
|
457
|
+
script: slidingWindowLimitScript,
|
|
458
|
+
hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
|
|
459
|
+
},
|
|
460
|
+
getRemaining: {
|
|
461
|
+
script: slidingWindowRemainingTokensScript,
|
|
462
|
+
hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
tokenBucket: {
|
|
466
|
+
limit: {
|
|
467
|
+
script: tokenBucketLimitScript,
|
|
468
|
+
hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
|
|
469
|
+
},
|
|
470
|
+
getRemaining: {
|
|
471
|
+
script: tokenBucketRemainingTokensScript,
|
|
472
|
+
hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
cachedFixedWindow: {
|
|
476
|
+
limit: {
|
|
477
|
+
script: cachedFixedWindowLimitScript,
|
|
478
|
+
hash: "c26b12703dd137939b9a69a3a9b18e906a2d940f"
|
|
479
|
+
},
|
|
480
|
+
getRemaining: {
|
|
481
|
+
script: cachedFixedWindowRemainingTokenScript,
|
|
482
|
+
hash: "8e8f222ccae68b595ee6e3f3bf2199629a62b91a"
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
multiRegion: {
|
|
487
|
+
fixedWindow: {
|
|
488
|
+
limit: {
|
|
489
|
+
script: fixedWindowLimitScript2,
|
|
490
|
+
hash: "a8c14f3835aa87bd70e5e2116081b81664abcf5c"
|
|
491
|
+
},
|
|
492
|
+
getRemaining: {
|
|
493
|
+
script: fixedWindowRemainingTokensScript2,
|
|
494
|
+
hash: "8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd"
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
slidingWindow: {
|
|
498
|
+
limit: {
|
|
499
|
+
script: slidingWindowLimitScript2,
|
|
500
|
+
hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
|
|
501
|
+
},
|
|
502
|
+
getRemaining: {
|
|
503
|
+
script: slidingWindowRemainingTokensScript2,
|
|
504
|
+
hash: "558c9306b7ec54abb50747fe0b17e5d44bd24868"
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var RESET_SCRIPT = {
|
|
510
|
+
script: resetScript,
|
|
511
|
+
hash: "54bd274ddc59fb3be0f42deee2f64322a10e2b50"
|
|
512
|
+
};
|
|
513
|
+
|
|
299
514
|
// src/types.ts
|
|
300
515
|
var DenyListExtension = "denyList";
|
|
301
516
|
var IpDenyListKey = "ipDenyList";
|
|
@@ -735,9 +950,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
735
950
|
analytics: config.analytics,
|
|
736
951
|
ctx: {
|
|
737
952
|
regionContexts: config.redis.map((redis) => ({
|
|
738
|
-
redis
|
|
739
|
-
scriptHashes: {},
|
|
740
|
-
cacheScripts: config.cacheScripts ?? true
|
|
953
|
+
redis
|
|
741
954
|
})),
|
|
742
955
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
743
956
|
}
|
|
@@ -786,8 +999,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
786
999
|
redis: regionContext.redis,
|
|
787
1000
|
request: safeEval(
|
|
788
1001
|
regionContext,
|
|
789
|
-
|
|
790
|
-
"limitHash",
|
|
1002
|
+
SCRIPTS.multiRegion.fixedWindow.limit,
|
|
791
1003
|
[key],
|
|
792
1004
|
[requestId, windowDuration, incrementBy]
|
|
793
1005
|
)
|
|
@@ -862,8 +1074,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
862
1074
|
redis: regionContext.redis,
|
|
863
1075
|
request: safeEval(
|
|
864
1076
|
regionContext,
|
|
865
|
-
|
|
866
|
-
"getRemainingHash",
|
|
1077
|
+
SCRIPTS.multiRegion.fixedWindow.getRemaining,
|
|
867
1078
|
[key],
|
|
868
1079
|
[null]
|
|
869
1080
|
)
|
|
@@ -889,8 +1100,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
889
1100
|
await Promise.all(ctx.regionContexts.map((regionContext) => {
|
|
890
1101
|
safeEval(
|
|
891
1102
|
regionContext,
|
|
892
|
-
|
|
893
|
-
"resetHash",
|
|
1103
|
+
RESET_SCRIPT,
|
|
894
1104
|
[pattern],
|
|
895
1105
|
[null]
|
|
896
1106
|
);
|
|
@@ -943,8 +1153,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
943
1153
|
redis: regionContext.redis,
|
|
944
1154
|
request: safeEval(
|
|
945
1155
|
regionContext,
|
|
946
|
-
|
|
947
|
-
"limitHash",
|
|
1156
|
+
SCRIPTS.multiRegion.slidingWindow.limit,
|
|
948
1157
|
[currentKey, previousKey],
|
|
949
1158
|
[tokens, now, windowDuration, requestId, incrementBy]
|
|
950
1159
|
// lua seems to return `1` for true and `null` for false
|
|
@@ -1033,8 +1242,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1033
1242
|
redis: regionContext.redis,
|
|
1034
1243
|
request: safeEval(
|
|
1035
1244
|
regionContext,
|
|
1036
|
-
|
|
1037
|
-
"getRemainingHash",
|
|
1245
|
+
SCRIPTS.multiRegion.slidingWindow.getRemaining,
|
|
1038
1246
|
[currentKey, previousKey],
|
|
1039
1247
|
[now, windowSize]
|
|
1040
1248
|
// lua seems to return `1` for true and `null` for false
|
|
@@ -1054,8 +1262,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1054
1262
|
await Promise.all(ctx.regionContexts.map((regionContext) => {
|
|
1055
1263
|
safeEval(
|
|
1056
1264
|
regionContext,
|
|
1057
|
-
|
|
1058
|
-
"resetHash",
|
|
1265
|
+
RESET_SCRIPT,
|
|
1059
1266
|
[pattern],
|
|
1060
1267
|
[null]
|
|
1061
1268
|
);
|
|
@@ -1065,162 +1272,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1065
1272
|
}
|
|
1066
1273
|
};
|
|
1067
1274
|
|
|
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
1275
|
// src/single.ts
|
|
1225
1276
|
var RegionRatelimit = class extends Ratelimit {
|
|
1226
1277
|
/**
|
|
@@ -1233,9 +1284,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1233
1284
|
timeout: config.timeout,
|
|
1234
1285
|
analytics: config.analytics,
|
|
1235
1286
|
ctx: {
|
|
1236
|
-
redis: config.redis
|
|
1237
|
-
scriptHashes: {},
|
|
1238
|
-
cacheScripts: config.cacheScripts ?? true
|
|
1287
|
+
redis: config.redis
|
|
1239
1288
|
},
|
|
1240
1289
|
ephemeralCache: config.ephemeralCache,
|
|
1241
1290
|
enableProtection: config.enableProtection,
|
|
@@ -1282,8 +1331,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1282
1331
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1283
1332
|
const usedTokensAfterUpdate = await safeEval(
|
|
1284
1333
|
ctx,
|
|
1285
|
-
|
|
1286
|
-
"limitHash",
|
|
1334
|
+
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1287
1335
|
[key],
|
|
1288
1336
|
[windowDuration, incrementBy]
|
|
1289
1337
|
);
|
|
@@ -1306,8 +1354,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1306
1354
|
const key = [identifier, bucket].join(":");
|
|
1307
1355
|
const usedTokens = await safeEval(
|
|
1308
1356
|
ctx,
|
|
1309
|
-
|
|
1310
|
-
"getRemainingHash",
|
|
1357
|
+
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1311
1358
|
[key],
|
|
1312
1359
|
[null]
|
|
1313
1360
|
);
|
|
@@ -1323,8 +1370,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1323
1370
|
}
|
|
1324
1371
|
await safeEval(
|
|
1325
1372
|
ctx,
|
|
1326
|
-
|
|
1327
|
-
"resetHash",
|
|
1373
|
+
RESET_SCRIPT,
|
|
1328
1374
|
[pattern],
|
|
1329
1375
|
[null]
|
|
1330
1376
|
);
|
|
@@ -1372,8 +1418,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1372
1418
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1373
1419
|
const remainingTokens = await safeEval(
|
|
1374
1420
|
ctx,
|
|
1375
|
-
|
|
1376
|
-
"limitHash",
|
|
1421
|
+
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1377
1422
|
[currentKey, previousKey],
|
|
1378
1423
|
[tokens, now, windowSize, incrementBy]
|
|
1379
1424
|
);
|
|
@@ -1398,8 +1443,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1398
1443
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1399
1444
|
const usedTokens = await safeEval(
|
|
1400
1445
|
ctx,
|
|
1401
|
-
|
|
1402
|
-
"getRemainingHash",
|
|
1446
|
+
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1403
1447
|
[currentKey, previousKey],
|
|
1404
1448
|
[now, windowSize]
|
|
1405
1449
|
);
|
|
@@ -1415,8 +1459,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1415
1459
|
}
|
|
1416
1460
|
await safeEval(
|
|
1417
1461
|
ctx,
|
|
1418
|
-
|
|
1419
|
-
"resetHash",
|
|
1462
|
+
RESET_SCRIPT,
|
|
1420
1463
|
[pattern],
|
|
1421
1464
|
[null]
|
|
1422
1465
|
);
|
|
@@ -1457,8 +1500,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1457
1500
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1458
1501
|
const [remaining, reset] = await safeEval(
|
|
1459
1502
|
ctx,
|
|
1460
|
-
|
|
1461
|
-
"limitHash",
|
|
1503
|
+
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1462
1504
|
[identifier],
|
|
1463
1505
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1464
1506
|
);
|
|
@@ -1477,8 +1519,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1477
1519
|
async getRemaining(ctx, identifier) {
|
|
1478
1520
|
const [remainingTokens, refilledAt] = await safeEval(
|
|
1479
1521
|
ctx,
|
|
1480
|
-
|
|
1481
|
-
"getRemainingHash",
|
|
1522
|
+
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1482
1523
|
[identifier],
|
|
1483
1524
|
[maxTokens]
|
|
1484
1525
|
);
|
|
@@ -1496,8 +1537,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1496
1537
|
}
|
|
1497
1538
|
await safeEval(
|
|
1498
1539
|
ctx,
|
|
1499
|
-
|
|
1500
|
-
"resetHash",
|
|
1540
|
+
RESET_SCRIPT,
|
|
1501
1541
|
[pattern],
|
|
1502
1542
|
[null]
|
|
1503
1543
|
);
|
|
@@ -1545,8 +1585,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1545
1585
|
const success = cachedTokensAfterUpdate < tokens;
|
|
1546
1586
|
const pending = success ? safeEval(
|
|
1547
1587
|
ctx,
|
|
1548
|
-
|
|
1549
|
-
"limitHash",
|
|
1588
|
+
SCRIPTS.singleRegion.cachedFixedWindow.limit,
|
|
1550
1589
|
[key],
|
|
1551
1590
|
[windowDuration, incrementBy]
|
|
1552
1591
|
) : Promise.resolve();
|
|
@@ -1560,8 +1599,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1560
1599
|
}
|
|
1561
1600
|
const usedTokensAfterUpdate = await safeEval(
|
|
1562
1601
|
ctx,
|
|
1563
|
-
|
|
1564
|
-
"limitHash",
|
|
1602
|
+
SCRIPTS.singleRegion.cachedFixedWindow.limit,
|
|
1565
1603
|
[key],
|
|
1566
1604
|
[windowDuration, incrementBy]
|
|
1567
1605
|
);
|
|
@@ -1591,8 +1629,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1591
1629
|
}
|
|
1592
1630
|
const usedTokens = await safeEval(
|
|
1593
1631
|
ctx,
|
|
1594
|
-
|
|
1595
|
-
"getRemainingHash",
|
|
1632
|
+
SCRIPTS.singleRegion.cachedFixedWindow.getRemaining,
|
|
1596
1633
|
[key],
|
|
1597
1634
|
[null]
|
|
1598
1635
|
);
|
|
@@ -1611,8 +1648,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1611
1648
|
const pattern = [identifier, "*"].join(":");
|
|
1612
1649
|
await safeEval(
|
|
1613
1650
|
ctx,
|
|
1614
|
-
|
|
1615
|
-
"resetHash",
|
|
1651
|
+
RESET_SCRIPT,
|
|
1616
1652
|
[pattern],
|
|
1617
1653
|
[null]
|
|
1618
1654
|
);
|