@upstash/ratelimit 2.0.1 → 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/README.md +1 -1
- package/dist/index.d.mts +7 -9
- package/dist/index.d.ts +7 -9
- 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.mjs
CHANGED
|
@@ -134,38 +134,181 @@ function ms(d) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// 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);
|
|
137
|
+
var safeEval = async (ctx, script, keys, args) => {
|
|
153
138
|
try {
|
|
154
|
-
return await ctx.redis.evalsha(
|
|
139
|
+
return await ctx.redis.evalsha(script.hash, keys, args);
|
|
155
140
|
} catch (error) {
|
|
156
141
|
if (`${error}`.includes("NOSCRIPT")) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
142
|
+
const hash = await ctx.redis.scriptLoad(script.script);
|
|
143
|
+
if (hash !== script.hash) {
|
|
144
|
+
console.warn(
|
|
145
|
+
"Upstash Ratelimit: Expected hash and the hash received from Redis are different. Ratelimit will work as usual but performance will be reduced."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return await ctx.redis.evalsha(hash, keys, args);
|
|
162
149
|
}
|
|
163
150
|
throw error;
|
|
164
151
|
}
|
|
165
152
|
};
|
|
166
153
|
|
|
167
|
-
// src/lua-scripts/
|
|
154
|
+
// src/lua-scripts/single.ts
|
|
168
155
|
var fixedWindowLimitScript = `
|
|
156
|
+
local key = KEYS[1]
|
|
157
|
+
local window = ARGV[1]
|
|
158
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
159
|
+
|
|
160
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
161
|
+
if r == tonumber(incrementBy) then
|
|
162
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
163
|
+
-- So we only need the expire command once
|
|
164
|
+
redis.call("PEXPIRE", key, window)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
return r
|
|
168
|
+
`;
|
|
169
|
+
var fixedWindowRemainingTokensScript = `
|
|
170
|
+
local key = KEYS[1]
|
|
171
|
+
local tokens = 0
|
|
172
|
+
|
|
173
|
+
local value = redis.call('GET', key)
|
|
174
|
+
if value then
|
|
175
|
+
tokens = value
|
|
176
|
+
end
|
|
177
|
+
return tokens
|
|
178
|
+
`;
|
|
179
|
+
var slidingWindowLimitScript = `
|
|
180
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
181
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
182
|
+
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
183
|
+
local now = ARGV[2] -- current timestamp in milliseconds
|
|
184
|
+
local window = ARGV[3] -- interval in milliseconds
|
|
185
|
+
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
|
|
186
|
+
|
|
187
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
188
|
+
if requestsInCurrentWindow == false then
|
|
189
|
+
requestsInCurrentWindow = 0
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
193
|
+
if requestsInPreviousWindow == false then
|
|
194
|
+
requestsInPreviousWindow = 0
|
|
195
|
+
end
|
|
196
|
+
local percentageInCurrent = ( now % window ) / window
|
|
197
|
+
-- weighted requests to consider from the previous window
|
|
198
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
199
|
+
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
|
|
200
|
+
return -1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
local newValue = redis.call("INCRBY", currentKey, incrementBy)
|
|
204
|
+
if newValue == tonumber(incrementBy) then
|
|
205
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
206
|
+
-- So we only need the expire command once
|
|
207
|
+
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
|
|
208
|
+
end
|
|
209
|
+
return tokens - ( newValue + requestsInPreviousWindow )
|
|
210
|
+
`;
|
|
211
|
+
var slidingWindowRemainingTokensScript = `
|
|
212
|
+
local currentKey = KEYS[1] -- identifier including prefixes
|
|
213
|
+
local previousKey = KEYS[2] -- key of the previous bucket
|
|
214
|
+
local now = ARGV[1] -- current timestamp in milliseconds
|
|
215
|
+
local window = ARGV[2] -- interval in milliseconds
|
|
216
|
+
|
|
217
|
+
local requestsInCurrentWindow = redis.call("GET", currentKey)
|
|
218
|
+
if requestsInCurrentWindow == false then
|
|
219
|
+
requestsInCurrentWindow = 0
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
local requestsInPreviousWindow = redis.call("GET", previousKey)
|
|
223
|
+
if requestsInPreviousWindow == false then
|
|
224
|
+
requestsInPreviousWindow = 0
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
local percentageInCurrent = ( now % window ) / window
|
|
228
|
+
-- weighted requests to consider from the previous window
|
|
229
|
+
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)
|
|
230
|
+
|
|
231
|
+
return requestsInPreviousWindow + requestsInCurrentWindow
|
|
232
|
+
`;
|
|
233
|
+
var tokenBucketLimitScript = `
|
|
234
|
+
local key = KEYS[1] -- identifier including prefixes
|
|
235
|
+
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
|
|
236
|
+
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
|
|
237
|
+
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
|
|
238
|
+
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
|
|
239
|
+
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
|
|
240
|
+
|
|
241
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
242
|
+
|
|
243
|
+
local refilledAt
|
|
244
|
+
local tokens
|
|
245
|
+
|
|
246
|
+
if bucket[1] == false then
|
|
247
|
+
refilledAt = now
|
|
248
|
+
tokens = maxTokens
|
|
249
|
+
else
|
|
250
|
+
refilledAt = tonumber(bucket[1])
|
|
251
|
+
tokens = tonumber(bucket[2])
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if now >= refilledAt + interval then
|
|
255
|
+
local numRefills = math.floor((now - refilledAt) / interval)
|
|
256
|
+
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
|
|
257
|
+
|
|
258
|
+
refilledAt = refilledAt + numRefills * interval
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
if tokens == 0 then
|
|
262
|
+
return {-1, refilledAt + interval}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
local remaining = tokens - incrementBy
|
|
266
|
+
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
|
|
267
|
+
|
|
268
|
+
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
|
|
269
|
+
redis.call("PEXPIRE", key, expireAt)
|
|
270
|
+
return {remaining, refilledAt + interval}
|
|
271
|
+
`;
|
|
272
|
+
var tokenBucketIdentifierNotFound = -1;
|
|
273
|
+
var tokenBucketRemainingTokensScript = `
|
|
274
|
+
local key = KEYS[1]
|
|
275
|
+
local maxTokens = tonumber(ARGV[1])
|
|
276
|
+
|
|
277
|
+
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
|
|
278
|
+
|
|
279
|
+
if bucket[1] == false then
|
|
280
|
+
return {maxTokens, ${tokenBucketIdentifierNotFound}}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
return {tonumber(bucket[2]), tonumber(bucket[1])}
|
|
284
|
+
`;
|
|
285
|
+
var cachedFixedWindowLimitScript = `
|
|
286
|
+
local key = KEYS[1]
|
|
287
|
+
local window = ARGV[1]
|
|
288
|
+
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
|
|
289
|
+
|
|
290
|
+
local r = redis.call("INCRBY", key, incrementBy)
|
|
291
|
+
if r == incrementBy then
|
|
292
|
+
-- The first time this key is set, the value will be equal to incrementBy.
|
|
293
|
+
-- So we only need the expire command once
|
|
294
|
+
redis.call("PEXPIRE", key, window)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
return r
|
|
298
|
+
`;
|
|
299
|
+
var cachedFixedWindowRemainingTokenScript = `
|
|
300
|
+
local key = KEYS[1]
|
|
301
|
+
local tokens = 0
|
|
302
|
+
|
|
303
|
+
local value = redis.call('GET', key)
|
|
304
|
+
if value then
|
|
305
|
+
tokens = value
|
|
306
|
+
end
|
|
307
|
+
return tokens
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
// src/lua-scripts/multi.ts
|
|
311
|
+
var fixedWindowLimitScript2 = `
|
|
169
312
|
local key = KEYS[1]
|
|
170
313
|
local id = ARGV[1]
|
|
171
314
|
local window = ARGV[2]
|
|
@@ -181,7 +324,7 @@ var fixedWindowLimitScript = `
|
|
|
181
324
|
|
|
182
325
|
return fields
|
|
183
326
|
`;
|
|
184
|
-
var
|
|
327
|
+
var fixedWindowRemainingTokensScript2 = `
|
|
185
328
|
local key = KEYS[1]
|
|
186
329
|
local tokens = 0
|
|
187
330
|
|
|
@@ -189,7 +332,7 @@ var fixedWindowRemainingTokensScript = `
|
|
|
189
332
|
|
|
190
333
|
return fields
|
|
191
334
|
`;
|
|
192
|
-
var
|
|
335
|
+
var slidingWindowLimitScript2 = `
|
|
193
336
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
194
337
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
195
338
|
local tokens = tonumber(ARGV[1]) -- tokens per window
|
|
@@ -224,7 +367,7 @@ var slidingWindowLimitScript = `
|
|
|
224
367
|
end
|
|
225
368
|
return {currentFields, previousFields, true}
|
|
226
369
|
`;
|
|
227
|
-
var
|
|
370
|
+
var slidingWindowRemainingTokensScript2 = `
|
|
228
371
|
local currentKey = KEYS[1] -- identifier including prefixes
|
|
229
372
|
local previousKey = KEYS[2] -- key of the previous bucket
|
|
230
373
|
local now = ARGV[1] -- current timestamp in milliseconds
|
|
@@ -273,6 +416,78 @@ var resetScript = `
|
|
|
273
416
|
until cursor == "0"
|
|
274
417
|
`;
|
|
275
418
|
|
|
419
|
+
// src/lua-scripts/hash.ts
|
|
420
|
+
var SCRIPTS = {
|
|
421
|
+
singleRegion: {
|
|
422
|
+
fixedWindow: {
|
|
423
|
+
limit: {
|
|
424
|
+
script: fixedWindowLimitScript,
|
|
425
|
+
hash: "b13943e359636db027ad280f1def143f02158c13"
|
|
426
|
+
},
|
|
427
|
+
getRemaining: {
|
|
428
|
+
script: fixedWindowRemainingTokensScript,
|
|
429
|
+
hash: "8c4c341934502aee132643ffbe58ead3450e5208"
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
slidingWindow: {
|
|
433
|
+
limit: {
|
|
434
|
+
script: slidingWindowLimitScript,
|
|
435
|
+
hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
|
|
436
|
+
},
|
|
437
|
+
getRemaining: {
|
|
438
|
+
script: slidingWindowRemainingTokensScript,
|
|
439
|
+
hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
tokenBucket: {
|
|
443
|
+
limit: {
|
|
444
|
+
script: tokenBucketLimitScript,
|
|
445
|
+
hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
|
|
446
|
+
},
|
|
447
|
+
getRemaining: {
|
|
448
|
+
script: tokenBucketRemainingTokensScript,
|
|
449
|
+
hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
cachedFixedWindow: {
|
|
453
|
+
limit: {
|
|
454
|
+
script: cachedFixedWindowLimitScript,
|
|
455
|
+
hash: "c26b12703dd137939b9a69a3a9b18e906a2d940f"
|
|
456
|
+
},
|
|
457
|
+
getRemaining: {
|
|
458
|
+
script: cachedFixedWindowRemainingTokenScript,
|
|
459
|
+
hash: "8e8f222ccae68b595ee6e3f3bf2199629a62b91a"
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
multiRegion: {
|
|
464
|
+
fixedWindow: {
|
|
465
|
+
limit: {
|
|
466
|
+
script: fixedWindowLimitScript2,
|
|
467
|
+
hash: "a8c14f3835aa87bd70e5e2116081b81664abcf5c"
|
|
468
|
+
},
|
|
469
|
+
getRemaining: {
|
|
470
|
+
script: fixedWindowRemainingTokensScript2,
|
|
471
|
+
hash: "8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd"
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
slidingWindow: {
|
|
475
|
+
limit: {
|
|
476
|
+
script: slidingWindowLimitScript2,
|
|
477
|
+
hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
|
|
478
|
+
},
|
|
479
|
+
getRemaining: {
|
|
480
|
+
script: slidingWindowRemainingTokensScript2,
|
|
481
|
+
hash: "558c9306b7ec54abb50747fe0b17e5d44bd24868"
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
var RESET_SCRIPT = {
|
|
487
|
+
script: resetScript,
|
|
488
|
+
hash: "54bd274ddc59fb3be0f42deee2f64322a10e2b50"
|
|
489
|
+
};
|
|
490
|
+
|
|
276
491
|
// src/types.ts
|
|
277
492
|
var DenyListExtension = "denyList";
|
|
278
493
|
var IpDenyListKey = "ipDenyList";
|
|
@@ -712,9 +927,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
712
927
|
analytics: config.analytics,
|
|
713
928
|
ctx: {
|
|
714
929
|
regionContexts: config.redis.map((redis) => ({
|
|
715
|
-
redis
|
|
716
|
-
scriptHashes: {},
|
|
717
|
-
cacheScripts: config.cacheScripts ?? true
|
|
930
|
+
redis
|
|
718
931
|
})),
|
|
719
932
|
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0
|
|
720
933
|
}
|
|
@@ -763,8 +976,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
763
976
|
redis: regionContext.redis,
|
|
764
977
|
request: safeEval(
|
|
765
978
|
regionContext,
|
|
766
|
-
|
|
767
|
-
"limitHash",
|
|
979
|
+
SCRIPTS.multiRegion.fixedWindow.limit,
|
|
768
980
|
[key],
|
|
769
981
|
[requestId, windowDuration, incrementBy]
|
|
770
982
|
)
|
|
@@ -839,8 +1051,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
839
1051
|
redis: regionContext.redis,
|
|
840
1052
|
request: safeEval(
|
|
841
1053
|
regionContext,
|
|
842
|
-
|
|
843
|
-
"getRemainingHash",
|
|
1054
|
+
SCRIPTS.multiRegion.fixedWindow.getRemaining,
|
|
844
1055
|
[key],
|
|
845
1056
|
[null]
|
|
846
1057
|
)
|
|
@@ -866,8 +1077,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
866
1077
|
await Promise.all(ctx.regionContexts.map((regionContext) => {
|
|
867
1078
|
safeEval(
|
|
868
1079
|
regionContext,
|
|
869
|
-
|
|
870
|
-
"resetHash",
|
|
1080
|
+
RESET_SCRIPT,
|
|
871
1081
|
[pattern],
|
|
872
1082
|
[null]
|
|
873
1083
|
);
|
|
@@ -920,8 +1130,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
920
1130
|
redis: regionContext.redis,
|
|
921
1131
|
request: safeEval(
|
|
922
1132
|
regionContext,
|
|
923
|
-
|
|
924
|
-
"limitHash",
|
|
1133
|
+
SCRIPTS.multiRegion.slidingWindow.limit,
|
|
925
1134
|
[currentKey, previousKey],
|
|
926
1135
|
[tokens, now, windowDuration, requestId, incrementBy]
|
|
927
1136
|
// lua seems to return `1` for true and `null` for false
|
|
@@ -1010,8 +1219,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1010
1219
|
redis: regionContext.redis,
|
|
1011
1220
|
request: safeEval(
|
|
1012
1221
|
regionContext,
|
|
1013
|
-
|
|
1014
|
-
"getRemainingHash",
|
|
1222
|
+
SCRIPTS.multiRegion.slidingWindow.getRemaining,
|
|
1015
1223
|
[currentKey, previousKey],
|
|
1016
1224
|
[now, windowSize]
|
|
1017
1225
|
// lua seems to return `1` for true and `null` for false
|
|
@@ -1031,8 +1239,7 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1031
1239
|
await Promise.all(ctx.regionContexts.map((regionContext) => {
|
|
1032
1240
|
safeEval(
|
|
1033
1241
|
regionContext,
|
|
1034
|
-
|
|
1035
|
-
"resetHash",
|
|
1242
|
+
RESET_SCRIPT,
|
|
1036
1243
|
[pattern],
|
|
1037
1244
|
[null]
|
|
1038
1245
|
);
|
|
@@ -1042,162 +1249,6 @@ var MultiRegionRatelimit = class extends Ratelimit {
|
|
|
1042
1249
|
}
|
|
1043
1250
|
};
|
|
1044
1251
|
|
|
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
1252
|
// src/single.ts
|
|
1202
1253
|
var RegionRatelimit = class extends Ratelimit {
|
|
1203
1254
|
/**
|
|
@@ -1210,9 +1261,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1210
1261
|
timeout: config.timeout,
|
|
1211
1262
|
analytics: config.analytics,
|
|
1212
1263
|
ctx: {
|
|
1213
|
-
redis: config.redis
|
|
1214
|
-
scriptHashes: {},
|
|
1215
|
-
cacheScripts: config.cacheScripts ?? true
|
|
1264
|
+
redis: config.redis
|
|
1216
1265
|
},
|
|
1217
1266
|
ephemeralCache: config.ephemeralCache,
|
|
1218
1267
|
enableProtection: config.enableProtection,
|
|
@@ -1259,8 +1308,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1259
1308
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1260
1309
|
const usedTokensAfterUpdate = await safeEval(
|
|
1261
1310
|
ctx,
|
|
1262
|
-
|
|
1263
|
-
"limitHash",
|
|
1311
|
+
SCRIPTS.singleRegion.fixedWindow.limit,
|
|
1264
1312
|
[key],
|
|
1265
1313
|
[windowDuration, incrementBy]
|
|
1266
1314
|
);
|
|
@@ -1283,8 +1331,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1283
1331
|
const key = [identifier, bucket].join(":");
|
|
1284
1332
|
const usedTokens = await safeEval(
|
|
1285
1333
|
ctx,
|
|
1286
|
-
|
|
1287
|
-
"getRemainingHash",
|
|
1334
|
+
SCRIPTS.singleRegion.fixedWindow.getRemaining,
|
|
1288
1335
|
[key],
|
|
1289
1336
|
[null]
|
|
1290
1337
|
);
|
|
@@ -1300,8 +1347,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1300
1347
|
}
|
|
1301
1348
|
await safeEval(
|
|
1302
1349
|
ctx,
|
|
1303
|
-
|
|
1304
|
-
"resetHash",
|
|
1350
|
+
RESET_SCRIPT,
|
|
1305
1351
|
[pattern],
|
|
1306
1352
|
[null]
|
|
1307
1353
|
);
|
|
@@ -1349,8 +1395,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1349
1395
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1350
1396
|
const remainingTokens = await safeEval(
|
|
1351
1397
|
ctx,
|
|
1352
|
-
|
|
1353
|
-
"limitHash",
|
|
1398
|
+
SCRIPTS.singleRegion.slidingWindow.limit,
|
|
1354
1399
|
[currentKey, previousKey],
|
|
1355
1400
|
[tokens, now, windowSize, incrementBy]
|
|
1356
1401
|
);
|
|
@@ -1375,8 +1420,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1375
1420
|
const previousKey = [identifier, previousWindow].join(":");
|
|
1376
1421
|
const usedTokens = await safeEval(
|
|
1377
1422
|
ctx,
|
|
1378
|
-
|
|
1379
|
-
"getRemainingHash",
|
|
1423
|
+
SCRIPTS.singleRegion.slidingWindow.getRemaining,
|
|
1380
1424
|
[currentKey, previousKey],
|
|
1381
1425
|
[now, windowSize]
|
|
1382
1426
|
);
|
|
@@ -1392,8 +1436,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1392
1436
|
}
|
|
1393
1437
|
await safeEval(
|
|
1394
1438
|
ctx,
|
|
1395
|
-
|
|
1396
|
-
"resetHash",
|
|
1439
|
+
RESET_SCRIPT,
|
|
1397
1440
|
[pattern],
|
|
1398
1441
|
[null]
|
|
1399
1442
|
);
|
|
@@ -1434,8 +1477,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1434
1477
|
const incrementBy = rate ? Math.max(1, rate) : 1;
|
|
1435
1478
|
const [remaining, reset] = await safeEval(
|
|
1436
1479
|
ctx,
|
|
1437
|
-
|
|
1438
|
-
"limitHash",
|
|
1480
|
+
SCRIPTS.singleRegion.tokenBucket.limit,
|
|
1439
1481
|
[identifier],
|
|
1440
1482
|
[maxTokens, intervalDuration, refillRate, now, incrementBy]
|
|
1441
1483
|
);
|
|
@@ -1454,8 +1496,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1454
1496
|
async getRemaining(ctx, identifier) {
|
|
1455
1497
|
const [remainingTokens, refilledAt] = await safeEval(
|
|
1456
1498
|
ctx,
|
|
1457
|
-
|
|
1458
|
-
"getRemainingHash",
|
|
1499
|
+
SCRIPTS.singleRegion.tokenBucket.getRemaining,
|
|
1459
1500
|
[identifier],
|
|
1460
1501
|
[maxTokens]
|
|
1461
1502
|
);
|
|
@@ -1473,8 +1514,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1473
1514
|
}
|
|
1474
1515
|
await safeEval(
|
|
1475
1516
|
ctx,
|
|
1476
|
-
|
|
1477
|
-
"resetHash",
|
|
1517
|
+
RESET_SCRIPT,
|
|
1478
1518
|
[pattern],
|
|
1479
1519
|
[null]
|
|
1480
1520
|
);
|
|
@@ -1522,8 +1562,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1522
1562
|
const success = cachedTokensAfterUpdate < tokens;
|
|
1523
1563
|
const pending = success ? safeEval(
|
|
1524
1564
|
ctx,
|
|
1525
|
-
|
|
1526
|
-
"limitHash",
|
|
1565
|
+
SCRIPTS.singleRegion.cachedFixedWindow.limit,
|
|
1527
1566
|
[key],
|
|
1528
1567
|
[windowDuration, incrementBy]
|
|
1529
1568
|
) : Promise.resolve();
|
|
@@ -1537,8 +1576,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1537
1576
|
}
|
|
1538
1577
|
const usedTokensAfterUpdate = await safeEval(
|
|
1539
1578
|
ctx,
|
|
1540
|
-
|
|
1541
|
-
"limitHash",
|
|
1579
|
+
SCRIPTS.singleRegion.cachedFixedWindow.limit,
|
|
1542
1580
|
[key],
|
|
1543
1581
|
[windowDuration, incrementBy]
|
|
1544
1582
|
);
|
|
@@ -1568,8 +1606,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1568
1606
|
}
|
|
1569
1607
|
const usedTokens = await safeEval(
|
|
1570
1608
|
ctx,
|
|
1571
|
-
|
|
1572
|
-
"getRemainingHash",
|
|
1609
|
+
SCRIPTS.singleRegion.cachedFixedWindow.getRemaining,
|
|
1573
1610
|
[key],
|
|
1574
1611
|
[null]
|
|
1575
1612
|
);
|
|
@@ -1588,8 +1625,7 @@ var RegionRatelimit = class extends Ratelimit {
|
|
|
1588
1625
|
const pattern = [identifier, "*"].join(":");
|
|
1589
1626
|
await safeEval(
|
|
1590
1627
|
ctx,
|
|
1591
|
-
|
|
1592
|
-
"resetHash",
|
|
1628
|
+
RESET_SCRIPT,
|
|
1593
1629
|
[pattern],
|
|
1594
1630
|
[null]
|
|
1595
1631
|
);
|