@orpc/ratelimit 2.0.0-beta.34 → 2.0.0-beta.36
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 +30 -20
- package/dist/adapters/base-redis.d.mts +67 -0
- package/dist/adapters/base-redis.d.ts +67 -0
- package/dist/adapters/base-redis.mjs +52 -0
- package/dist/adapters/redis.d.mts +7 -47
- package/dist/adapters/redis.d.ts +7 -47
- package/dist/adapters/redis.mjs +6 -80
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev).
|
|
|
46
46
|
|
|
47
47
|
**Built-in features**
|
|
48
48
|
|
|
49
|
-
| Package
|
|
50
|
-
|
|
|
51
|
-
| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)
|
|
52
|
-
| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)
|
|
53
|
-
| [@orpc/
|
|
54
|
-
| [@orpc/
|
|
49
|
+
| Package | Purpose | Docs |
|
|
50
|
+
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
51
|
+
| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) |
|
|
52
|
+
| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit) | Rate limiting with memory, Redis, and Upstash adapters. | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) |
|
|
53
|
+
| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters. | [Lock](https://orpc.dev/docs/helpers/lock) |
|
|
54
|
+
| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation) | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation) |
|
|
55
|
+
| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema) | Smart coercion for OpenAPI requests. | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |
|
|
55
56
|
|
|
56
57
|
**Framework & ecosystem integrations**
|
|
57
58
|
|
|
58
|
-
| Package | Purpose
|
|
59
|
-
| ------------------------------------------------------------------------------------ |
|
|
60
|
-
| [@orpc/next](https://www.npmjs.com/package/@orpc/next) | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data).
|
|
61
|
-
| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk) | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.
|
|
62
|
-
| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query) | Integrate with [TanStack Query](https://tanstack.com/query/latest).
|
|
63
|
-
| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada) | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
|
|
64
|
-
| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/).
|
|
65
|
-
| [@orpc/experimental-msw](https://www.npmjs.com/package/@orpc/experimental-msw) | Mock procedures with [Mock Service Worker](https://mswjs.io/).
|
|
66
|
-
| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).
|
|
67
|
-
| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/).
|
|
68
|
-
| [@orpc/node](https://www.npmjs.com/package/@orpc/node) | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.
|
|
69
|
-
| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/).
|
|
70
|
-
| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare
|
|
71
|
-
| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc) | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
|
|
59
|
+
| Package | Purpose | Docs |
|
|
60
|
+
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| [@orpc/next](https://www.npmjs.com/package/@orpc/next) | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next) |
|
|
62
|
+
| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk) | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk) |
|
|
63
|
+
| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query) | Integrate with [TanStack Query](https://tanstack.com/query/latest). | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query) |
|
|
64
|
+
| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada) | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada) |
|
|
65
|
+
| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) |
|
|
66
|
+
| [@orpc/experimental-msw](https://www.npmjs.com/package/@orpc/experimental-msw) | Mock procedures with [Mock Service Worker](https://mswjs.io/). | [MSW](https://orpc.dev/docs/integrations/msw) |
|
|
67
|
+
| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) |
|
|
68
|
+
| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) |
|
|
69
|
+
| [@orpc/node](https://www.npmjs.com/package/@orpc/node) | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads. | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression) |
|
|
70
|
+
| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock) |
|
|
71
|
+
| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |
|
|
72
|
+
| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc) | Reuse existing [tRPC](https://trpc.io/) routers within oRPC. | [tRPC](https://orpc.dev/docs/integrations/trpc) |
|
|
72
73
|
|
|
73
74
|
**Observability**
|
|
74
75
|
|
|
@@ -94,6 +95,14 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
94
95
|
</tr>
|
|
95
96
|
</table>
|
|
96
97
|
|
|
98
|
+
### Special Sponsors
|
|
99
|
+
|
|
100
|
+
<table>
|
|
101
|
+
<tr>
|
|
102
|
+
<td align="center"><a href="http://twitter.com/rauchg?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guillermo Rauch"><img src="https://avatars.githubusercontent.com/u/13041?u=1ee8d111657cdd02ff6d253df00978d17ee6d722&v=4" width="279" alt="Guillermo Rauch"/><br />Guillermo Rauch</a></td>
|
|
103
|
+
</tr>
|
|
104
|
+
</table>
|
|
105
|
+
|
|
97
106
|
### Organization Sponsors
|
|
98
107
|
|
|
99
108
|
<table>
|
|
@@ -127,6 +136,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
127
136
|
<td align="center"><a href="https://github.com/ellis-driscoll?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
|
|
128
137
|
<td align="center"><a href="https://github.com/hoangbn?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
|
|
129
138
|
<td align="center"><a href="https://opencollective.com/guest-ac41de3b?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Orestis Ioannou"><img src="https://images.opencollective.com/guest-ac41de3b/avatar/460.png" width="139" alt="Orestis Ioannou"/><br />Orestis Ioannou</a></td>
|
|
139
|
+
<td align="center"><a href="https://automatio.ai/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Stefan Smiljkovic"><img src="https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&v=4" width="139" alt="Stefan Smiljkovic"/><br />Stefan Smiljkovic</a></td>
|
|
130
140
|
</tr>
|
|
131
141
|
</table>
|
|
132
142
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { RateLimiter, RateLimitOptions, RateLimitResult } from '../index.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options shared by every Redis-backed rate limiter adapter.
|
|
5
|
+
*
|
|
6
|
+
* @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters}
|
|
7
|
+
*/
|
|
8
|
+
interface BaseRedisRateLimiterOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The prefix to use for Redis keys.
|
|
11
|
+
*
|
|
12
|
+
* @default ''
|
|
13
|
+
*/
|
|
14
|
+
prefix?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of requests allowed within the window.
|
|
17
|
+
*/
|
|
18
|
+
maxRequests: number;
|
|
19
|
+
/**
|
|
20
|
+
* The duration of the fixed window in milliseconds.
|
|
21
|
+
*/
|
|
22
|
+
window: number;
|
|
23
|
+
/**
|
|
24
|
+
* Block until the request may pass or timeout is reached.
|
|
25
|
+
*
|
|
26
|
+
* @default { enabled: false }
|
|
27
|
+
*/
|
|
28
|
+
blockingUntilReady?: {
|
|
29
|
+
/**
|
|
30
|
+
* Block until the request may pass or timeout is reached.
|
|
31
|
+
*
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* milliseconds
|
|
37
|
+
*/
|
|
38
|
+
timeout: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Base class for Redis-backed rate limiter adapters. It owns the key naming,
|
|
43
|
+
* the fixed-window script, and the blocking logic, so every adapter built on
|
|
44
|
+
* it shares counters with the others regardless of the Redis client in use.
|
|
45
|
+
*
|
|
46
|
+
* Extend it and implement `evalScript` to support another Redis client.
|
|
47
|
+
*
|
|
48
|
+
* @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters}
|
|
49
|
+
*/
|
|
50
|
+
declare abstract class BaseRedisRateLimiter implements RateLimiter {
|
|
51
|
+
protected readonly prefix: string;
|
|
52
|
+
protected readonly maxRequests: number;
|
|
53
|
+
protected readonly window: number;
|
|
54
|
+
protected readonly blockingUntilReady: BaseRedisRateLimiterOptions['blockingUntilReady'];
|
|
55
|
+
constructor(options: BaseRedisRateLimiterOptions);
|
|
56
|
+
/**
|
|
57
|
+
* Runs a Lua script (`EVAL script numkeys key [key ...] arg [arg ...]`) and resolves with its reply.
|
|
58
|
+
*/
|
|
59
|
+
protected abstract evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
60
|
+
limit(key: string, options?: RateLimitOptions): Promise<Required<RateLimitResult>>;
|
|
61
|
+
private checkLimit;
|
|
62
|
+
private blockUntilReady;
|
|
63
|
+
private resolveWeight;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { BaseRedisRateLimiter };
|
|
67
|
+
export type { BaseRedisRateLimiterOptions };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { RateLimiter, RateLimitOptions, RateLimitResult } from '../index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options shared by every Redis-backed rate limiter adapter.
|
|
5
|
+
*
|
|
6
|
+
* @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters}
|
|
7
|
+
*/
|
|
8
|
+
interface BaseRedisRateLimiterOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The prefix to use for Redis keys.
|
|
11
|
+
*
|
|
12
|
+
* @default ''
|
|
13
|
+
*/
|
|
14
|
+
prefix?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of requests allowed within the window.
|
|
17
|
+
*/
|
|
18
|
+
maxRequests: number;
|
|
19
|
+
/**
|
|
20
|
+
* The duration of the fixed window in milliseconds.
|
|
21
|
+
*/
|
|
22
|
+
window: number;
|
|
23
|
+
/**
|
|
24
|
+
* Block until the request may pass or timeout is reached.
|
|
25
|
+
*
|
|
26
|
+
* @default { enabled: false }
|
|
27
|
+
*/
|
|
28
|
+
blockingUntilReady?: {
|
|
29
|
+
/**
|
|
30
|
+
* Block until the request may pass or timeout is reached.
|
|
31
|
+
*
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* milliseconds
|
|
37
|
+
*/
|
|
38
|
+
timeout: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Base class for Redis-backed rate limiter adapters. It owns the key naming,
|
|
43
|
+
* the fixed-window script, and the blocking logic, so every adapter built on
|
|
44
|
+
* it shares counters with the others regardless of the Redis client in use.
|
|
45
|
+
*
|
|
46
|
+
* Extend it and implement `evalScript` to support another Redis client.
|
|
47
|
+
*
|
|
48
|
+
* @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters}
|
|
49
|
+
*/
|
|
50
|
+
declare abstract class BaseRedisRateLimiter implements RateLimiter {
|
|
51
|
+
protected readonly prefix: string;
|
|
52
|
+
protected readonly maxRequests: number;
|
|
53
|
+
protected readonly window: number;
|
|
54
|
+
protected readonly blockingUntilReady: BaseRedisRateLimiterOptions['blockingUntilReady'];
|
|
55
|
+
constructor(options: BaseRedisRateLimiterOptions);
|
|
56
|
+
/**
|
|
57
|
+
* Runs a Lua script (`EVAL script numkeys key [key ...] arg [arg ...]`) and resolves with its reply.
|
|
58
|
+
*/
|
|
59
|
+
protected abstract evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
60
|
+
limit(key: string, options?: RateLimitOptions): Promise<Required<RateLimitResult>>;
|
|
61
|
+
private checkLimit;
|
|
62
|
+
private blockUntilReady;
|
|
63
|
+
private resolveWeight;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { BaseRedisRateLimiter };
|
|
67
|
+
export type { BaseRedisRateLimiterOptions };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { sleep } from '@orpc/shared';
|
|
2
|
+
|
|
3
|
+
const FIXED_WINDOW_SCRIPT = `local c=redis.call('INCRBY',KEYS[1],ARGV[1]) if c==tonumber(ARGV[1]) then redis.call('PEXPIRE',KEYS[1],ARGV[2]) end return {c,redis.call('PTTL',KEYS[1])}`;
|
|
4
|
+
class BaseRedisRateLimiter {
|
|
5
|
+
prefix;
|
|
6
|
+
maxRequests;
|
|
7
|
+
window;
|
|
8
|
+
blockingUntilReady;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.prefix = options.prefix ?? "";
|
|
11
|
+
this.maxRequests = options.maxRequests;
|
|
12
|
+
this.window = options.window;
|
|
13
|
+
this.blockingUntilReady = options.blockingUntilReady;
|
|
14
|
+
}
|
|
15
|
+
async limit(key, options) {
|
|
16
|
+
key = `${this.prefix}${key}`;
|
|
17
|
+
const weight = this.resolveWeight(options);
|
|
18
|
+
return this.blockingUntilReady?.enabled ? this.blockUntilReady(key, this.blockingUntilReady.timeout, weight) : this.checkLimit(key, weight);
|
|
19
|
+
}
|
|
20
|
+
async checkLimit(key, weight) {
|
|
21
|
+
const [used, ttl] = await this.evalScript(
|
|
22
|
+
FIXED_WINDOW_SCRIPT,
|
|
23
|
+
[key],
|
|
24
|
+
[String(weight), String(this.window)]
|
|
25
|
+
);
|
|
26
|
+
return {
|
|
27
|
+
success: used <= this.maxRequests,
|
|
28
|
+
limit: this.maxRequests,
|
|
29
|
+
remaining: Math.max(0, this.maxRequests - used),
|
|
30
|
+
reset: Date.now() + ttl
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async blockUntilReady(key, timeoutMs, weight) {
|
|
34
|
+
const deadlineAtMs = Date.now() + timeoutMs;
|
|
35
|
+
while (true) {
|
|
36
|
+
const result = await this.checkLimit(key, weight);
|
|
37
|
+
if (result.success || result.reset > deadlineAtMs) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
await sleep(result.reset - Date.now());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
resolveWeight(options) {
|
|
44
|
+
const weight = options?.weight ?? 1;
|
|
45
|
+
if (!Number.isInteger(weight) || weight <= 0) {
|
|
46
|
+
throw new TypeError("Rate limit weight must be an integer greater than 0");
|
|
47
|
+
}
|
|
48
|
+
return weight;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { BaseRedisRateLimiter };
|
|
@@ -1,38 +1,8 @@
|
|
|
1
|
-
import { RedisClientType } from 'redis';
|
|
2
|
-
import {
|
|
1
|
+
import { RedisClientType, RedisClusterType } from 'redis';
|
|
2
|
+
import { BaseRedisRateLimiter, BaseRedisRateLimiterOptions } from './base-redis.mjs';
|
|
3
|
+
import '../shared/ratelimit.DwqoOfMm.mjs';
|
|
3
4
|
|
|
4
|
-
interface RedisRateLimiterOptions {
|
|
5
|
-
/**
|
|
6
|
-
* The prefix to use for Redis keys.
|
|
7
|
-
*
|
|
8
|
-
* @default ''
|
|
9
|
-
*/
|
|
10
|
-
prefix?: string;
|
|
11
|
-
/**
|
|
12
|
-
* Maximum number of requests allowed within the window.
|
|
13
|
-
*/
|
|
14
|
-
maxRequests: number;
|
|
15
|
-
/**
|
|
16
|
-
* The duration of the fixed window in milliseconds.
|
|
17
|
-
*/
|
|
18
|
-
window: number;
|
|
19
|
-
/**
|
|
20
|
-
* Block until the request may pass or timeout is reached.
|
|
21
|
-
*
|
|
22
|
-
* @default { enabled: false }
|
|
23
|
-
*/
|
|
24
|
-
blockingUntilReady?: {
|
|
25
|
-
/**
|
|
26
|
-
* Block until the request may pass or timeout is reached.
|
|
27
|
-
*
|
|
28
|
-
* @default false
|
|
29
|
-
*/
|
|
30
|
-
enabled: boolean;
|
|
31
|
-
/**
|
|
32
|
-
* milliseconds
|
|
33
|
-
*/
|
|
34
|
-
timeout: number;
|
|
35
|
-
};
|
|
5
|
+
interface RedisRateLimiterOptions extends BaseRedisRateLimiterOptions {
|
|
36
6
|
}
|
|
37
7
|
/**
|
|
38
8
|
* Rate limiter adapter for Redis. Enforces a fixed-window limit using a
|
|
@@ -40,20 +10,10 @@ interface RedisRateLimiterOptions {
|
|
|
40
10
|
*
|
|
41
11
|
* @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters}
|
|
42
12
|
*/
|
|
43
|
-
declare class RedisRateLimiter
|
|
13
|
+
declare class RedisRateLimiter extends BaseRedisRateLimiter {
|
|
44
14
|
private readonly redis;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
private readonly window;
|
|
48
|
-
private readonly blockingUntilReady;
|
|
49
|
-
private scriptSha;
|
|
50
|
-
constructor(redis: RedisClientType<any, any, any, any, any>, options: RedisRateLimiterOptions);
|
|
51
|
-
limit(key: string, options?: RateLimitOptions): Promise<Required<RateLimitResult>>;
|
|
52
|
-
private checkLimit;
|
|
53
|
-
private blockUntilReady;
|
|
54
|
-
private executeScript;
|
|
55
|
-
private evalSha;
|
|
56
|
-
private resolveWeight;
|
|
15
|
+
constructor(redis: RedisClientType<any, any, any, any, any> | RedisClusterType<any, any, any, any, any>, options: RedisRateLimiterOptions);
|
|
16
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
57
17
|
}
|
|
58
18
|
|
|
59
19
|
export { RedisRateLimiter };
|
package/dist/adapters/redis.d.ts
CHANGED
|
@@ -1,38 +1,8 @@
|
|
|
1
|
-
import { RedisClientType } from 'redis';
|
|
2
|
-
import {
|
|
1
|
+
import { RedisClientType, RedisClusterType } from 'redis';
|
|
2
|
+
import { BaseRedisRateLimiter, BaseRedisRateLimiterOptions } from './base-redis.js';
|
|
3
|
+
import '../shared/ratelimit.DwqoOfMm.js';
|
|
3
4
|
|
|
4
|
-
interface RedisRateLimiterOptions {
|
|
5
|
-
/**
|
|
6
|
-
* The prefix to use for Redis keys.
|
|
7
|
-
*
|
|
8
|
-
* @default ''
|
|
9
|
-
*/
|
|
10
|
-
prefix?: string;
|
|
11
|
-
/**
|
|
12
|
-
* Maximum number of requests allowed within the window.
|
|
13
|
-
*/
|
|
14
|
-
maxRequests: number;
|
|
15
|
-
/**
|
|
16
|
-
* The duration of the fixed window in milliseconds.
|
|
17
|
-
*/
|
|
18
|
-
window: number;
|
|
19
|
-
/**
|
|
20
|
-
* Block until the request may pass or timeout is reached.
|
|
21
|
-
*
|
|
22
|
-
* @default { enabled: false }
|
|
23
|
-
*/
|
|
24
|
-
blockingUntilReady?: {
|
|
25
|
-
/**
|
|
26
|
-
* Block until the request may pass or timeout is reached.
|
|
27
|
-
*
|
|
28
|
-
* @default false
|
|
29
|
-
*/
|
|
30
|
-
enabled: boolean;
|
|
31
|
-
/**
|
|
32
|
-
* milliseconds
|
|
33
|
-
*/
|
|
34
|
-
timeout: number;
|
|
35
|
-
};
|
|
5
|
+
interface RedisRateLimiterOptions extends BaseRedisRateLimiterOptions {
|
|
36
6
|
}
|
|
37
7
|
/**
|
|
38
8
|
* Rate limiter adapter for Redis. Enforces a fixed-window limit using a
|
|
@@ -40,20 +10,10 @@ interface RedisRateLimiterOptions {
|
|
|
40
10
|
*
|
|
41
11
|
* @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters}
|
|
42
12
|
*/
|
|
43
|
-
declare class RedisRateLimiter
|
|
13
|
+
declare class RedisRateLimiter extends BaseRedisRateLimiter {
|
|
44
14
|
private readonly redis;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
private readonly window;
|
|
48
|
-
private readonly blockingUntilReady;
|
|
49
|
-
private scriptSha;
|
|
50
|
-
constructor(redis: RedisClientType<any, any, any, any, any>, options: RedisRateLimiterOptions);
|
|
51
|
-
limit(key: string, options?: RateLimitOptions): Promise<Required<RateLimitResult>>;
|
|
52
|
-
private checkLimit;
|
|
53
|
-
private blockUntilReady;
|
|
54
|
-
private executeScript;
|
|
55
|
-
private evalSha;
|
|
56
|
-
private resolveWeight;
|
|
15
|
+
constructor(redis: RedisClientType<any, any, any, any, any> | RedisClusterType<any, any, any, any, any>, options: RedisRateLimiterOptions);
|
|
16
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
57
17
|
}
|
|
58
18
|
|
|
59
19
|
export { RedisRateLimiter };
|
package/dist/adapters/redis.mjs
CHANGED
|
@@ -1,90 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseRedisRateLimiter } from './base-redis.mjs';
|
|
2
|
+
import '@orpc/shared';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
local key = KEYS[1]
|
|
5
|
-
local weight = tonumber(ARGV[1])
|
|
6
|
-
local window = tonumber(ARGV[2])
|
|
7
|
-
|
|
8
|
-
local current = redis.call('INCRBY', key, weight)
|
|
9
|
-
|
|
10
|
-
if current == weight then
|
|
11
|
-
redis.call('PEXPIRE', key, window)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
local ttl = redis.call('PTTL', key)
|
|
15
|
-
|
|
16
|
-
return { current, ttl }
|
|
17
|
-
`;
|
|
18
|
-
class RedisRateLimiter {
|
|
19
|
-
redis;
|
|
20
|
-
prefix;
|
|
21
|
-
maxRequests;
|
|
22
|
-
window;
|
|
23
|
-
blockingUntilReady;
|
|
24
|
-
scriptSha;
|
|
4
|
+
class RedisRateLimiter extends BaseRedisRateLimiter {
|
|
25
5
|
constructor(redis, options) {
|
|
6
|
+
super(options);
|
|
26
7
|
this.redis = redis;
|
|
27
|
-
this.prefix = options.prefix ?? "";
|
|
28
|
-
this.maxRequests = options.maxRequests;
|
|
29
|
-
this.window = options.window;
|
|
30
|
-
this.blockingUntilReady = options.blockingUntilReady;
|
|
31
8
|
}
|
|
32
|
-
async
|
|
33
|
-
key = `${this.prefix}${key}`;
|
|
34
|
-
const weight = this.resolveWeight(options);
|
|
9
|
+
async evalScript(script, keys, args) {
|
|
35
10
|
if (!this.redis.isOpen) {
|
|
36
11
|
await this.redis.connect();
|
|
37
12
|
}
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
async checkLimit(key, weight) {
|
|
41
|
-
const [used, ttl] = await this.executeScript(key, weight);
|
|
42
|
-
return {
|
|
43
|
-
success: used <= this.maxRequests,
|
|
44
|
-
limit: this.maxRequests,
|
|
45
|
-
remaining: Math.max(0, this.maxRequests - used),
|
|
46
|
-
reset: Date.now() + ttl
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
async blockUntilReady(key, timeoutMs, weight) {
|
|
50
|
-
const deadlineAtMs = Date.now() + timeoutMs;
|
|
51
|
-
while (true) {
|
|
52
|
-
const result = await this.checkLimit(key, weight);
|
|
53
|
-
if (result.success || result.reset > deadlineAtMs) {
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
await sleep(result.reset - Date.now());
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
async executeScript(key, weight) {
|
|
60
|
-
try {
|
|
61
|
-
return await this.evalSha(key, weight);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
if (error instanceof Error && error.message.startsWith("NOSCRIPT")) {
|
|
64
|
-
this.scriptSha = void 0;
|
|
65
|
-
return await this.evalSha(key, weight);
|
|
66
|
-
}
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
async evalSha(key, weight) {
|
|
71
|
-
this.scriptSha ??= await this.redis.scriptLoad(FIXED_WINDOW_RATELIMIT_SCRIPT);
|
|
72
|
-
return await this.redis.evalSha(this.scriptSha, {
|
|
73
|
-
keys: [key],
|
|
74
|
-
arguments: [
|
|
75
|
-
String(weight),
|
|
76
|
-
String(this.window)
|
|
77
|
-
]
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
resolveWeight(options) {
|
|
81
|
-
const weight = options?.weight ?? 1;
|
|
82
|
-
if (!Number.isInteger(weight) || weight <= 0) {
|
|
83
|
-
throw new TypeError(
|
|
84
|
-
"Rate limit weight must be an integer greater than 0"
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return weight;
|
|
13
|
+
return await this.redis.eval(script, { keys, arguments: args });
|
|
88
14
|
}
|
|
89
15
|
}
|
|
90
16
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orpc/ratelimit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.36",
|
|
5
5
|
"description": "Rate limiting for oRPC procedures, with memory, Redis, and Upstash adapters",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": [
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"import": "./dist/index.mjs",
|
|
33
33
|
"default": "./dist/index.mjs"
|
|
34
34
|
},
|
|
35
|
+
"./base-redis": {
|
|
36
|
+
"types": "./dist/adapters/base-redis.d.mts",
|
|
37
|
+
"import": "./dist/adapters/base-redis.mjs",
|
|
38
|
+
"default": "./dist/adapters/base-redis.mjs"
|
|
39
|
+
},
|
|
35
40
|
"./memory": {
|
|
36
41
|
"types": "./dist/adapters/memory.d.mts",
|
|
37
42
|
"import": "./dist/adapters/memory.mjs",
|
|
@@ -64,8 +69,8 @@
|
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
"dependencies": {
|
|
67
|
-
"@orpc/server": "2.0.0-beta.
|
|
68
|
-
"@orpc/shared": "2.0.0-beta.
|
|
72
|
+
"@orpc/server": "2.0.0-beta.36",
|
|
73
|
+
"@orpc/shared": "2.0.0-beta.36",
|
|
69
74
|
"@standard-server/core": "~0.9.0"
|
|
70
75
|
},
|
|
71
76
|
"devDependencies": {
|