@upstash/ratelimit 0.1.5 → 0.2.0-rc.0
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 +5 -4
- package/esm/_dnt.shims.js +62 -0
- package/esm/cache.js +12 -0
- package/esm/multi.js +4 -2
- package/esm/ratelimit.js +37 -2
- package/esm/single.js +84 -0
- package/package.json +12 -4
- package/script/_dnt.shims.js +66 -0
- package/script/cache.js +12 -0
- package/script/multi.js +27 -2
- package/script/ratelimit.js +39 -3
- package/script/single.js +84 -0
- package/types/_dnt.shims.d.ts +5 -0
- package/types/cache.d.ts +3 -0
- package/types/duration.d.ts +2 -2
- package/types/multi.d.ts +7 -1
- package/types/ratelimit.d.ts +12 -2
- package/types/single.d.ts +40 -1
- package/types/types.d.ts +8 -5
package/README.md
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
It is the only connectionless (HTTP based) rate limiting library and designed
|
|
7
7
|
for:
|
|
8
8
|
|
|
9
|
-
- Serverless functions (AWS Lambda ...)
|
|
9
|
+
- Serverless functions (AWS Lambda, Vercel ...)
|
|
10
10
|
- Cloudflare Workers
|
|
11
|
-
-
|
|
11
|
+
- Vercel Edge
|
|
12
|
+
- Fastly Compute@Edge
|
|
12
13
|
- Next.js, Jamstack ...
|
|
13
14
|
- Client side web/mobile applications
|
|
14
15
|
- WebAssembly
|
|
@@ -25,7 +26,7 @@ for:
|
|
|
25
26
|
- [Use it](#use-it)
|
|
26
27
|
- [Block until ready](#block-until-ready)
|
|
27
28
|
- [Ephemeral Cache](#ephemeral-cache)
|
|
28
|
-
- [MultiRegion replicated ratelimiting](#multiregion-replicated-ratelimiting)
|
|
29
|
+
- [MultiRegion replicated ratelimiting](#multiregion-replicated-ratelimiting)
|
|
29
30
|
- [Usage](#usage)
|
|
30
31
|
- [Asynchronous synchronization between databases](#asynchronous-synchronization-between-databases)
|
|
31
32
|
- [Example](#example)
|
|
@@ -243,7 +244,7 @@ const ratelimit = new MultiRegionRatelimit({
|
|
|
243
244
|
/* auth */
|
|
244
245
|
}),
|
|
245
246
|
],
|
|
246
|
-
limiter:
|
|
247
|
+
limiter: MultiRegionRatelimit.slidingWindow(10, "10 s"),
|
|
247
248
|
});
|
|
248
249
|
|
|
249
250
|
// Use a constant string to limit all requests with a single ratelimit
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { crypto } from "@deno/shim-crypto";
|
|
2
|
+
export { crypto } from "@deno/shim-crypto";
|
|
3
|
+
const dntGlobals = {
|
|
4
|
+
crypto,
|
|
5
|
+
};
|
|
6
|
+
export const dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
|
|
7
|
+
// deno-lint-ignore ban-types
|
|
8
|
+
function createMergeProxy(baseObj, extObj) {
|
|
9
|
+
return new Proxy(baseObj, {
|
|
10
|
+
get(_target, prop, _receiver) {
|
|
11
|
+
if (prop in extObj) {
|
|
12
|
+
return extObj[prop];
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
return baseObj[prop];
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
set(_target, prop, value) {
|
|
19
|
+
if (prop in extObj) {
|
|
20
|
+
delete extObj[prop];
|
|
21
|
+
}
|
|
22
|
+
baseObj[prop] = value;
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
deleteProperty(_target, prop) {
|
|
26
|
+
let success = false;
|
|
27
|
+
if (prop in extObj) {
|
|
28
|
+
delete extObj[prop];
|
|
29
|
+
success = true;
|
|
30
|
+
}
|
|
31
|
+
if (prop in baseObj) {
|
|
32
|
+
delete baseObj[prop];
|
|
33
|
+
success = true;
|
|
34
|
+
}
|
|
35
|
+
return success;
|
|
36
|
+
},
|
|
37
|
+
ownKeys(_target) {
|
|
38
|
+
const baseKeys = Reflect.ownKeys(baseObj);
|
|
39
|
+
const extKeys = Reflect.ownKeys(extObj);
|
|
40
|
+
const extKeysSet = new Set(extKeys);
|
|
41
|
+
return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
|
|
42
|
+
},
|
|
43
|
+
defineProperty(_target, prop, desc) {
|
|
44
|
+
if (prop in extObj) {
|
|
45
|
+
delete extObj[prop];
|
|
46
|
+
}
|
|
47
|
+
Reflect.defineProperty(baseObj, prop, desc);
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
51
|
+
if (prop in extObj) {
|
|
52
|
+
return Reflect.getOwnPropertyDescriptor(extObj, prop);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return Reflect.getOwnPropertyDescriptor(baseObj, prop);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
has(_target, prop) {
|
|
59
|
+
return prop in extObj || prop in baseObj;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
package/esm/cache.js
CHANGED
|
@@ -25,4 +25,16 @@ export class Cache {
|
|
|
25
25
|
blockUntil(identifier, reset) {
|
|
26
26
|
this.cache.set(identifier, reset);
|
|
27
27
|
}
|
|
28
|
+
set(key, value) {
|
|
29
|
+
this.cache.set(key, value);
|
|
30
|
+
}
|
|
31
|
+
get(key) {
|
|
32
|
+
return this.cache.get(key) || null;
|
|
33
|
+
}
|
|
34
|
+
incr(key) {
|
|
35
|
+
let value = this.cache.get(key) ?? 0;
|
|
36
|
+
value += 1;
|
|
37
|
+
this.cache.set(key, value);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
28
40
|
}
|
package/esm/multi.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
1
2
|
import { ms } from "./duration.js";
|
|
2
3
|
import { Ratelimit } from "./ratelimit.js";
|
|
3
4
|
import { Cache } from "./cache.js";
|
|
@@ -24,6 +25,7 @@ export class MultiRegionRatelimit extends Ratelimit {
|
|
|
24
25
|
super({
|
|
25
26
|
prefix: config.prefix,
|
|
26
27
|
limiter: config.limiter,
|
|
28
|
+
timeout: config.timeout,
|
|
27
29
|
ctx: {
|
|
28
30
|
redis: config.redis,
|
|
29
31
|
cache: config.ephemeralCache
|
|
@@ -88,7 +90,7 @@ export class MultiRegionRatelimit extends Ratelimit {
|
|
|
88
90
|
};
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
|
-
const requestID = crypto.randomUUID();
|
|
93
|
+
const requestID = dntShim.crypto.randomUUID();
|
|
92
94
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
93
95
|
const key = [identifier, bucket].join(":");
|
|
94
96
|
const dbs = ctx.redis.map((redis) => ({
|
|
@@ -208,7 +210,7 @@ export class MultiRegionRatelimit extends Ratelimit {
|
|
|
208
210
|
};
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
|
-
const requestID = crypto.randomUUID();
|
|
213
|
+
const requestID = dntShim.crypto.randomUUID();
|
|
212
214
|
const now = Date.now();
|
|
213
215
|
const currentWindow = Math.floor(now / windowSize);
|
|
214
216
|
const currentKey = [identifier, currentWindow].join(":");
|
package/esm/ratelimit.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { Cache } from "./cache.js";
|
|
2
|
+
export class TimeoutError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("Timeout");
|
|
5
|
+
this.name = "TimeoutError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
9
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
4
10
|
*
|
|
@@ -9,7 +15,7 @@ import { Cache } from "./cache.js";
|
|
|
9
15
|
* limiter: Ratelimit.slidingWindow(
|
|
10
16
|
* 10, // Allow 10 requests per window of 30 minutes
|
|
11
17
|
* "30 m", // interval of 30 minutes
|
|
12
|
-
* )
|
|
18
|
+
* ),
|
|
13
19
|
* })
|
|
14
20
|
*
|
|
15
21
|
* ```
|
|
@@ -34,6 +40,12 @@ export class Ratelimit {
|
|
|
34
40
|
writable: true,
|
|
35
41
|
value: void 0
|
|
36
42
|
});
|
|
43
|
+
Object.defineProperty(this, "timeout", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
value: void 0
|
|
48
|
+
});
|
|
37
49
|
/**
|
|
38
50
|
* Determine if a request should pass or be rejected based on the identifier and previously chosen ratelimit.
|
|
39
51
|
*
|
|
@@ -59,7 +71,29 @@ export class Ratelimit {
|
|
|
59
71
|
writable: true,
|
|
60
72
|
value: async (identifier) => {
|
|
61
73
|
const key = [this.prefix, identifier].join(":");
|
|
62
|
-
|
|
74
|
+
let timeoutId = null;
|
|
75
|
+
try {
|
|
76
|
+
const arr = [this.limiter(this.ctx, key)];
|
|
77
|
+
if (this.timeout) {
|
|
78
|
+
arr.push(new Promise((resolve) => {
|
|
79
|
+
timeoutId = setTimeout(() => {
|
|
80
|
+
resolve({
|
|
81
|
+
success: true,
|
|
82
|
+
limit: 0,
|
|
83
|
+
remaining: 0,
|
|
84
|
+
reset: 0,
|
|
85
|
+
pending: Promise.resolve(),
|
|
86
|
+
});
|
|
87
|
+
}, this.timeout);
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
return await Promise.race(arr);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (timeoutId) {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
63
97
|
}
|
|
64
98
|
});
|
|
65
99
|
/**
|
|
@@ -125,6 +159,7 @@ export class Ratelimit {
|
|
|
125
159
|
});
|
|
126
160
|
this.ctx = config.ctx;
|
|
127
161
|
this.limiter = config.limiter;
|
|
162
|
+
this.timeout = config.timeout;
|
|
128
163
|
this.prefix = config.prefix ?? "@upstash/ratelimit";
|
|
129
164
|
if (config.ephemeralCache instanceof Map) {
|
|
130
165
|
this.ctx.cache = new Cache(config.ephemeralCache);
|
package/esm/single.js
CHANGED
|
@@ -23,6 +23,7 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
23
23
|
super({
|
|
24
24
|
prefix: config.prefix,
|
|
25
25
|
limiter: config.limiter,
|
|
26
|
+
timeout: config.timeout,
|
|
26
27
|
ctx: {
|
|
27
28
|
redis: config.redis,
|
|
28
29
|
},
|
|
@@ -288,4 +289,87 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
288
289
|
};
|
|
289
290
|
};
|
|
290
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* cachedFixedWindow first uses the local cache to decide if a request may pass and then updates
|
|
294
|
+
* it asynchronously.
|
|
295
|
+
* This is experimental and not yet recommended for production use.
|
|
296
|
+
*
|
|
297
|
+
* @experimental
|
|
298
|
+
*
|
|
299
|
+
* Each requests inside a fixed time increases a counter.
|
|
300
|
+
* Once the counter reaches a maxmimum allowed number, all further requests are
|
|
301
|
+
* rejected.
|
|
302
|
+
*
|
|
303
|
+
* **Pro:**
|
|
304
|
+
*
|
|
305
|
+
* - Newer requests are not starved by old ones.
|
|
306
|
+
* - Low storage cost.
|
|
307
|
+
*
|
|
308
|
+
* **Con:**
|
|
309
|
+
*
|
|
310
|
+
* A burst of requests near the boundary of a window can result in a very
|
|
311
|
+
* high request rate because two windows will be filled with requests quickly.
|
|
312
|
+
*
|
|
313
|
+
* @param tokens - How many requests a user can make in each time window.
|
|
314
|
+
* @param window - A fixed timeframe
|
|
315
|
+
*/
|
|
316
|
+
static cachedFixedWindow(
|
|
317
|
+
/**
|
|
318
|
+
* How many requests are allowed per window.
|
|
319
|
+
*/
|
|
320
|
+
tokens,
|
|
321
|
+
/**
|
|
322
|
+
* The duration in which `tokens` requests are allowed.
|
|
323
|
+
*/
|
|
324
|
+
window) {
|
|
325
|
+
const windowDuration = ms(window);
|
|
326
|
+
const script = `
|
|
327
|
+
local key = KEYS[1]
|
|
328
|
+
local window = ARGV[1]
|
|
329
|
+
|
|
330
|
+
local r = redis.call("INCR", key)
|
|
331
|
+
if r == 1 then
|
|
332
|
+
-- The first time this key is set, the value will be 1.
|
|
333
|
+
-- So we only need the expire command once
|
|
334
|
+
redis.call("PEXPIRE", key, window)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
return r
|
|
338
|
+
`;
|
|
339
|
+
return async function (ctx, identifier) {
|
|
340
|
+
if (!ctx.cache) {
|
|
341
|
+
throw new Error("This algorithm requires a cache");
|
|
342
|
+
}
|
|
343
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
344
|
+
const key = [identifier, bucket].join(":");
|
|
345
|
+
const reset = (bucket + 1) * windowDuration;
|
|
346
|
+
const hit = typeof ctx.cache.get(key) === "number";
|
|
347
|
+
if (hit) {
|
|
348
|
+
const cachedTokensAfterUpdate = ctx.cache.incr(key);
|
|
349
|
+
const success = cachedTokensAfterUpdate < tokens;
|
|
350
|
+
const pending = success
|
|
351
|
+
? ctx.redis.eval(script, [key], [windowDuration]).then((t) => {
|
|
352
|
+
ctx.cache.set(key, t);
|
|
353
|
+
})
|
|
354
|
+
: Promise.resolve();
|
|
355
|
+
return {
|
|
356
|
+
success,
|
|
357
|
+
limit: tokens,
|
|
358
|
+
remaining: tokens - cachedTokensAfterUpdate,
|
|
359
|
+
reset: reset,
|
|
360
|
+
pending,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const usedTokensAfterUpdate = (await ctx.redis.eval(script, [key], [windowDuration]));
|
|
364
|
+
ctx.cache.set(key, usedTokensAfterUpdate);
|
|
365
|
+
const remaining = tokens - usedTokensAfterUpdate;
|
|
366
|
+
return {
|
|
367
|
+
success: remaining >= 0,
|
|
368
|
+
limit: tokens,
|
|
369
|
+
remaining,
|
|
370
|
+
reset: reset,
|
|
371
|
+
pending: Promise.resolve(),
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
}
|
|
291
375
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"main": "./script/mod.js",
|
|
4
4
|
"types": "./types/mod.d.ts",
|
|
5
5
|
"name": "@upstash/ratelimit",
|
|
6
|
-
"version": "v0.
|
|
6
|
+
"version": "v0.2.0-rc.0",
|
|
7
7
|
"description": "A serverless ratelimiter built on top of Upstash REST API.",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -43,9 +43,17 @@
|
|
|
43
43
|
],
|
|
44
44
|
"exports": {
|
|
45
45
|
".": {
|
|
46
|
-
"import":
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
"import": {
|
|
47
|
+
"types": "./types/mod.d.ts",
|
|
48
|
+
"default": "./esm/mod.js"
|
|
49
|
+
},
|
|
50
|
+
"require": {
|
|
51
|
+
"types": "./types/mod.d.ts",
|
|
52
|
+
"default": "./script/mod.js"
|
|
53
|
+
}
|
|
49
54
|
}
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@deno/shim-crypto": "~0.3.1"
|
|
50
58
|
}
|
|
51
59
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dntGlobalThis = exports.crypto = void 0;
|
|
4
|
+
const shim_crypto_1 = require("@deno/shim-crypto");
|
|
5
|
+
var shim_crypto_2 = require("@deno/shim-crypto");
|
|
6
|
+
Object.defineProperty(exports, "crypto", { enumerable: true, get: function () { return shim_crypto_2.crypto; } });
|
|
7
|
+
const dntGlobals = {
|
|
8
|
+
crypto: shim_crypto_1.crypto,
|
|
9
|
+
};
|
|
10
|
+
exports.dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
|
|
11
|
+
// deno-lint-ignore ban-types
|
|
12
|
+
function createMergeProxy(baseObj, extObj) {
|
|
13
|
+
return new Proxy(baseObj, {
|
|
14
|
+
get(_target, prop, _receiver) {
|
|
15
|
+
if (prop in extObj) {
|
|
16
|
+
return extObj[prop];
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
return baseObj[prop];
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
set(_target, prop, value) {
|
|
23
|
+
if (prop in extObj) {
|
|
24
|
+
delete extObj[prop];
|
|
25
|
+
}
|
|
26
|
+
baseObj[prop] = value;
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
deleteProperty(_target, prop) {
|
|
30
|
+
let success = false;
|
|
31
|
+
if (prop in extObj) {
|
|
32
|
+
delete extObj[prop];
|
|
33
|
+
success = true;
|
|
34
|
+
}
|
|
35
|
+
if (prop in baseObj) {
|
|
36
|
+
delete baseObj[prop];
|
|
37
|
+
success = true;
|
|
38
|
+
}
|
|
39
|
+
return success;
|
|
40
|
+
},
|
|
41
|
+
ownKeys(_target) {
|
|
42
|
+
const baseKeys = Reflect.ownKeys(baseObj);
|
|
43
|
+
const extKeys = Reflect.ownKeys(extObj);
|
|
44
|
+
const extKeysSet = new Set(extKeys);
|
|
45
|
+
return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
|
|
46
|
+
},
|
|
47
|
+
defineProperty(_target, prop, desc) {
|
|
48
|
+
if (prop in extObj) {
|
|
49
|
+
delete extObj[prop];
|
|
50
|
+
}
|
|
51
|
+
Reflect.defineProperty(baseObj, prop, desc);
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
55
|
+
if (prop in extObj) {
|
|
56
|
+
return Reflect.getOwnPropertyDescriptor(extObj, prop);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return Reflect.getOwnPropertyDescriptor(baseObj, prop);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
has(_target, prop) {
|
|
63
|
+
return prop in extObj || prop in baseObj;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
package/script/cache.js
CHANGED
|
@@ -28,5 +28,17 @@ class Cache {
|
|
|
28
28
|
blockUntil(identifier, reset) {
|
|
29
29
|
this.cache.set(identifier, reset);
|
|
30
30
|
}
|
|
31
|
+
set(key, value) {
|
|
32
|
+
this.cache.set(key, value);
|
|
33
|
+
}
|
|
34
|
+
get(key) {
|
|
35
|
+
return this.cache.get(key) || null;
|
|
36
|
+
}
|
|
37
|
+
incr(key) {
|
|
38
|
+
let value = this.cache.get(key) ?? 0;
|
|
39
|
+
value += 1;
|
|
40
|
+
this.cache.set(key, value);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
31
43
|
}
|
|
32
44
|
exports.Cache = Cache;
|
package/script/multi.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
26
|
exports.MultiRegionRatelimit = void 0;
|
|
27
|
+
const dntShim = __importStar(require("./_dnt.shims.js"));
|
|
4
28
|
const duration_js_1 = require("./duration.js");
|
|
5
29
|
const ratelimit_js_1 = require("./ratelimit.js");
|
|
6
30
|
const cache_js_1 = require("./cache.js");
|
|
@@ -27,6 +51,7 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
27
51
|
super({
|
|
28
52
|
prefix: config.prefix,
|
|
29
53
|
limiter: config.limiter,
|
|
54
|
+
timeout: config.timeout,
|
|
30
55
|
ctx: {
|
|
31
56
|
redis: config.redis,
|
|
32
57
|
cache: config.ephemeralCache
|
|
@@ -91,7 +116,7 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
91
116
|
};
|
|
92
117
|
}
|
|
93
118
|
}
|
|
94
|
-
const requestID = crypto.randomUUID();
|
|
119
|
+
const requestID = dntShim.crypto.randomUUID();
|
|
95
120
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
96
121
|
const key = [identifier, bucket].join(":");
|
|
97
122
|
const dbs = ctx.redis.map((redis) => ({
|
|
@@ -211,7 +236,7 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
211
236
|
};
|
|
212
237
|
}
|
|
213
238
|
}
|
|
214
|
-
const requestID = crypto.randomUUID();
|
|
239
|
+
const requestID = dntShim.crypto.randomUUID();
|
|
215
240
|
const now = Date.now();
|
|
216
241
|
const currentWindow = Math.floor(now / windowSize);
|
|
217
242
|
const currentKey = [identifier, currentWindow].join(":");
|
package/script/ratelimit.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Ratelimit = void 0;
|
|
3
|
+
exports.Ratelimit = exports.TimeoutError = void 0;
|
|
4
4
|
const cache_js_1 = require("./cache.js");
|
|
5
|
+
class TimeoutError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Timeout");
|
|
8
|
+
this.name = "TimeoutError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.TimeoutError = TimeoutError;
|
|
5
12
|
/**
|
|
6
13
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
7
14
|
*
|
|
@@ -12,7 +19,7 @@ const cache_js_1 = require("./cache.js");
|
|
|
12
19
|
* limiter: Ratelimit.slidingWindow(
|
|
13
20
|
* 10, // Allow 10 requests per window of 30 minutes
|
|
14
21
|
* "30 m", // interval of 30 minutes
|
|
15
|
-
* )
|
|
22
|
+
* ),
|
|
16
23
|
* })
|
|
17
24
|
*
|
|
18
25
|
* ```
|
|
@@ -37,6 +44,12 @@ class Ratelimit {
|
|
|
37
44
|
writable: true,
|
|
38
45
|
value: void 0
|
|
39
46
|
});
|
|
47
|
+
Object.defineProperty(this, "timeout", {
|
|
48
|
+
enumerable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
value: void 0
|
|
52
|
+
});
|
|
40
53
|
/**
|
|
41
54
|
* Determine if a request should pass or be rejected based on the identifier and previously chosen ratelimit.
|
|
42
55
|
*
|
|
@@ -62,7 +75,29 @@ class Ratelimit {
|
|
|
62
75
|
writable: true,
|
|
63
76
|
value: async (identifier) => {
|
|
64
77
|
const key = [this.prefix, identifier].join(":");
|
|
65
|
-
|
|
78
|
+
let timeoutId = null;
|
|
79
|
+
try {
|
|
80
|
+
const arr = [this.limiter(this.ctx, key)];
|
|
81
|
+
if (this.timeout) {
|
|
82
|
+
arr.push(new Promise((resolve) => {
|
|
83
|
+
timeoutId = setTimeout(() => {
|
|
84
|
+
resolve({
|
|
85
|
+
success: true,
|
|
86
|
+
limit: 0,
|
|
87
|
+
remaining: 0,
|
|
88
|
+
reset: 0,
|
|
89
|
+
pending: Promise.resolve(),
|
|
90
|
+
});
|
|
91
|
+
}, this.timeout);
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
return await Promise.race(arr);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (timeoutId) {
|
|
98
|
+
clearTimeout(timeoutId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
66
101
|
}
|
|
67
102
|
});
|
|
68
103
|
/**
|
|
@@ -128,6 +163,7 @@ class Ratelimit {
|
|
|
128
163
|
});
|
|
129
164
|
this.ctx = config.ctx;
|
|
130
165
|
this.limiter = config.limiter;
|
|
166
|
+
this.timeout = config.timeout;
|
|
131
167
|
this.prefix = config.prefix ?? "@upstash/ratelimit";
|
|
132
168
|
if (config.ephemeralCache instanceof Map) {
|
|
133
169
|
this.ctx.cache = new cache_js_1.Cache(config.ephemeralCache);
|
package/script/single.js
CHANGED
|
@@ -26,6 +26,7 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
26
26
|
super({
|
|
27
27
|
prefix: config.prefix,
|
|
28
28
|
limiter: config.limiter,
|
|
29
|
+
timeout: config.timeout,
|
|
29
30
|
ctx: {
|
|
30
31
|
redis: config.redis,
|
|
31
32
|
},
|
|
@@ -291,5 +292,88 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
291
292
|
};
|
|
292
293
|
};
|
|
293
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* cachedFixedWindow first uses the local cache to decide if a request may pass and then updates
|
|
297
|
+
* it asynchronously.
|
|
298
|
+
* This is experimental and not yet recommended for production use.
|
|
299
|
+
*
|
|
300
|
+
* @experimental
|
|
301
|
+
*
|
|
302
|
+
* Each requests inside a fixed time increases a counter.
|
|
303
|
+
* Once the counter reaches a maxmimum allowed number, all further requests are
|
|
304
|
+
* rejected.
|
|
305
|
+
*
|
|
306
|
+
* **Pro:**
|
|
307
|
+
*
|
|
308
|
+
* - Newer requests are not starved by old ones.
|
|
309
|
+
* - Low storage cost.
|
|
310
|
+
*
|
|
311
|
+
* **Con:**
|
|
312
|
+
*
|
|
313
|
+
* A burst of requests near the boundary of a window can result in a very
|
|
314
|
+
* high request rate because two windows will be filled with requests quickly.
|
|
315
|
+
*
|
|
316
|
+
* @param tokens - How many requests a user can make in each time window.
|
|
317
|
+
* @param window - A fixed timeframe
|
|
318
|
+
*/
|
|
319
|
+
static cachedFixedWindow(
|
|
320
|
+
/**
|
|
321
|
+
* How many requests are allowed per window.
|
|
322
|
+
*/
|
|
323
|
+
tokens,
|
|
324
|
+
/**
|
|
325
|
+
* The duration in which `tokens` requests are allowed.
|
|
326
|
+
*/
|
|
327
|
+
window) {
|
|
328
|
+
const windowDuration = (0, duration_js_1.ms)(window);
|
|
329
|
+
const script = `
|
|
330
|
+
local key = KEYS[1]
|
|
331
|
+
local window = ARGV[1]
|
|
332
|
+
|
|
333
|
+
local r = redis.call("INCR", key)
|
|
334
|
+
if r == 1 then
|
|
335
|
+
-- The first time this key is set, the value will be 1.
|
|
336
|
+
-- So we only need the expire command once
|
|
337
|
+
redis.call("PEXPIRE", key, window)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
return r
|
|
341
|
+
`;
|
|
342
|
+
return async function (ctx, identifier) {
|
|
343
|
+
if (!ctx.cache) {
|
|
344
|
+
throw new Error("This algorithm requires a cache");
|
|
345
|
+
}
|
|
346
|
+
const bucket = Math.floor(Date.now() / windowDuration);
|
|
347
|
+
const key = [identifier, bucket].join(":");
|
|
348
|
+
const reset = (bucket + 1) * windowDuration;
|
|
349
|
+
const hit = typeof ctx.cache.get(key) === "number";
|
|
350
|
+
if (hit) {
|
|
351
|
+
const cachedTokensAfterUpdate = ctx.cache.incr(key);
|
|
352
|
+
const success = cachedTokensAfterUpdate < tokens;
|
|
353
|
+
const pending = success
|
|
354
|
+
? ctx.redis.eval(script, [key], [windowDuration]).then((t) => {
|
|
355
|
+
ctx.cache.set(key, t);
|
|
356
|
+
})
|
|
357
|
+
: Promise.resolve();
|
|
358
|
+
return {
|
|
359
|
+
success,
|
|
360
|
+
limit: tokens,
|
|
361
|
+
remaining: tokens - cachedTokensAfterUpdate,
|
|
362
|
+
reset: reset,
|
|
363
|
+
pending,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const usedTokensAfterUpdate = (await ctx.redis.eval(script, [key], [windowDuration]));
|
|
367
|
+
ctx.cache.set(key, usedTokensAfterUpdate);
|
|
368
|
+
const remaining = tokens - usedTokensAfterUpdate;
|
|
369
|
+
return {
|
|
370
|
+
success: remaining >= 0,
|
|
371
|
+
limit: tokens,
|
|
372
|
+
remaining,
|
|
373
|
+
reset: reset,
|
|
374
|
+
pending: Promise.resolve(),
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
}
|
|
294
378
|
}
|
|
295
379
|
exports.RegionRatelimit = RegionRatelimit;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { crypto, type Crypto, type SubtleCrypto, type AlgorithmIdentifier, type Algorithm, type RsaOaepParams, type BufferSource, type AesCtrParams, type AesCbcParams, type AesGcmParams, type CryptoKey, type KeyAlgorithm, type KeyType, type KeyUsage, type EcdhKeyDeriveParams, type HkdfParams, type HashAlgorithmIdentifier, type Pbkdf2Params, type AesDerivedKeyParams, type HmacImportParams, type JsonWebKey, type RsaOtherPrimesInfo, type KeyFormat, type RsaHashedKeyGenParams, type RsaKeyGenParams, type BigInteger, type EcKeyGenParams, type NamedCurve, type CryptoKeyPair, type AesKeyGenParams, type HmacKeyGenParams, type RsaHashedImportParams, type EcKeyImportParams, type AesKeyAlgorithm, type RsaPssParams, type EcdsaParams } from "@deno/shim-crypto";
|
|
2
|
+
export {} from "@types/node";
|
|
3
|
+
export declare const dntGlobalThis: Omit<typeof globalThis, "crypto"> & {
|
|
4
|
+
crypto: import("@deno/shim-crypto").Crypto;
|
|
5
|
+
};
|
package/types/cache.d.ts
CHANGED
package/types/duration.d.ts
CHANGED
package/types/multi.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Duration } from "./duration.js";
|
|
|
2
2
|
import type { Algorithm, MultiRegionContext } from "./types.js";
|
|
3
3
|
import { Ratelimit } from "./ratelimit.js";
|
|
4
4
|
import type { Redis } from "./types.js";
|
|
5
|
-
export
|
|
5
|
+
export type MultiRegionRatelimitConfig = {
|
|
6
6
|
/**
|
|
7
7
|
* Instances of `@upstash/redis`
|
|
8
8
|
* @see https://github.com/upstash/upstash-redis#quick-start
|
|
@@ -39,6 +39,12 @@ export declare type MultiRegionRatelimitConfig = {
|
|
|
39
39
|
* if the map or th ratelimit instance is created outside your serverless function handler.
|
|
40
40
|
*/
|
|
41
41
|
ephemeralCache?: Map<string, number> | false;
|
|
42
|
+
/**
|
|
43
|
+
* If set, the ratelimiter will allow requests to pass after this many milliseconds.
|
|
44
|
+
*
|
|
45
|
+
* Use this if you want to allow requests in case of network problems
|
|
46
|
+
*/
|
|
47
|
+
timeout?: number;
|
|
42
48
|
};
|
|
43
49
|
/**
|
|
44
50
|
* Ratelimiter using serverless redis from https://upstash.com/
|
package/types/ratelimit.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Algorithm, Context, RatelimitResponse } from "./types.js";
|
|
2
|
-
export declare
|
|
2
|
+
export declare class TimeoutError extends Error {
|
|
3
|
+
constructor();
|
|
4
|
+
}
|
|
5
|
+
export type RatelimitConfig<TContext> = {
|
|
3
6
|
/**
|
|
4
7
|
* The ratelimiter function to use.
|
|
5
8
|
*
|
|
@@ -35,6 +38,12 @@ export declare type RatelimitConfig<TContext> = {
|
|
|
35
38
|
* if the map or the ratelimit instance is created outside your serverless function handler.
|
|
36
39
|
*/
|
|
37
40
|
ephemeralCache?: Map<string, number> | false;
|
|
41
|
+
/**
|
|
42
|
+
* If set, the ratelimiter will allow requests to pass after this many milliseconds.
|
|
43
|
+
*
|
|
44
|
+
* Use this if you want to allow requests in case of network problems
|
|
45
|
+
*/
|
|
46
|
+
timeout?: number;
|
|
38
47
|
};
|
|
39
48
|
/**
|
|
40
49
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
@@ -46,7 +55,7 @@ export declare type RatelimitConfig<TContext> = {
|
|
|
46
55
|
* limiter: Ratelimit.slidingWindow(
|
|
47
56
|
* 10, // Allow 10 requests per window of 30 minutes
|
|
48
57
|
* "30 m", // interval of 30 minutes
|
|
49
|
-
* )
|
|
58
|
+
* ),
|
|
50
59
|
* })
|
|
51
60
|
*
|
|
52
61
|
* ```
|
|
@@ -55,6 +64,7 @@ export declare abstract class Ratelimit<TContext extends Context> {
|
|
|
55
64
|
protected readonly limiter: Algorithm<TContext>;
|
|
56
65
|
protected readonly ctx: TContext;
|
|
57
66
|
protected readonly prefix: string;
|
|
67
|
+
protected readonly timeout?: number;
|
|
58
68
|
constructor(config: RatelimitConfig<TContext>);
|
|
59
69
|
/**
|
|
60
70
|
* Determine if a request should pass or be rejected based on the identifier and previously chosen ratelimit.
|
package/types/single.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Duration } from "./duration.js";
|
|
|
2
2
|
import type { Algorithm, RegionContext } from "./types.js";
|
|
3
3
|
import type { Redis } from "./types.js";
|
|
4
4
|
import { Ratelimit } from "./ratelimit.js";
|
|
5
|
-
export
|
|
5
|
+
export type RegionRatelimitConfig = {
|
|
6
6
|
/**
|
|
7
7
|
* Instance of `@upstash/redis`
|
|
8
8
|
* @see https://github.com/upstash/upstash-redis#quick-start
|
|
@@ -42,6 +42,12 @@ export declare type RegionRatelimitConfig = {
|
|
|
42
42
|
* if the map or the ratelimit instance is created outside your serverless function handler.
|
|
43
43
|
*/
|
|
44
44
|
ephemeralCache?: Map<string, number> | false;
|
|
45
|
+
/**
|
|
46
|
+
* If set, the ratelimiter will allow requests to pass after this many milliseconds.
|
|
47
|
+
*
|
|
48
|
+
* Use this if you want to allow requests in case of network problems
|
|
49
|
+
*/
|
|
50
|
+
timeout?: number;
|
|
45
51
|
};
|
|
46
52
|
/**
|
|
47
53
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
@@ -145,4 +151,37 @@ export declare class RegionRatelimit extends Ratelimit<RegionContext> {
|
|
|
145
151
|
* Useful to allow higher burst limits.
|
|
146
152
|
*/
|
|
147
153
|
maxTokens: number): Algorithm<RegionContext>;
|
|
154
|
+
/**
|
|
155
|
+
* cachedFixedWindow first uses the local cache to decide if a request may pass and then updates
|
|
156
|
+
* it asynchronously.
|
|
157
|
+
* This is experimental and not yet recommended for production use.
|
|
158
|
+
*
|
|
159
|
+
* @experimental
|
|
160
|
+
*
|
|
161
|
+
* Each requests inside a fixed time increases a counter.
|
|
162
|
+
* Once the counter reaches a maxmimum allowed number, all further requests are
|
|
163
|
+
* rejected.
|
|
164
|
+
*
|
|
165
|
+
* **Pro:**
|
|
166
|
+
*
|
|
167
|
+
* - Newer requests are not starved by old ones.
|
|
168
|
+
* - Low storage cost.
|
|
169
|
+
*
|
|
170
|
+
* **Con:**
|
|
171
|
+
*
|
|
172
|
+
* A burst of requests near the boundary of a window can result in a very
|
|
173
|
+
* high request rate because two windows will be filled with requests quickly.
|
|
174
|
+
*
|
|
175
|
+
* @param tokens - How many requests a user can make in each time window.
|
|
176
|
+
* @param window - A fixed timeframe
|
|
177
|
+
*/
|
|
178
|
+
static cachedFixedWindow(
|
|
179
|
+
/**
|
|
180
|
+
* How many requests are allowed per window.
|
|
181
|
+
*/
|
|
182
|
+
tokens: number,
|
|
183
|
+
/**
|
|
184
|
+
* The duration in which `tokens` requests are allowed.
|
|
185
|
+
*/
|
|
186
|
+
window: Duration): Algorithm<RegionContext>;
|
|
148
187
|
}
|
package/types/types.d.ts
CHANGED
|
@@ -11,17 +11,20 @@ export interface EphemeralCache {
|
|
|
11
11
|
reset: number;
|
|
12
12
|
};
|
|
13
13
|
blockUntil: (identifier: string, reset: number) => void;
|
|
14
|
+
set: (key: string, value: number) => void;
|
|
15
|
+
get: (key: string) => number | null;
|
|
16
|
+
incr: (key: string) => number;
|
|
14
17
|
}
|
|
15
|
-
export
|
|
18
|
+
export type RegionContext = {
|
|
16
19
|
redis: Redis;
|
|
17
20
|
cache?: EphemeralCache;
|
|
18
21
|
};
|
|
19
|
-
export
|
|
22
|
+
export type MultiRegionContext = {
|
|
20
23
|
redis: Redis[];
|
|
21
24
|
cache?: EphemeralCache;
|
|
22
25
|
};
|
|
23
|
-
export
|
|
24
|
-
export
|
|
26
|
+
export type Context = RegionContext | MultiRegionContext;
|
|
27
|
+
export type RatelimitResponse = {
|
|
25
28
|
/**
|
|
26
29
|
* Whether the request may pass(true) or exceeded the limit(false)
|
|
27
30
|
*/
|
|
@@ -62,6 +65,6 @@ export declare type RatelimitResponse = {
|
|
|
62
65
|
*/
|
|
63
66
|
pending: Promise<unknown>;
|
|
64
67
|
};
|
|
65
|
-
export
|
|
68
|
+
export type Algorithm<TContext> = (ctx: TContext, identifier: string, opts?: {
|
|
66
69
|
cache?: EphemeralCache;
|
|
67
70
|
}) => Promise<RatelimitResponse>;
|