@upstash/ratelimit 0.1.2 → 0.1.3-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 +9 -9
- package/esm/mod.js +2 -2
- package/esm/{global.js → multi.js} +5 -5
- package/esm/ratelimit.js +1 -1
- package/esm/{region.js → single.js} +9 -1
- package/package.json +1 -1
- package/script/mod.js +5 -5
- package/script/{global.js → multi.js} +7 -7
- package/script/ratelimit.js +1 -1
- package/script/{region.js → single.js} +9 -1
- package/types/mod.d.ts +4 -4
- package/types/{global.d.ts → multi.d.ts} +10 -10
- package/types/{region.d.ts → single.d.ts} +0 -0
- package/types/types.d.ts +25 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Upstash
|
|
1
|
+
# Upstash Ratelimit
|
|
2
2
|
|
|
3
3
|
An HTTP/REST based Redis client built on top of Upstash REST API.
|
|
4
4
|
[Upstash REST API](https://docs.upstash.com/features/restapi).
|
|
@@ -155,15 +155,15 @@ doExpensiveCalculation();
|
|
|
155
155
|
return "Here you go!";
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
##
|
|
158
|
+
## MultiRegionly replicated ratelimiting
|
|
159
159
|
|
|
160
160
|
Using a single redis instance has the downside of providing low latencies to the
|
|
161
161
|
part of your userbase closest to the deployed db. That's why we also built
|
|
162
|
-
`
|
|
163
|
-
well as offering lower latencies to more of your users.
|
|
162
|
+
`MultiRegionRatelimit` which replicates the state across multiple redis
|
|
163
|
+
databases as well as offering lower latencies to more of your users.
|
|
164
164
|
|
|
165
|
-
`
|
|
166
|
-
returning immediately. Only afterwards will the state be asynchronously
|
|
165
|
+
`MultiRegionRatelimit` does this by checking the current limit in the closest db
|
|
166
|
+
and returning immediately. Only afterwards will the state be asynchronously
|
|
167
167
|
replicated to the other datbases leveraging
|
|
168
168
|
[CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type). Due
|
|
169
169
|
to the nature of distributed systems, there is no way to guarantee the set
|
|
@@ -175,11 +175,11 @@ global latency.
|
|
|
175
175
|
The api is the same, except for asking for multiple redis instances:
|
|
176
176
|
|
|
177
177
|
```ts
|
|
178
|
-
import {
|
|
178
|
+
import { MultiRegionRatelimit } from "@upstash/ratelimit"; // for deno: see above
|
|
179
179
|
import { Redis } from "@upstash/redis";
|
|
180
180
|
|
|
181
181
|
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
|
182
|
-
const ratelimit = new
|
|
182
|
+
const ratelimit = new MultiRegionRatelimit({
|
|
183
183
|
redis: [
|
|
184
184
|
new Redis({/* auth */}),
|
|
185
185
|
new Redis({/* auth */}),
|
|
@@ -275,7 +275,7 @@ const ratelimit = new Ratelimit({
|
|
|
275
275
|
|
|
276
276
|
### Token Bucket
|
|
277
277
|
|
|
278
|
-
_Not yet supported for `
|
|
278
|
+
_Not yet supported for `MultiRegionRatelimit`_
|
|
279
279
|
|
|
280
280
|
Consider a bucket filled with `{maxTokens}` tokens that refills constantly at
|
|
281
281
|
`{refillRate}` per `{interval}`. Every request will remove one token from the
|
package/esm/mod.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { RegionRatelimit as Ratelimit } from "./
|
|
2
|
-
export {
|
|
1
|
+
export { RegionRatelimit as Ratelimit } from "./single.js";
|
|
2
|
+
export { MultiRegionRatelimit } from "./multi.js";
|
|
@@ -5,9 +5,9 @@ import { Ratelimit } from "./ratelimit.js";
|
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* ```ts
|
|
8
|
-
* const { limit } = new
|
|
8
|
+
* const { limit } = new MultiRegionRatelimit({
|
|
9
9
|
* redis: Redis.fromEnv(),
|
|
10
|
-
* limiter:
|
|
10
|
+
* limiter: MultiRegionRatelimit.fixedWindow(
|
|
11
11
|
* 10, // Allow 10 requests per window of 30 minutes
|
|
12
12
|
* "30 m", // interval of 30 minutes
|
|
13
13
|
* )
|
|
@@ -15,7 +15,7 @@ import { Ratelimit } from "./ratelimit.js";
|
|
|
15
15
|
*
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
-
export class
|
|
18
|
+
export class MultiRegionRatelimit extends Ratelimit {
|
|
19
19
|
/**
|
|
20
20
|
* Create a new Ratelimit instance by providing a `@upstash/redis` instance and the algorithn of your choice.
|
|
21
21
|
*/
|
|
@@ -108,12 +108,12 @@ export class GlobalRatelimit extends Ratelimit {
|
|
|
108
108
|
/**
|
|
109
109
|
* Do not await sync. This should not run in the critical path.
|
|
110
110
|
*/
|
|
111
|
-
sync();
|
|
112
111
|
return {
|
|
113
112
|
success: remaining > 0,
|
|
114
113
|
limit: tokens,
|
|
115
114
|
remaining,
|
|
116
115
|
reset: (bucket + 1) * windowDuration,
|
|
116
|
+
pending: sync(),
|
|
117
117
|
};
|
|
118
118
|
};
|
|
119
119
|
}
|
|
@@ -216,12 +216,12 @@ export class GlobalRatelimit extends Ratelimit {
|
|
|
216
216
|
/**
|
|
217
217
|
* Do not await sync. This should not run in the critical path.
|
|
218
218
|
*/
|
|
219
|
-
sync();
|
|
220
219
|
return {
|
|
221
220
|
success: remaining > 0,
|
|
222
221
|
limit: tokens,
|
|
223
222
|
remaining,
|
|
224
223
|
reset: (currentWindow + 1) * windowDuration,
|
|
224
|
+
pending: sync(),
|
|
225
225
|
};
|
|
226
226
|
};
|
|
227
227
|
}
|
package/esm/ratelimit.js
CHANGED
|
@@ -92,7 +92,7 @@ export class Ratelimit {
|
|
|
92
92
|
* An identifier per user or api.
|
|
93
93
|
* Choose a userID, or api token, or ip address.
|
|
94
94
|
*
|
|
95
|
-
* If you want to
|
|
95
|
+
* If you want to limit your api across all users, you can set a constant string.
|
|
96
96
|
*/
|
|
97
97
|
identifier,
|
|
98
98
|
/**
|
|
@@ -76,6 +76,7 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
76
76
|
limit: tokens,
|
|
77
77
|
remaining: tokens - usedTokensAfterUpdate,
|
|
78
78
|
reset: (bucket + 1) * windowDuration,
|
|
79
|
+
pending: Promise.resolve(),
|
|
79
80
|
};
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -147,6 +148,7 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
147
148
|
limit: tokens,
|
|
148
149
|
remaining,
|
|
149
150
|
reset: (currentWindow + 1) * windowSize,
|
|
151
|
+
pending: Promise.resolve(),
|
|
150
152
|
};
|
|
151
153
|
};
|
|
152
154
|
}
|
|
@@ -226,7 +228,13 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
226
228
|
const now = Date.now();
|
|
227
229
|
const key = [identifier, Math.floor(now / intervalDuration)].join(":");
|
|
228
230
|
const [remaining, reset] = (await ctx.redis.eval(script, [key], [maxTokens, intervalDuration, refillRate, now]));
|
|
229
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
success: remaining > 0,
|
|
233
|
+
limit: maxTokens,
|
|
234
|
+
remaining,
|
|
235
|
+
reset,
|
|
236
|
+
pending: Promise.resolve(),
|
|
237
|
+
};
|
|
230
238
|
};
|
|
231
239
|
}
|
|
232
240
|
}
|
package/package.json
CHANGED
package/script/mod.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
var
|
|
5
|
-
Object.defineProperty(exports, "Ratelimit", { enumerable: true, get: function () { return
|
|
6
|
-
var
|
|
7
|
-
Object.defineProperty(exports, "
|
|
3
|
+
exports.MultiRegionRatelimit = exports.Ratelimit = void 0;
|
|
4
|
+
var single_js_1 = require("./single.js");
|
|
5
|
+
Object.defineProperty(exports, "Ratelimit", { enumerable: true, get: function () { return single_js_1.RegionRatelimit; } });
|
|
6
|
+
var multi_js_1 = require("./multi.js");
|
|
7
|
+
Object.defineProperty(exports, "MultiRegionRatelimit", { enumerable: true, get: function () { return multi_js_1.MultiRegionRatelimit; } });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.MultiRegionRatelimit = void 0;
|
|
4
4
|
const duration_js_1 = require("./duration.js");
|
|
5
5
|
const ratelimit_js_1 = require("./ratelimit.js");
|
|
6
6
|
/**
|
|
@@ -8,9 +8,9 @@ const ratelimit_js_1 = require("./ratelimit.js");
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
11
|
-
* const { limit } = new
|
|
11
|
+
* const { limit } = new MultiRegionRatelimit({
|
|
12
12
|
* redis: Redis.fromEnv(),
|
|
13
|
-
* limiter:
|
|
13
|
+
* limiter: MultiRegionRatelimit.fixedWindow(
|
|
14
14
|
* 10, // Allow 10 requests per window of 30 minutes
|
|
15
15
|
* "30 m", // interval of 30 minutes
|
|
16
16
|
* )
|
|
@@ -18,7 +18,7 @@ const ratelimit_js_1 = require("./ratelimit.js");
|
|
|
18
18
|
*
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
class
|
|
21
|
+
class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
22
22
|
/**
|
|
23
23
|
* Create a new Ratelimit instance by providing a `@upstash/redis` instance and the algorithn of your choice.
|
|
24
24
|
*/
|
|
@@ -111,12 +111,12 @@ class GlobalRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
111
111
|
/**
|
|
112
112
|
* Do not await sync. This should not run in the critical path.
|
|
113
113
|
*/
|
|
114
|
-
sync();
|
|
115
114
|
return {
|
|
116
115
|
success: remaining > 0,
|
|
117
116
|
limit: tokens,
|
|
118
117
|
remaining,
|
|
119
118
|
reset: (bucket + 1) * windowDuration,
|
|
119
|
+
pending: sync(),
|
|
120
120
|
};
|
|
121
121
|
};
|
|
122
122
|
}
|
|
@@ -219,14 +219,14 @@ class GlobalRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
219
219
|
/**
|
|
220
220
|
* Do not await sync. This should not run in the critical path.
|
|
221
221
|
*/
|
|
222
|
-
sync();
|
|
223
222
|
return {
|
|
224
223
|
success: remaining > 0,
|
|
225
224
|
limit: tokens,
|
|
226
225
|
remaining,
|
|
227
226
|
reset: (currentWindow + 1) * windowDuration,
|
|
227
|
+
pending: sync(),
|
|
228
228
|
};
|
|
229
229
|
};
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
-
exports.
|
|
232
|
+
exports.MultiRegionRatelimit = MultiRegionRatelimit;
|
package/script/ratelimit.js
CHANGED
|
@@ -95,7 +95,7 @@ class Ratelimit {
|
|
|
95
95
|
* An identifier per user or api.
|
|
96
96
|
* Choose a userID, or api token, or ip address.
|
|
97
97
|
*
|
|
98
|
-
* If you want to
|
|
98
|
+
* If you want to limit your api across all users, you can set a constant string.
|
|
99
99
|
*/
|
|
100
100
|
identifier,
|
|
101
101
|
/**
|
|
@@ -79,6 +79,7 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
79
79
|
limit: tokens,
|
|
80
80
|
remaining: tokens - usedTokensAfterUpdate,
|
|
81
81
|
reset: (bucket + 1) * windowDuration,
|
|
82
|
+
pending: Promise.resolve(),
|
|
82
83
|
};
|
|
83
84
|
};
|
|
84
85
|
}
|
|
@@ -150,6 +151,7 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
150
151
|
limit: tokens,
|
|
151
152
|
remaining,
|
|
152
153
|
reset: (currentWindow + 1) * windowSize,
|
|
154
|
+
pending: Promise.resolve(),
|
|
153
155
|
};
|
|
154
156
|
};
|
|
155
157
|
}
|
|
@@ -229,7 +231,13 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
229
231
|
const now = Date.now();
|
|
230
232
|
const key = [identifier, Math.floor(now / intervalDuration)].join(":");
|
|
231
233
|
const [remaining, reset] = (await ctx.redis.eval(script, [key], [maxTokens, intervalDuration, refillRate, now]));
|
|
232
|
-
return {
|
|
234
|
+
return {
|
|
235
|
+
success: remaining > 0,
|
|
236
|
+
limit: maxTokens,
|
|
237
|
+
remaining,
|
|
238
|
+
reset,
|
|
239
|
+
pending: Promise.resolve(),
|
|
240
|
+
};
|
|
233
241
|
};
|
|
234
242
|
}
|
|
235
243
|
}
|
package/types/mod.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { RegionRatelimit as Ratelimit } from "./
|
|
2
|
-
export type { RegionRatelimitConfig as RatelimitConfig } from "./
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
1
|
+
export { RegionRatelimit as Ratelimit } from "./single.js";
|
|
2
|
+
export type { RegionRatelimitConfig as RatelimitConfig } from "./single.js";
|
|
3
|
+
export { MultiRegionRatelimit } from "./multi.js";
|
|
4
|
+
export type { MultiRegionRatelimitConfig } from "./multi.js";
|
|
5
5
|
export type { Algorithm } from "./types.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Duration } from "./duration.js";
|
|
2
|
-
import type { Algorithm,
|
|
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 declare type
|
|
5
|
+
export declare type MultiRegionRatelimitConfig = {
|
|
6
6
|
/**
|
|
7
7
|
* Instances of `@upstash/redis`
|
|
8
8
|
* @see https://github.com/upstash/upstash-redis#quick-start
|
|
@@ -13,9 +13,9 @@ export declare type GlobalRatelimitConfig = {
|
|
|
13
13
|
*
|
|
14
14
|
* Choose one of the predefined ones or implement your own.
|
|
15
15
|
* Available algorithms are exposed via static methods:
|
|
16
|
-
* -
|
|
16
|
+
* - MultiRegionRatelimit.fixedWindow
|
|
17
17
|
*/
|
|
18
|
-
limiter: Algorithm<
|
|
18
|
+
limiter: Algorithm<MultiRegionContext>;
|
|
19
19
|
/**
|
|
20
20
|
* All keys in redis are prefixed with this.
|
|
21
21
|
*
|
|
@@ -28,9 +28,9 @@ export declare type GlobalRatelimitConfig = {
|
|
|
28
28
|
*
|
|
29
29
|
* @example
|
|
30
30
|
* ```ts
|
|
31
|
-
* const { limit } = new
|
|
31
|
+
* const { limit } = new MultiRegionRatelimit({
|
|
32
32
|
* redis: Redis.fromEnv(),
|
|
33
|
-
* limiter:
|
|
33
|
+
* limiter: MultiRegionRatelimit.fixedWindow(
|
|
34
34
|
* 10, // Allow 10 requests per window of 30 minutes
|
|
35
35
|
* "30 m", // interval of 30 minutes
|
|
36
36
|
* )
|
|
@@ -38,11 +38,11 @@ export declare type GlobalRatelimitConfig = {
|
|
|
38
38
|
*
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
|
-
export declare class
|
|
41
|
+
export declare class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
|
|
42
42
|
/**
|
|
43
43
|
* Create a new Ratelimit instance by providing a `@upstash/redis` instance and the algorithn of your choice.
|
|
44
44
|
*/
|
|
45
|
-
constructor(config:
|
|
45
|
+
constructor(config: MultiRegionRatelimitConfig);
|
|
46
46
|
/**
|
|
47
47
|
* Each requests inside a fixed time increases a counter.
|
|
48
48
|
* Once the counter reaches a maxmimum allowed number, all further requests are
|
|
@@ -69,7 +69,7 @@ export declare class GlobalRatelimit extends Ratelimit<GlobalContext> {
|
|
|
69
69
|
/**
|
|
70
70
|
* The duration in which `tokens` requests are allowed.
|
|
71
71
|
*/
|
|
72
|
-
window: Duration): Algorithm<
|
|
72
|
+
window: Duration): Algorithm<MultiRegionContext>;
|
|
73
73
|
/**
|
|
74
74
|
* Combined approach of `slidingLogs` and `fixedWindow` with lower storage
|
|
75
75
|
* costs than `slidingLogs` and improved boundary behavior by calcualting a
|
|
@@ -94,5 +94,5 @@ export declare class GlobalRatelimit extends Ratelimit<GlobalContext> {
|
|
|
94
94
|
/**
|
|
95
95
|
* The duration in which `tokens` requests are allowed.
|
|
96
96
|
*/
|
|
97
|
-
window: Duration): Algorithm<
|
|
97
|
+
window: Duration): Algorithm<MultiRegionContext>;
|
|
98
98
|
}
|
|
File without changes
|
package/types/types.d.ts
CHANGED
|
@@ -5,10 +5,10 @@ export interface Redis {
|
|
|
5
5
|
export declare type RegionContext = {
|
|
6
6
|
redis: Redis;
|
|
7
7
|
};
|
|
8
|
-
export declare type
|
|
8
|
+
export declare type MultiRegionContext = {
|
|
9
9
|
redis: Redis[];
|
|
10
10
|
};
|
|
11
|
-
export declare type Context = RegionContext |
|
|
11
|
+
export declare type Context = RegionContext | MultiRegionContext;
|
|
12
12
|
export declare type RatelimitResponse = {
|
|
13
13
|
/**
|
|
14
14
|
* Whether the request may pass(true) or exceeded the limit(false)
|
|
@@ -26,5 +26,28 @@ export declare type RatelimitResponse = {
|
|
|
26
26
|
* Unix timestamp in milliseconds when the limits are reset.
|
|
27
27
|
*/
|
|
28
28
|
reset: number;
|
|
29
|
+
/**
|
|
30
|
+
* For the MultiRegion setup we do some synchronizing in the background, after returning the current limit.
|
|
31
|
+
* In most case you can simply ignore this.
|
|
32
|
+
*
|
|
33
|
+
* On Vercel Edge or Cloudflare workers, you need to explicitely handle the pending Promise like this:
|
|
34
|
+
*
|
|
35
|
+
* **Vercel Edge:**
|
|
36
|
+
* https://nextjs.org/docs/api-reference/next/server#nextfetchevent
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const { pending } = await ratelimit.limit("id")
|
|
40
|
+
* event.waitUntil(pending)
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* **Cloudflare Worker:**
|
|
44
|
+
* https://developers.cloudflare.com/workers/runtime-apis/fetch-event/#syntax-module-worker
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* const { pending } = await ratelimit.limit("id")
|
|
48
|
+
* context.waitUntil(pending)
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
pending: Promise<unknown>;
|
|
29
52
|
};
|
|
30
53
|
export declare type Algorithm<TContext> = (ctx: TContext, identifier: string) => Promise<RatelimitResponse>;
|