@upstash/ratelimit 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.releaserc +14 -0
- package/README.md +42 -11
- package/esm/cache.js +28 -0
- package/esm/multi.js +45 -8
- package/esm/ratelimit.js +7 -0
- package/esm/single.js +59 -8
- package/package.json +2 -2
- package/script/cache.js +32 -0
- package/script/multi.js +45 -8
- package/script/ratelimit.js +7 -0
- package/script/single.js +59 -8
- package/types/cache.d.ts +13 -0
- package/types/multi.d.ts +17 -0
- package/types/ratelimit.d.ts +17 -0
- package/types/single.d.ts +17 -2
- package/types/types.d.ts +15 -1
package/.releaserc
ADDED
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
# Upstash
|
|
2
|
-
|
|
3
|
-
An HTTP/REST based Redis client built on top of Upstash REST API.
|
|
4
|
-
[Upstash REST API](https://docs.upstash.com/features/restapi).
|
|
1
|
+
# Upstash RateLimit
|
|
5
2
|
|
|
6
3
|
[](https://github.com/upstash/ratelimit/actions/workflows/tests.yaml)
|
|
7
4
|

|
|
8
5
|
|
|
9
|
-
It is the only connectionless (HTTP based)
|
|
6
|
+
It is the only connectionless (HTTP based) rate limiting library and designed
|
|
7
|
+
for:
|
|
10
8
|
|
|
11
9
|
- Serverless functions (AWS Lambda ...)
|
|
12
10
|
- Cloudflare Workers
|
|
@@ -18,6 +16,7 @@ It is the only connectionless (HTTP based) ratelimiter and designed for:
|
|
|
18
16
|
|
|
19
17
|
<!-- toc -->
|
|
20
18
|
|
|
19
|
+
- [Docs](#docs)
|
|
21
20
|
- [Quick Start](#quick-start)
|
|
22
21
|
- [Install](#install)
|
|
23
22
|
- [npm](#npm)
|
|
@@ -25,8 +24,10 @@ It is the only connectionless (HTTP based) ratelimiter and designed for:
|
|
|
25
24
|
- [Create database](#create-database)
|
|
26
25
|
- [Use it](#use-it)
|
|
27
26
|
- [Block until ready](#block-until-ready)
|
|
28
|
-
- [
|
|
27
|
+
- [Ephemeral Cache](#ephemeral-cache)
|
|
28
|
+
- [MultiRegion replicated ratelimiting](#multiregion-replicated-ratelimiting)
|
|
29
29
|
- [Usage](#usage)
|
|
30
|
+
- [Asynchronous synchronization between databases](#asynchronous-synchronization-between-databases)
|
|
30
31
|
- [Example](#example)
|
|
31
32
|
- [Ratelimiting algorithms](#ratelimiting-algorithms)
|
|
32
33
|
- [Fixed Window](#fixed-window)
|
|
@@ -48,6 +49,10 @@ It is the only connectionless (HTTP based) ratelimiter and designed for:
|
|
|
48
49
|
|
|
49
50
|
<!-- tocstop -->
|
|
50
51
|
|
|
52
|
+
## Docs
|
|
53
|
+
|
|
54
|
+
[doc.deno.land](https://doc.deno.land/https://deno.land/x/upstash_ratelimit/src/mod.ts)
|
|
55
|
+
|
|
51
56
|
## Quick Start
|
|
52
57
|
|
|
53
58
|
### Install
|
|
@@ -176,11 +181,37 @@ doExpensiveCalculation();
|
|
|
176
181
|
return "Here you go!";
|
|
177
182
|
```
|
|
178
183
|
|
|
179
|
-
|
|
184
|
+
### Ephemeral Cache
|
|
185
|
+
|
|
186
|
+
For extreme load or denial of service attacks, it might be too expensive to call
|
|
187
|
+
redis for every incoming request, just to find out it should be blocked because
|
|
188
|
+
they have exceeded the limit.
|
|
189
|
+
|
|
190
|
+
You can use an ephemeral in memory cache by passing the `ephemeralCache` option:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const cache = new Map(); // must be outside of your serverless function handler
|
|
194
|
+
|
|
195
|
+
// ...
|
|
196
|
+
|
|
197
|
+
const ratelimit = new Ratelimit({
|
|
198
|
+
// ...
|
|
199
|
+
ephemeralCache: cache,
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
If enabled, the ratelimiter will keep a global cache of identifiers and their
|
|
204
|
+
reset timestamps, that have exhausted their ratelimit. In serverless
|
|
205
|
+
environments this is only possible if you create the cache or ratelimiter
|
|
206
|
+
instance outside of your handler function. While the function is still hot, the
|
|
207
|
+
ratelimiter can block requests without having to request data from redis, thus
|
|
208
|
+
saving time and money.
|
|
209
|
+
|
|
210
|
+
## MultiRegion replicated ratelimiting
|
|
180
211
|
|
|
181
|
-
Using a single redis instance has the downside of providing low latencies
|
|
182
|
-
part of your userbase closest to the deployed db. That's why we also
|
|
183
|
-
`MultiRegionRatelimit` which replicates the state across multiple redis
|
|
212
|
+
Using a single redis instance has the downside of providing low latencies only
|
|
213
|
+
to the part of your userbase closest to the deployed db. That's why we also
|
|
214
|
+
built `MultiRegionRatelimit` which replicates the state across multiple redis
|
|
184
215
|
databases as well as offering lower latencies to more of your users.
|
|
185
216
|
|
|
186
217
|
`MultiRegionRatelimit` does this by checking the current limit in the closest db
|
|
@@ -247,7 +278,7 @@ context.waitUntil(pending);
|
|
|
247
278
|
### Example
|
|
248
279
|
|
|
249
280
|
Let's assume you have customers in the US and Europe. In this case you can
|
|
250
|
-
create 2 regional redis databases on [
|
|
281
|
+
create 2 regional redis databases on [Upstash](https://console.upstash.com) and
|
|
251
282
|
your users will enjoy the latency of whichever db is closest to them.
|
|
252
283
|
|
|
253
284
|
## Ratelimiting algorithms
|
package/esm/cache.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class Cache {
|
|
2
|
+
constructor(cache) {
|
|
3
|
+
/**
|
|
4
|
+
* Stores identifier -> reset (in milliseconds)
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(this, "cache", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
writable: true,
|
|
10
|
+
value: void 0
|
|
11
|
+
});
|
|
12
|
+
this.cache = cache;
|
|
13
|
+
}
|
|
14
|
+
isBlocked(identifier) {
|
|
15
|
+
if (!this.cache.has(identifier)) {
|
|
16
|
+
return { blocked: false, reset: 0 };
|
|
17
|
+
}
|
|
18
|
+
const reset = this.cache.get(identifier);
|
|
19
|
+
if (reset < Date.now()) {
|
|
20
|
+
this.cache.delete(identifier);
|
|
21
|
+
return { blocked: false, reset: 0 };
|
|
22
|
+
}
|
|
23
|
+
return { blocked: true, reset: reset };
|
|
24
|
+
}
|
|
25
|
+
blockUntil(identifier, reset) {
|
|
26
|
+
this.cache.set(identifier, reset);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/esm/multi.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ms } from "./duration.js";
|
|
2
2
|
import { Ratelimit } from "./ratelimit.js";
|
|
3
|
+
import { Cache } from "./cache.js";
|
|
3
4
|
/**
|
|
4
5
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
5
6
|
*
|
|
@@ -23,7 +24,12 @@ export class MultiRegionRatelimit extends Ratelimit {
|
|
|
23
24
|
super({
|
|
24
25
|
prefix: config.prefix,
|
|
25
26
|
limiter: config.limiter,
|
|
26
|
-
ctx: {
|
|
27
|
+
ctx: {
|
|
28
|
+
redis: config.redis,
|
|
29
|
+
cache: config.ephermeralCache
|
|
30
|
+
? new Cache(config.ephermeralCache)
|
|
31
|
+
: undefined,
|
|
32
|
+
},
|
|
27
33
|
});
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
@@ -70,6 +76,18 @@ export class MultiRegionRatelimit extends Ratelimit {
|
|
|
70
76
|
return members
|
|
71
77
|
`;
|
|
72
78
|
return async function (ctx, identifier) {
|
|
79
|
+
if (ctx.cache) {
|
|
80
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
81
|
+
if (blocked) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
limit: tokens,
|
|
85
|
+
remaining: 0,
|
|
86
|
+
reset: reset,
|
|
87
|
+
pending: Promise.resolve(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
73
91
|
const requestID = crypto.randomUUID();
|
|
74
92
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
75
93
|
const key = [identifier, bucket].join(":");
|
|
@@ -108,11 +126,16 @@ export class MultiRegionRatelimit extends Ratelimit {
|
|
|
108
126
|
/**
|
|
109
127
|
* Do not await sync. This should not run in the critical path.
|
|
110
128
|
*/
|
|
129
|
+
const success = remaining > 0;
|
|
130
|
+
const reset = (bucket + 1) * windowDuration;
|
|
131
|
+
if (ctx.cache && !success) {
|
|
132
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
133
|
+
}
|
|
111
134
|
return {
|
|
112
|
-
success
|
|
135
|
+
success,
|
|
113
136
|
limit: tokens,
|
|
114
137
|
remaining,
|
|
115
|
-
reset
|
|
138
|
+
reset,
|
|
116
139
|
pending: sync(),
|
|
117
140
|
};
|
|
118
141
|
};
|
|
@@ -173,6 +196,18 @@ export class MultiRegionRatelimit extends Ratelimit {
|
|
|
173
196
|
`;
|
|
174
197
|
const windowDuration = ms(window);
|
|
175
198
|
return async function (ctx, identifier) {
|
|
199
|
+
if (ctx.cache) {
|
|
200
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
201
|
+
if (blocked) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
limit: tokens,
|
|
205
|
+
remaining: 0,
|
|
206
|
+
reset: reset,
|
|
207
|
+
pending: Promise.resolve(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
176
211
|
const requestID = crypto.randomUUID();
|
|
177
212
|
const now = Date.now();
|
|
178
213
|
const currentWindow = Math.floor(now / windowSize);
|
|
@@ -213,14 +248,16 @@ export class MultiRegionRatelimit extends Ratelimit {
|
|
|
213
248
|
await db.redis.sadd(currentKey, ...allIDs);
|
|
214
249
|
}
|
|
215
250
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
251
|
+
const success = remaining > 0;
|
|
252
|
+
const reset = (currentWindow + 1) * windowDuration;
|
|
253
|
+
if (ctx.cache && !success) {
|
|
254
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
255
|
+
}
|
|
219
256
|
return {
|
|
220
|
-
success
|
|
257
|
+
success,
|
|
221
258
|
limit: tokens,
|
|
222
259
|
remaining,
|
|
223
|
-
reset
|
|
260
|
+
reset,
|
|
224
261
|
pending: sync(),
|
|
225
262
|
};
|
|
226
263
|
};
|
package/esm/ratelimit.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Cache } from "./cache.js";
|
|
1
2
|
/**
|
|
2
3
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
3
4
|
*
|
|
@@ -125,5 +126,11 @@ export class Ratelimit {
|
|
|
125
126
|
this.ctx = config.ctx;
|
|
126
127
|
this.limiter = config.limiter;
|
|
127
128
|
this.prefix = config.prefix ?? "@upstash/ratelimit";
|
|
129
|
+
if (config.ephermeralCache instanceof Map) {
|
|
130
|
+
this.ctx.cache = new Cache(config.ephermeralCache);
|
|
131
|
+
}
|
|
132
|
+
else if (typeof config.ephermeralCache === "undefined") {
|
|
133
|
+
this.ctx.cache = new Cache(new Map());
|
|
134
|
+
}
|
|
128
135
|
}
|
|
129
136
|
}
|
package/esm/single.js
CHANGED
|
@@ -23,7 +23,10 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
23
23
|
super({
|
|
24
24
|
prefix: config.prefix,
|
|
25
25
|
limiter: config.limiter,
|
|
26
|
-
ctx: {
|
|
26
|
+
ctx: {
|
|
27
|
+
redis: config.redis,
|
|
28
|
+
},
|
|
29
|
+
ephermeralCache: config.ephermeralCache,
|
|
27
30
|
});
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
@@ -70,12 +73,29 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
70
73
|
return async function (ctx, identifier) {
|
|
71
74
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
72
75
|
const key = [identifier, bucket].join(":");
|
|
76
|
+
if (ctx.cache) {
|
|
77
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
78
|
+
if (blocked) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
limit: tokens,
|
|
82
|
+
remaining: 0,
|
|
83
|
+
reset: reset,
|
|
84
|
+
pending: Promise.resolve(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
73
88
|
const usedTokensAfterUpdate = (await ctx.redis.eval(script, [key], [windowDuration]));
|
|
89
|
+
const success = usedTokensAfterUpdate <= tokens;
|
|
90
|
+
const reset = (bucket + 1) * windowDuration;
|
|
91
|
+
if (ctx.cache && !success) {
|
|
92
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
93
|
+
}
|
|
74
94
|
return {
|
|
75
|
-
success
|
|
95
|
+
success,
|
|
76
96
|
limit: tokens,
|
|
77
97
|
remaining: tokens - usedTokensAfterUpdate,
|
|
78
|
-
reset
|
|
98
|
+
reset,
|
|
79
99
|
pending: Promise.resolve(),
|
|
80
100
|
};
|
|
81
101
|
};
|
|
@@ -142,12 +162,29 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
142
162
|
const currentKey = [identifier, currentWindow].join(":");
|
|
143
163
|
const previousWindow = currentWindow - windowSize;
|
|
144
164
|
const previousKey = [identifier, previousWindow].join(":");
|
|
165
|
+
if (ctx.cache) {
|
|
166
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
167
|
+
if (blocked) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
limit: tokens,
|
|
171
|
+
remaining: 0,
|
|
172
|
+
reset: reset,
|
|
173
|
+
pending: Promise.resolve(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
145
177
|
const remaining = (await ctx.redis.eval(script, [currentKey, previousKey], [tokens, now, windowSize]));
|
|
178
|
+
const success = remaining > 0;
|
|
179
|
+
const reset = (currentWindow + 1) * windowSize;
|
|
180
|
+
if (ctx.cache && !success) {
|
|
181
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
182
|
+
}
|
|
146
183
|
return {
|
|
147
|
-
success
|
|
184
|
+
success,
|
|
148
185
|
limit: tokens,
|
|
149
186
|
remaining,
|
|
150
|
-
reset
|
|
187
|
+
reset,
|
|
151
188
|
pending: Promise.resolve(),
|
|
152
189
|
};
|
|
153
190
|
};
|
|
@@ -164,8 +201,6 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
164
201
|
* rate.
|
|
165
202
|
* - Allows to set a higher initial burst limit by setting `maxTokens` higher
|
|
166
203
|
* than `refillRate`
|
|
167
|
-
*
|
|
168
|
-
* **Usage of Upstash Redis requests:**
|
|
169
204
|
*/
|
|
170
205
|
static tokenBucket(
|
|
171
206
|
/**
|
|
@@ -225,11 +260,27 @@ export class RegionRatelimit extends Ratelimit {
|
|
|
225
260
|
`;
|
|
226
261
|
const intervalDuration = ms(interval);
|
|
227
262
|
return async function (ctx, identifier) {
|
|
263
|
+
if (ctx.cache) {
|
|
264
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
265
|
+
if (blocked) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
limit: maxTokens,
|
|
269
|
+
remaining: 0,
|
|
270
|
+
reset: reset,
|
|
271
|
+
pending: Promise.resolve(),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
228
275
|
const now = Date.now();
|
|
229
276
|
const key = [identifier, Math.floor(now / intervalDuration)].join(":");
|
|
230
277
|
const [remaining, reset] = (await ctx.redis.eval(script, [key], [maxTokens, intervalDuration, refillRate, now]));
|
|
278
|
+
const success = remaining > 0;
|
|
279
|
+
if (ctx.cache && !success) {
|
|
280
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
281
|
+
}
|
|
231
282
|
return {
|
|
232
|
-
success
|
|
283
|
+
success,
|
|
233
284
|
limit: maxTokens,
|
|
234
285
|
remaining,
|
|
235
286
|
reset,
|
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.1.
|
|
6
|
+
"version": "v0.1.4",
|
|
7
7
|
"description": "A serverless ratelimiter built on top of Upstash REST API.",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"size-limit": "latest"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@upstash/redis": "^1.
|
|
32
|
+
"@upstash/redis": "^1.4.0"
|
|
33
33
|
},
|
|
34
34
|
"size-limit": [
|
|
35
35
|
{
|
package/script/cache.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Cache = void 0;
|
|
4
|
+
class Cache {
|
|
5
|
+
constructor(cache) {
|
|
6
|
+
/**
|
|
7
|
+
* Stores identifier -> reset (in milliseconds)
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(this, "cache", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: void 0
|
|
14
|
+
});
|
|
15
|
+
this.cache = cache;
|
|
16
|
+
}
|
|
17
|
+
isBlocked(identifier) {
|
|
18
|
+
if (!this.cache.has(identifier)) {
|
|
19
|
+
return { blocked: false, reset: 0 };
|
|
20
|
+
}
|
|
21
|
+
const reset = this.cache.get(identifier);
|
|
22
|
+
if (reset < Date.now()) {
|
|
23
|
+
this.cache.delete(identifier);
|
|
24
|
+
return { blocked: false, reset: 0 };
|
|
25
|
+
}
|
|
26
|
+
return { blocked: true, reset: reset };
|
|
27
|
+
}
|
|
28
|
+
blockUntil(identifier, reset) {
|
|
29
|
+
this.cache.set(identifier, reset);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.Cache = Cache;
|
package/script/multi.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MultiRegionRatelimit = void 0;
|
|
4
4
|
const duration_js_1 = require("./duration.js");
|
|
5
5
|
const ratelimit_js_1 = require("./ratelimit.js");
|
|
6
|
+
const cache_js_1 = require("./cache.js");
|
|
6
7
|
/**
|
|
7
8
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
8
9
|
*
|
|
@@ -26,7 +27,12 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
26
27
|
super({
|
|
27
28
|
prefix: config.prefix,
|
|
28
29
|
limiter: config.limiter,
|
|
29
|
-
ctx: {
|
|
30
|
+
ctx: {
|
|
31
|
+
redis: config.redis,
|
|
32
|
+
cache: config.ephermeralCache
|
|
33
|
+
? new cache_js_1.Cache(config.ephermeralCache)
|
|
34
|
+
: undefined,
|
|
35
|
+
},
|
|
30
36
|
});
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
@@ -73,6 +79,18 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
73
79
|
return members
|
|
74
80
|
`;
|
|
75
81
|
return async function (ctx, identifier) {
|
|
82
|
+
if (ctx.cache) {
|
|
83
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
84
|
+
if (blocked) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
limit: tokens,
|
|
88
|
+
remaining: 0,
|
|
89
|
+
reset: reset,
|
|
90
|
+
pending: Promise.resolve(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
76
94
|
const requestID = crypto.randomUUID();
|
|
77
95
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
78
96
|
const key = [identifier, bucket].join(":");
|
|
@@ -111,11 +129,16 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
111
129
|
/**
|
|
112
130
|
* Do not await sync. This should not run in the critical path.
|
|
113
131
|
*/
|
|
132
|
+
const success = remaining > 0;
|
|
133
|
+
const reset = (bucket + 1) * windowDuration;
|
|
134
|
+
if (ctx.cache && !success) {
|
|
135
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
136
|
+
}
|
|
114
137
|
return {
|
|
115
|
-
success
|
|
138
|
+
success,
|
|
116
139
|
limit: tokens,
|
|
117
140
|
remaining,
|
|
118
|
-
reset
|
|
141
|
+
reset,
|
|
119
142
|
pending: sync(),
|
|
120
143
|
};
|
|
121
144
|
};
|
|
@@ -176,6 +199,18 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
176
199
|
`;
|
|
177
200
|
const windowDuration = (0, duration_js_1.ms)(window);
|
|
178
201
|
return async function (ctx, identifier) {
|
|
202
|
+
if (ctx.cache) {
|
|
203
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
204
|
+
if (blocked) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
limit: tokens,
|
|
208
|
+
remaining: 0,
|
|
209
|
+
reset: reset,
|
|
210
|
+
pending: Promise.resolve(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
179
214
|
const requestID = crypto.randomUUID();
|
|
180
215
|
const now = Date.now();
|
|
181
216
|
const currentWindow = Math.floor(now / windowSize);
|
|
@@ -216,14 +251,16 @@ class MultiRegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
216
251
|
await db.redis.sadd(currentKey, ...allIDs);
|
|
217
252
|
}
|
|
218
253
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
254
|
+
const success = remaining > 0;
|
|
255
|
+
const reset = (currentWindow + 1) * windowDuration;
|
|
256
|
+
if (ctx.cache && !success) {
|
|
257
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
258
|
+
}
|
|
222
259
|
return {
|
|
223
|
-
success
|
|
260
|
+
success,
|
|
224
261
|
limit: tokens,
|
|
225
262
|
remaining,
|
|
226
|
-
reset
|
|
263
|
+
reset,
|
|
227
264
|
pending: sync(),
|
|
228
265
|
};
|
|
229
266
|
};
|
package/script/ratelimit.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Ratelimit = void 0;
|
|
4
|
+
const cache_js_1 = require("./cache.js");
|
|
4
5
|
/**
|
|
5
6
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
6
7
|
*
|
|
@@ -128,6 +129,12 @@ class Ratelimit {
|
|
|
128
129
|
this.ctx = config.ctx;
|
|
129
130
|
this.limiter = config.limiter;
|
|
130
131
|
this.prefix = config.prefix ?? "@upstash/ratelimit";
|
|
132
|
+
if (config.ephermeralCache instanceof Map) {
|
|
133
|
+
this.ctx.cache = new cache_js_1.Cache(config.ephermeralCache);
|
|
134
|
+
}
|
|
135
|
+
else if (typeof config.ephermeralCache === "undefined") {
|
|
136
|
+
this.ctx.cache = new cache_js_1.Cache(new Map());
|
|
137
|
+
}
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
exports.Ratelimit = Ratelimit;
|
package/script/single.js
CHANGED
|
@@ -26,7 +26,10 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
26
26
|
super({
|
|
27
27
|
prefix: config.prefix,
|
|
28
28
|
limiter: config.limiter,
|
|
29
|
-
ctx: {
|
|
29
|
+
ctx: {
|
|
30
|
+
redis: config.redis,
|
|
31
|
+
},
|
|
32
|
+
ephermeralCache: config.ephermeralCache,
|
|
30
33
|
});
|
|
31
34
|
}
|
|
32
35
|
/**
|
|
@@ -73,12 +76,29 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
73
76
|
return async function (ctx, identifier) {
|
|
74
77
|
const bucket = Math.floor(Date.now() / windowDuration);
|
|
75
78
|
const key = [identifier, bucket].join(":");
|
|
79
|
+
if (ctx.cache) {
|
|
80
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
81
|
+
if (blocked) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
limit: tokens,
|
|
85
|
+
remaining: 0,
|
|
86
|
+
reset: reset,
|
|
87
|
+
pending: Promise.resolve(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
76
91
|
const usedTokensAfterUpdate = (await ctx.redis.eval(script, [key], [windowDuration]));
|
|
92
|
+
const success = usedTokensAfterUpdate <= tokens;
|
|
93
|
+
const reset = (bucket + 1) * windowDuration;
|
|
94
|
+
if (ctx.cache && !success) {
|
|
95
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
96
|
+
}
|
|
77
97
|
return {
|
|
78
|
-
success
|
|
98
|
+
success,
|
|
79
99
|
limit: tokens,
|
|
80
100
|
remaining: tokens - usedTokensAfterUpdate,
|
|
81
|
-
reset
|
|
101
|
+
reset,
|
|
82
102
|
pending: Promise.resolve(),
|
|
83
103
|
};
|
|
84
104
|
};
|
|
@@ -145,12 +165,29 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
145
165
|
const currentKey = [identifier, currentWindow].join(":");
|
|
146
166
|
const previousWindow = currentWindow - windowSize;
|
|
147
167
|
const previousKey = [identifier, previousWindow].join(":");
|
|
168
|
+
if (ctx.cache) {
|
|
169
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
170
|
+
if (blocked) {
|
|
171
|
+
return {
|
|
172
|
+
success: false,
|
|
173
|
+
limit: tokens,
|
|
174
|
+
remaining: 0,
|
|
175
|
+
reset: reset,
|
|
176
|
+
pending: Promise.resolve(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
148
180
|
const remaining = (await ctx.redis.eval(script, [currentKey, previousKey], [tokens, now, windowSize]));
|
|
181
|
+
const success = remaining > 0;
|
|
182
|
+
const reset = (currentWindow + 1) * windowSize;
|
|
183
|
+
if (ctx.cache && !success) {
|
|
184
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
185
|
+
}
|
|
149
186
|
return {
|
|
150
|
-
success
|
|
187
|
+
success,
|
|
151
188
|
limit: tokens,
|
|
152
189
|
remaining,
|
|
153
|
-
reset
|
|
190
|
+
reset,
|
|
154
191
|
pending: Promise.resolve(),
|
|
155
192
|
};
|
|
156
193
|
};
|
|
@@ -167,8 +204,6 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
167
204
|
* rate.
|
|
168
205
|
* - Allows to set a higher initial burst limit by setting `maxTokens` higher
|
|
169
206
|
* than `refillRate`
|
|
170
|
-
*
|
|
171
|
-
* **Usage of Upstash Redis requests:**
|
|
172
207
|
*/
|
|
173
208
|
static tokenBucket(
|
|
174
209
|
/**
|
|
@@ -228,11 +263,27 @@ class RegionRatelimit extends ratelimit_js_1.Ratelimit {
|
|
|
228
263
|
`;
|
|
229
264
|
const intervalDuration = (0, duration_js_1.ms)(interval);
|
|
230
265
|
return async function (ctx, identifier) {
|
|
266
|
+
if (ctx.cache) {
|
|
267
|
+
const { blocked, reset } = ctx.cache.isBlocked(identifier);
|
|
268
|
+
if (blocked) {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
limit: maxTokens,
|
|
272
|
+
remaining: 0,
|
|
273
|
+
reset: reset,
|
|
274
|
+
pending: Promise.resolve(),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
231
278
|
const now = Date.now();
|
|
232
279
|
const key = [identifier, Math.floor(now / intervalDuration)].join(":");
|
|
233
280
|
const [remaining, reset] = (await ctx.redis.eval(script, [key], [maxTokens, intervalDuration, refillRate, now]));
|
|
281
|
+
const success = remaining > 0;
|
|
282
|
+
if (ctx.cache && !success) {
|
|
283
|
+
ctx.cache.blockUntil(identifier, reset);
|
|
284
|
+
}
|
|
234
285
|
return {
|
|
235
|
-
success
|
|
286
|
+
success,
|
|
236
287
|
limit: maxTokens,
|
|
237
288
|
remaining,
|
|
238
289
|
reset,
|
package/types/cache.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EphermeralCache } from "./types.js";
|
|
2
|
+
export declare class Cache implements EphermeralCache {
|
|
3
|
+
/**
|
|
4
|
+
* Stores identifier -> reset (in milliseconds)
|
|
5
|
+
*/
|
|
6
|
+
private readonly cache;
|
|
7
|
+
constructor(cache: Map<string, number>);
|
|
8
|
+
isBlocked(identifier: string): {
|
|
9
|
+
blocked: boolean;
|
|
10
|
+
reset: number;
|
|
11
|
+
};
|
|
12
|
+
blockUntil(identifier: string, reset: number): void;
|
|
13
|
+
}
|
package/types/multi.d.ts
CHANGED
|
@@ -22,6 +22,23 @@ export declare type MultiRegionRatelimitConfig = {
|
|
|
22
22
|
* @default `@upstash/ratelimit`
|
|
23
23
|
*/
|
|
24
24
|
prefix?: string;
|
|
25
|
+
/**
|
|
26
|
+
* If enabled, the ratelimiter will keep a global cache of identifiers, that have
|
|
27
|
+
* exhausted their ratelimit. In serverless environments this is only possible if
|
|
28
|
+
* you create the ratelimiter instance outside of your handler function. While the
|
|
29
|
+
* function is still hot, the ratelimiter can block requests without having to
|
|
30
|
+
* request data from redis, thus saving time and money.
|
|
31
|
+
*
|
|
32
|
+
* Whenever an identifier has exceeded its limit, the ratelimiter will add it to an
|
|
33
|
+
* internal list together with its reset timestamp. If the same identifier makes a
|
|
34
|
+
* new request before it is reset, we can immediately reject it.
|
|
35
|
+
*
|
|
36
|
+
* Set to `false` to disable.
|
|
37
|
+
*
|
|
38
|
+
* If left undefined, a map is created automatically, but it can only work
|
|
39
|
+
* if the map or th ratelimit instance is created outside your serverless function handler.
|
|
40
|
+
*/
|
|
41
|
+
ephermeralCache?: Map<string, number> | false;
|
|
25
42
|
};
|
|
26
43
|
/**
|
|
27
44
|
* Ratelimiter using serverless redis from https://upstash.com/
|
package/types/ratelimit.d.ts
CHANGED
|
@@ -18,6 +18,23 @@ export declare type RatelimitConfig<TContext> = {
|
|
|
18
18
|
* @default `@upstash/ratelimit`
|
|
19
19
|
*/
|
|
20
20
|
prefix?: string;
|
|
21
|
+
/**
|
|
22
|
+
* If enabled, the ratelimiter will keep a global cache of identifiers, that have
|
|
23
|
+
* exhausted their ratelimit. In serverless environments this is only possible if
|
|
24
|
+
* you create the ratelimiter instance outside of your handler function. While the
|
|
25
|
+
* function is still hot, the ratelimiter can block requests without having to
|
|
26
|
+
* request data from redis, thus saving time and money.
|
|
27
|
+
*
|
|
28
|
+
* Whenever an identifier has exceeded its limit, the ratelimiter will add it to an
|
|
29
|
+
* internal list together with its reset timestamp. If the same identifier makes a
|
|
30
|
+
* new request before it is reset, we can immediately reject it.
|
|
31
|
+
*
|
|
32
|
+
* Set to `false` to disable.
|
|
33
|
+
*
|
|
34
|
+
* If left undefined, a map is created automatically, but it can only work
|
|
35
|
+
* if the map or the ratelimit instance is created outside your serverless function handler.
|
|
36
|
+
*/
|
|
37
|
+
ephermeralCache?: Map<string, number> | false;
|
|
21
38
|
};
|
|
22
39
|
/**
|
|
23
40
|
* Ratelimiter using serverless redis from https://upstash.com/
|
package/types/single.d.ts
CHANGED
|
@@ -25,6 +25,23 @@ export declare type RegionRatelimitConfig = {
|
|
|
25
25
|
* @default `@upstash/ratelimit`
|
|
26
26
|
*/
|
|
27
27
|
prefix?: string;
|
|
28
|
+
/**
|
|
29
|
+
* If enabled, the ratelimiter will keep a global cache of identifiers, that have
|
|
30
|
+
* exhausted their ratelimit. In serverless environments this is only possible if
|
|
31
|
+
* you create the ratelimiter instance outside of your handler function. While the
|
|
32
|
+
* function is still hot, the ratelimiter can block requests without having to
|
|
33
|
+
* request data from redis, thus saving time and money.
|
|
34
|
+
*
|
|
35
|
+
* Whenever an identifier has exceeded its limit, the ratelimiter will add it to an
|
|
36
|
+
* internal list together with its reset timestamp. If the same identifier makes a
|
|
37
|
+
* new request before it is reset, we can immediately reject it.
|
|
38
|
+
*
|
|
39
|
+
* Set to `false` to disable.
|
|
40
|
+
*
|
|
41
|
+
* If left undefined, a map is created automatically, but it can only work
|
|
42
|
+
* if the map or the ratelimit instance is created outside your serverless function handler.
|
|
43
|
+
*/
|
|
44
|
+
ephermeralCache?: Map<string, number> | false;
|
|
28
45
|
};
|
|
29
46
|
/**
|
|
30
47
|
* Ratelimiter using serverless redis from https://upstash.com/
|
|
@@ -110,8 +127,6 @@ export declare class RegionRatelimit extends Ratelimit<RegionContext> {
|
|
|
110
127
|
* rate.
|
|
111
128
|
* - Allows to set a higher initial burst limit by setting `maxTokens` higher
|
|
112
129
|
* than `refillRate`
|
|
113
|
-
*
|
|
114
|
-
* **Usage of Upstash Redis requests:**
|
|
115
130
|
*/
|
|
116
131
|
static tokenBucket(
|
|
117
132
|
/**
|
package/types/types.d.ts
CHANGED
|
@@ -2,11 +2,23 @@ export interface Redis {
|
|
|
2
2
|
eval: (script: string, keys: string[], values: unknown[]) => Promise<unknown>;
|
|
3
3
|
sadd: (key: string, ...members: string[]) => Promise<number>;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* EphermeralCache is used to block certain identifiers right away in case they have already exceedd the ratelimit.
|
|
7
|
+
*/
|
|
8
|
+
export interface EphermeralCache {
|
|
9
|
+
isBlocked: (identifier: string) => {
|
|
10
|
+
blocked: boolean;
|
|
11
|
+
reset: number;
|
|
12
|
+
};
|
|
13
|
+
blockUntil: (identifier: string, reset: number) => void;
|
|
14
|
+
}
|
|
5
15
|
export declare type RegionContext = {
|
|
6
16
|
redis: Redis;
|
|
17
|
+
cache?: EphermeralCache;
|
|
7
18
|
};
|
|
8
19
|
export declare type MultiRegionContext = {
|
|
9
20
|
redis: Redis[];
|
|
21
|
+
cache?: EphermeralCache;
|
|
10
22
|
};
|
|
11
23
|
export declare type Context = RegionContext | MultiRegionContext;
|
|
12
24
|
export declare type RatelimitResponse = {
|
|
@@ -50,4 +62,6 @@ export declare type RatelimitResponse = {
|
|
|
50
62
|
*/
|
|
51
63
|
pending: Promise<unknown>;
|
|
52
64
|
};
|
|
53
|
-
export declare type Algorithm<TContext> = (ctx: TContext, identifier: string
|
|
65
|
+
export declare type Algorithm<TContext> = (ctx: TContext, identifier: string, opts?: {
|
|
66
|
+
cache?: EphermeralCache;
|
|
67
|
+
}) => Promise<RatelimitResponse>;
|