@orpc/ratelimit 2.0.0-beta.35 → 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 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 | 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/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) |
54
- | [@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) |
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 | Docs |
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). | [Next.js](https://orpc.dev/docs/integrations/next) |
61
- | [@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) |
62
- | [@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) |
63
- | [@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) |
64
- | [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) |
65
- | [@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) |
66
- | [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) |
67
- | [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) |
68
- | [@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) |
69
- | [@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) |
70
- | [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |
71
- | [@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) |
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&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guillermo Rauch"><img src="https://avatars.githubusercontent.com/u/13041?u=1ee8d111657cdd02ff6d253df00978d17ee6d722&amp;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>
@@ -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 { RateLimiter, RateLimitOptions, RateLimitResult } from '../index.mjs';
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 implements RateLimiter {
13
+ declare class RedisRateLimiter extends BaseRedisRateLimiter {
44
14
  private readonly redis;
45
- private readonly prefix;
46
- private readonly maxRequests;
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 };
@@ -1,38 +1,8 @@
1
- import { RedisClientType } from 'redis';
2
- import { RateLimiter, RateLimitOptions, RateLimitResult } from '../index.js';
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 implements RateLimiter {
13
+ declare class RedisRateLimiter extends BaseRedisRateLimiter {
44
14
  private readonly redis;
45
- private readonly prefix;
46
- private readonly maxRequests;
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 };
@@ -1,90 +1,16 @@
1
- import { sleep } from '@orpc/shared';
1
+ import { BaseRedisRateLimiter } from './base-redis.mjs';
2
+ import '@orpc/shared';
2
3
 
3
- const FIXED_WINDOW_RATELIMIT_SCRIPT = `
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 limit(key, options) {
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 this.blockingUntilReady?.enabled ? this.blockUntilReady(key, this.blockingUntilReady.timeout, weight) : this.checkLimit(key, weight);
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.35",
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.35",
68
- "@orpc/shared": "2.0.0-beta.35",
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": {