@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 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
- - Fastly Compute@Edge (see
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: Ratelimit.slidingWindow(10, "10 s"),
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
- return await this.limiter(this.ctx, key);
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.1.5",
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": "./esm/mod.js",
47
- "require": "./script/mod.js",
48
- "types": "./types/mod.d.ts"
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(":");
@@ -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
- return await this.limiter(this.ctx, key);
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
@@ -10,4 +10,7 @@ export declare class Cache implements EphemeralCache {
10
10
  reset: number;
11
11
  };
12
12
  blockUntil(identifier: string, reset: number): void;
13
+ set(key: string, value: number): void;
14
+ get(key: string): number | null;
15
+ incr(key: string): number;
13
16
  }
@@ -1,5 +1,5 @@
1
- declare type Unit = "ms" | "s" | "m" | "h" | "d";
2
- export declare type Duration = `${number} ${Unit}`;
1
+ type Unit = "ms" | "s" | "m" | "h" | "d";
2
+ export type Duration = `${number} ${Unit}`;
3
3
  /**
4
4
  * Convert a human readable duration to milliseconds
5
5
  */
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 declare type MultiRegionRatelimitConfig = {
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/
@@ -1,5 +1,8 @@
1
1
  import type { Algorithm, Context, RatelimitResponse } from "./types.js";
2
- export declare type RatelimitConfig<TContext> = {
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 declare type RegionRatelimitConfig = {
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 declare type RegionContext = {
18
+ export type RegionContext = {
16
19
  redis: Redis;
17
20
  cache?: EphemeralCache;
18
21
  };
19
- export declare type MultiRegionContext = {
22
+ export type MultiRegionContext = {
20
23
  redis: Redis[];
21
24
  cache?: EphemeralCache;
22
25
  };
23
- export declare type Context = RegionContext | MultiRegionContext;
24
- export declare type RatelimitResponse = {
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 declare type Algorithm<TContext> = (ctx: TContext, identifier: string, opts?: {
68
+ export type Algorithm<TContext> = (ctx: TContext, identifier: string, opts?: {
66
69
  cache?: EphemeralCache;
67
70
  }) => Promise<RatelimitResponse>;