@orpc/bun 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 CHANGED
@@ -43,29 +43,30 @@ You can read the documentation [here](https://orpc.dev).
43
43
 
44
44
  **Built-in features**
45
45
 
46
- | Package | Purpose | Docs |
47
- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
48
- | [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) |
49
- | [@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) |
50
- | [@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) |
51
- | [@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) |
46
+ | Package | Purpose | Docs |
47
+ | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
48
+ | [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) |
49
+ | [@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) |
50
+ | [@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) |
51
+ | [@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) |
52
+ | [@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) |
52
53
 
53
54
  **Framework & ecosystem integrations**
54
55
 
55
- | Package | Purpose | Docs |
56
- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
57
- | [@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) |
58
- | [@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) |
59
- | [@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) |
60
- | [@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) |
61
- | [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) |
62
- | [@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) |
63
- | [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) |
64
- | [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) |
65
- | [@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) |
66
- | [@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) |
67
- | [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/). | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) |
68
- | [@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) |
56
+ | Package | Purpose | Docs |
57
+ | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | [@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) |
59
+ | [@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) |
60
+ | [@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) |
61
+ | [@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) |
62
+ | [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) |
63
+ | [@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) |
64
+ | [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) |
65
+ | [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) |
66
+ | [@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) |
67
+ | [@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) |
68
+ | [@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) |
69
+ | [@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) |
69
70
 
70
71
  **Observability**
71
72
 
@@ -91,6 +92,14 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
91
92
  </tr>
92
93
  </table>
93
94
 
95
+ ### Special Sponsors
96
+
97
+ <table>
98
+ <tr>
99
+ <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>
100
+ </tr>
101
+ </table>
102
+
94
103
  ### Organization Sponsors
95
104
 
96
105
  <table>
@@ -124,6 +133,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
124
133
  <td align="center"><a href="https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
125
134
  <td align="center"><a href="https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
126
135
  <td align="center"><a href="https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;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>
136
+ <td align="center"><a href="https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Stefan Smiljkovic"><img src="https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4" width="139" alt="Stefan Smiljkovic"/><br />Stefan Smiljkovic</a></td>
127
137
  </tr>
128
138
  </table>
129
139
 
package/dist/index.d.mts CHANGED
@@ -1,10 +1,23 @@
1
- import { Publisher, PublisherOptions, PublisherSubscribeListenerOptions } from '@orpc/publisher';
2
- import { Promisable } from '@orpc/shared';
1
+ import { BaseRedisLocker, BaseRedisLockerOptions } from '@orpc/experimental-lock/base-redis';
3
2
  import { RedisClient } from 'bun';
4
- import { RPCSerializer } from '@orpc/client';
5
- import { RateLimiter, RateLimitOptions, RateLimitResult } from '@orpc/ratelimit';
3
+ import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamTrimOptions, RedisStreamEntry } from '@orpc/publisher/base-redis';
4
+ import { Promisable } from '@orpc/shared';
5
+ import { BaseRedisRateLimiter, BaseRedisRateLimiterOptions } from '@orpc/ratelimit/base-redis';
6
+
7
+ /**
8
+ * Locker adapter for Bun's built-in Redis client. Acquires locks with `SET NX PX`
9
+ * and releases them atomically with a Lua script. It shares locks with `RedisLocker`.
10
+ *
11
+ * @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
12
+ */
13
+ declare class experimental_BunRedisLocker extends BaseRedisLocker {
14
+ private readonly redis;
15
+ constructor(redis: RedisClient, options: BaseRedisLockerOptions);
16
+ protected acquire(key: string, token: string, ttl: number): Promise<boolean>;
17
+ protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
18
+ }
6
19
 
7
- interface BunRedisPublisherOptions extends PublisherOptions {
20
+ interface BunRedisPublisherOptions extends BaseRedisPublisherOptions {
8
21
  /**
9
22
  * Redis subscriber instance.
10
23
  * Pub/Sub takes over the connection, so a client with subscriptions
@@ -13,46 +26,6 @@ interface BunRedisPublisherOptions extends PublisherOptions {
13
26
  * @default redis.duplicate() (lazily created on first listen)
14
27
  */
15
28
  subscriber?: undefined | Promisable<RedisClient>;
16
- /**
17
- * The prefix to use for Redis keys.
18
- *
19
- * @default ''
20
- */
21
- prefix?: string;
22
- /**
23
- * Serializer for serialize and deserialize payloads.
24
- *
25
- * @default RPCSerializer
26
- */
27
- serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
28
- /**
29
- * Configuration for event resume support.
30
- *
31
- * When enabled, published events are temporarily stored so new
32
- * subscribers can resume from a previous position using `lastEventId`.
33
- *
34
- * @default { enabled: false }
35
- */
36
- resume?: {
37
- /**
38
- * Whether event resume support is enabled.
39
- *
40
- * When enabled, published events are temporarily stored so new
41
- * subscribers can resume from a previous position using `lastEventId`.
42
- *
43
- * @default false
44
- */
45
- enabled: boolean;
46
- /**
47
- * How long (in seconds) to retain events for resume.
48
- *
49
- * Expired events are cleaned up lazily for performance reasons, so
50
- * some events may remain available slightly longer than this period.
51
- *
52
- * @default 300 (5 min)
53
- */
54
- seconds?: number;
55
- };
56
29
  }
57
30
  /**
58
31
  * Publisher adapter for Bun's built-in Redis client. Distributes events across
@@ -60,48 +33,17 @@ interface BunRedisPublisherOptions extends PublisherOptions {
60
33
  *
61
34
  * @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
62
35
  */
63
- declare class BunRedisPublisher<T extends Record<string, object>> extends Publisher<T> {
36
+ declare class BunRedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
64
37
  private readonly redis;
65
38
  private subscriber;
66
- private readonly prefix;
67
- private readonly serializer;
68
- private readonly resumeEnabled;
69
- private readonly resumeSeconds;
70
- /**
71
- * The exactness of the `XTRIM` command.
72
- * Used for testing purpose.
73
- */
74
- private readonly xTrimExactness;
75
- constructor(redis: RedisClient, options?: BunRedisPublisherOptions);
76
- private readonly firstPublishTimeMap;
77
- publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
78
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
79
- private serializePayload;
80
- private deserializePayload;
39
+ constructor(redis: RedisClient, { subscriber, ...options }?: BunRedisPublisherOptions);
40
+ protected publishMessage(channel: string, message: string): Promise<void>;
41
+ protected subscribeChannel(channel: string, listener: (message: unknown) => void): Promise<() => Promise<void>>;
42
+ protected addStreamEntry(key: string, data: string, trim?: RedisStreamTrimOptions): Promise<string>;
43
+ protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
81
44
  }
82
45
 
83
- interface BunRedisRateLimiterOptions {
84
- /**
85
- * The prefix to use for Redis keys.
86
- *
87
- * @default ''
88
- */
89
- prefix?: string;
90
- /**
91
- * Maximum number of requests allowed within the window.
92
- */
93
- maxRequests: number;
94
- /**
95
- * The duration of the fixed window in milliseconds.
96
- */
97
- window: number;
98
- /**
99
- * Block until the request may pass or timeout is reached.
100
- */
101
- blockingUntilReady?: {
102
- enabled: boolean;
103
- timeout: number;
104
- };
46
+ interface BunRedisRateLimiterOptions extends BaseRedisRateLimiterOptions {
105
47
  }
106
48
  /**
107
49
  * Rate limiter adapter for Bun's built-in Redis client. Enforces a fixed-window
@@ -109,21 +51,11 @@ interface BunRedisRateLimiterOptions {
109
51
  *
110
52
  * @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters}
111
53
  */
112
- declare class BunRedisRateLimiter implements RateLimiter {
54
+ declare class BunRedisRateLimiter extends BaseRedisRateLimiter {
113
55
  private readonly redis;
114
- private readonly prefix;
115
- private readonly maxRequests;
116
- private readonly window;
117
- private readonly blockingUntilReady;
118
- private scriptSha;
119
56
  constructor(redis: RedisClient, options: BunRedisRateLimiterOptions);
120
- limit(key: string, options?: RateLimitOptions): Promise<Required<RateLimitResult>>;
121
- private checkLimit;
122
- private blockUntilReady;
123
- private executeScript;
124
- private evalSha;
125
- private resolveWeight;
57
+ protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
126
58
  }
127
59
 
128
- export { BunRedisPublisher, BunRedisRateLimiter };
60
+ export { BunRedisPublisher, BunRedisRateLimiter, experimental_BunRedisLocker };
129
61
  export type { BunRedisPublisherOptions, BunRedisRateLimiterOptions };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,23 @@
1
- import { Publisher, PublisherOptions, PublisherSubscribeListenerOptions } from '@orpc/publisher';
2
- import { Promisable } from '@orpc/shared';
1
+ import { BaseRedisLocker, BaseRedisLockerOptions } from '@orpc/experimental-lock/base-redis';
3
2
  import { RedisClient } from 'bun';
4
- import { RPCSerializer } from '@orpc/client';
5
- import { RateLimiter, RateLimitOptions, RateLimitResult } from '@orpc/ratelimit';
3
+ import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamTrimOptions, RedisStreamEntry } from '@orpc/publisher/base-redis';
4
+ import { Promisable } from '@orpc/shared';
5
+ import { BaseRedisRateLimiter, BaseRedisRateLimiterOptions } from '@orpc/ratelimit/base-redis';
6
+
7
+ /**
8
+ * Locker adapter for Bun's built-in Redis client. Acquires locks with `SET NX PX`
9
+ * and releases them atomically with a Lua script. It shares locks with `RedisLocker`.
10
+ *
11
+ * @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
12
+ */
13
+ declare class experimental_BunRedisLocker extends BaseRedisLocker {
14
+ private readonly redis;
15
+ constructor(redis: RedisClient, options: BaseRedisLockerOptions);
16
+ protected acquire(key: string, token: string, ttl: number): Promise<boolean>;
17
+ protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
18
+ }
6
19
 
7
- interface BunRedisPublisherOptions extends PublisherOptions {
20
+ interface BunRedisPublisherOptions extends BaseRedisPublisherOptions {
8
21
  /**
9
22
  * Redis subscriber instance.
10
23
  * Pub/Sub takes over the connection, so a client with subscriptions
@@ -13,46 +26,6 @@ interface BunRedisPublisherOptions extends PublisherOptions {
13
26
  * @default redis.duplicate() (lazily created on first listen)
14
27
  */
15
28
  subscriber?: undefined | Promisable<RedisClient>;
16
- /**
17
- * The prefix to use for Redis keys.
18
- *
19
- * @default ''
20
- */
21
- prefix?: string;
22
- /**
23
- * Serializer for serialize and deserialize payloads.
24
- *
25
- * @default RPCSerializer
26
- */
27
- serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
28
- /**
29
- * Configuration for event resume support.
30
- *
31
- * When enabled, published events are temporarily stored so new
32
- * subscribers can resume from a previous position using `lastEventId`.
33
- *
34
- * @default { enabled: false }
35
- */
36
- resume?: {
37
- /**
38
- * Whether event resume support is enabled.
39
- *
40
- * When enabled, published events are temporarily stored so new
41
- * subscribers can resume from a previous position using `lastEventId`.
42
- *
43
- * @default false
44
- */
45
- enabled: boolean;
46
- /**
47
- * How long (in seconds) to retain events for resume.
48
- *
49
- * Expired events are cleaned up lazily for performance reasons, so
50
- * some events may remain available slightly longer than this period.
51
- *
52
- * @default 300 (5 min)
53
- */
54
- seconds?: number;
55
- };
56
29
  }
57
30
  /**
58
31
  * Publisher adapter for Bun's built-in Redis client. Distributes events across
@@ -60,48 +33,17 @@ interface BunRedisPublisherOptions extends PublisherOptions {
60
33
  *
61
34
  * @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
62
35
  */
63
- declare class BunRedisPublisher<T extends Record<string, object>> extends Publisher<T> {
36
+ declare class BunRedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
64
37
  private readonly redis;
65
38
  private subscriber;
66
- private readonly prefix;
67
- private readonly serializer;
68
- private readonly resumeEnabled;
69
- private readonly resumeSeconds;
70
- /**
71
- * The exactness of the `XTRIM` command.
72
- * Used for testing purpose.
73
- */
74
- private readonly xTrimExactness;
75
- constructor(redis: RedisClient, options?: BunRedisPublisherOptions);
76
- private readonly firstPublishTimeMap;
77
- publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
78
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
79
- private serializePayload;
80
- private deserializePayload;
39
+ constructor(redis: RedisClient, { subscriber, ...options }?: BunRedisPublisherOptions);
40
+ protected publishMessage(channel: string, message: string): Promise<void>;
41
+ protected subscribeChannel(channel: string, listener: (message: unknown) => void): Promise<() => Promise<void>>;
42
+ protected addStreamEntry(key: string, data: string, trim?: RedisStreamTrimOptions): Promise<string>;
43
+ protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
81
44
  }
82
45
 
83
- interface BunRedisRateLimiterOptions {
84
- /**
85
- * The prefix to use for Redis keys.
86
- *
87
- * @default ''
88
- */
89
- prefix?: string;
90
- /**
91
- * Maximum number of requests allowed within the window.
92
- */
93
- maxRequests: number;
94
- /**
95
- * The duration of the fixed window in milliseconds.
96
- */
97
- window: number;
98
- /**
99
- * Block until the request may pass or timeout is reached.
100
- */
101
- blockingUntilReady?: {
102
- enabled: boolean;
103
- timeout: number;
104
- };
46
+ interface BunRedisRateLimiterOptions extends BaseRedisRateLimiterOptions {
105
47
  }
106
48
  /**
107
49
  * Rate limiter adapter for Bun's built-in Redis client. Enforces a fixed-window
@@ -109,21 +51,11 @@ interface BunRedisRateLimiterOptions {
109
51
  *
110
52
  * @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters}
111
53
  */
112
- declare class BunRedisRateLimiter implements RateLimiter {
54
+ declare class BunRedisRateLimiter extends BaseRedisRateLimiter {
113
55
  private readonly redis;
114
- private readonly prefix;
115
- private readonly maxRequests;
116
- private readonly window;
117
- private readonly blockingUntilReady;
118
- private scriptSha;
119
56
  constructor(redis: RedisClient, options: BunRedisRateLimiterOptions);
120
- limit(key: string, options?: RateLimitOptions): Promise<Required<RateLimitResult>>;
121
- private checkLimit;
122
- private blockUntilReady;
123
- private executeScript;
124
- private evalSha;
125
- private resolveWeight;
57
+ protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
126
58
  }
127
59
 
128
- export { BunRedisPublisher, BunRedisRateLimiter };
60
+ export { BunRedisPublisher, BunRedisRateLimiter, experimental_BunRedisLocker };
129
61
  export type { BunRedisPublisherOptions, BunRedisRateLimiterOptions };
package/dist/index.mjs CHANGED
@@ -1,212 +1,65 @@
1
- import { RPCSerializer } from '@orpc/client';
2
- import { Publisher } from '@orpc/publisher';
3
- import { stringifyJSON, parseEmptyableJSON, once, sleep } from '@orpc/shared';
4
- import { unwrapEvent, withEventMeta, getEventMeta } from '@standard-server/core';
1
+ import { BaseRedisLocker } from '@orpc/experimental-lock/base-redis';
2
+ import { BaseRedisPublisher } from '@orpc/publisher/base-redis';
3
+ import { BaseRedisRateLimiter } from '@orpc/ratelimit/base-redis';
5
4
 
6
- class BunRedisPublisher extends Publisher {
7
- constructor(redis, options = {}) {
5
+ class experimental_BunRedisLocker extends BaseRedisLocker {
6
+ constructor(redis, options) {
8
7
  super(options);
9
8
  this.redis = redis;
10
- this.prefix = options.prefix ?? "";
11
- this.serializer = options.serializer ?? new RPCSerializer();
12
- this.resumeEnabled = options.resume?.enabled ?? false;
13
- this.resumeSeconds = options.resume?.seconds ?? 300;
14
- this.subscriber = options.subscriber;
15
9
  }
16
- subscriber;
17
- prefix;
18
- serializer;
19
- resumeEnabled;
20
- resumeSeconds;
21
- /**
22
- * The exactness of the `XTRIM` command.
23
- * Used for testing purpose.
24
- */
25
- xTrimExactness = "~";
26
- firstPublishTimeMap = /* @__PURE__ */ new Map();
27
- async publish(event, payload) {
28
- const redisKey = `${this.prefix}${event}`;
29
- const data = this.serializePayload(payload);
30
- let id;
31
- if (this.resumeEnabled) {
32
- const now = Date.now();
33
- for (const [event2, firstPublishTime] of this.firstPublishTimeMap) {
34
- if (firstPublishTime + this.resumeSeconds * 1e3 < now) {
35
- this.firstPublishTimeMap.delete(event2);
36
- }
37
- }
38
- if (!this.firstPublishTimeMap.has(event)) {
39
- this.firstPublishTimeMap.set(event, now);
40
- const result = await Promise.all([
41
- this.redis.send("XADD", [redisKey, "*", "data", stringifyJSON(data)]),
42
- this.redis.send("XTRIM", [redisKey, "MINID", this.xTrimExactness, `${now - this.resumeSeconds * 1e3}-0`]),
43
- // Use a 2x TTL so events published near the end of the resume window
44
- // are not expired before the next window updates the key expiration.
45
- this.redis.expire(redisKey, this.resumeSeconds * 2)
46
- ]);
47
- id = result[0];
48
- } else {
49
- id = await this.redis.send("XADD", [redisKey, "*", "data", stringifyJSON(data)]);
50
- }
51
- }
52
- await this.redis.publish(redisKey, stringifyJSON({ data, id }));
10
+ async acquire(key, token, ttl) {
11
+ const result = await this.redis.send("SET", [key, token, "NX", "PX", String(ttl)]);
12
+ return result === "OK";
53
13
  }
54
- async subscribeListener(event, originalListener, { lastEventId, onError } = {}) {
55
- const redisKey = `${this.prefix}${event}`;
56
- let pendingPayloads = [];
57
- const resumedIds = /* @__PURE__ */ new Set();
58
- const deduplicatingListener = (payload) => {
59
- if (pendingPayloads) {
60
- pendingPayloads.push(payload);
61
- return;
62
- }
63
- const id = getEventMeta(payload)?.id;
64
- if (id !== void 0 && resumedIds.has(id)) {
65
- return;
66
- }
67
- originalListener(payload);
68
- };
69
- const redisListener = (message) => {
70
- try {
71
- const { id, data } = parseEmptyableJSON(message);
72
- const payload = this.deserializePayload(id, data);
73
- deduplicatingListener(payload);
74
- } catch (error) {
75
- onError?.(error);
76
- }
77
- };
78
- this.subscriber ??= this.redis.duplicate();
79
- const subscriber = await this.subscriber;
80
- try {
81
- const subscribePromise = subscriber.subscribe(redisKey, redisListener);
82
- try {
83
- if (this.resumeEnabled && lastEventId !== void 0) {
84
- const results = await this.redis.send("XREAD", ["STREAMS", redisKey, lastEventId]);
85
- if (results && results[redisKey]) {
86
- const messages = results[redisKey];
87
- for (const [id, message] of messages) {
88
- const rawData = message[1];
89
- const data = parseEmptyableJSON(rawData);
90
- const payload = this.deserializePayload(id, data);
91
- resumedIds.add(id);
92
- originalListener(payload);
93
- }
94
- }
95
- }
96
- } finally {
97
- const pending = pendingPayloads;
98
- pendingPayloads = void 0;
99
- for (const payload of pending) {
100
- deduplicatingListener(payload);
101
- }
102
- }
103
- await subscribePromise;
104
- } catch (error) {
105
- await subscriber.unsubscribe(redisKey, redisListener);
106
- throw error;
107
- }
108
- return once(async () => {
109
- await subscriber.unsubscribe(redisKey, redisListener);
110
- });
111
- }
112
- serializePayload(payload) {
113
- const [original, meta] = unwrapEvent(payload);
114
- return { payload: this.serializer.serialize(original, { useFormDataForBlobFields: false }), meta };
115
- }
116
- deserializePayload(id, { payload, meta }) {
117
- return withEventMeta(
118
- this.serializer.deserialize(payload),
119
- id === void 0 ? { ...meta } : { ...meta, id }
120
- );
14
+ async evalScript(script, keys, args) {
15
+ return await this.redis.send("EVAL", [script, String(keys.length), ...keys, ...args]);
121
16
  }
122
17
  }
123
18
 
124
- const FIXED_WINDOW_RATELIMIT_SCRIPT = `
125
- local key = KEYS[1]
126
- local weight = tonumber(ARGV[1])
127
- local window = tonumber(ARGV[2])
128
-
129
- local current = redis.call('INCRBY', key, weight)
130
-
131
- if current == weight then
132
- redis.call('PEXPIRE', key, window)
133
- end
134
-
135
- local ttl = redis.call('PTTL', key)
136
-
137
- return { current, ttl }
138
- `;
139
- class BunRedisRateLimiter {
140
- redis;
141
- prefix;
142
- maxRequests;
143
- window;
144
- blockingUntilReady;
145
- scriptSha;
146
- constructor(redis, options) {
19
+ class BunRedisPublisher extends BaseRedisPublisher {
20
+ constructor(redis, { subscriber, ...options } = {}) {
21
+ super(options);
147
22
  this.redis = redis;
148
- this.prefix = options.prefix ?? "";
149
- this.maxRequests = options.maxRequests;
150
- this.window = options.window;
151
- this.blockingUntilReady = options.blockingUntilReady;
23
+ this.subscriber = subscriber;
152
24
  }
153
- async limit(key, options) {
154
- key = `${this.prefix}${key}`;
155
- const weight = this.resolveWeight(options);
156
- return this.blockingUntilReady?.enabled ? this.blockUntilReady(key, this.blockingUntilReady.timeout, weight) : this.checkLimit(key, weight);
25
+ subscriber;
26
+ async publishMessage(channel, message) {
27
+ await this.redis.publish(channel, message);
157
28
  }
158
- async checkLimit(key, weight) {
159
- const [used, ttl] = await this.executeScript(key, weight);
160
- return {
161
- success: used <= this.maxRequests,
162
- limit: this.maxRequests,
163
- remaining: Math.max(0, this.maxRequests - used),
164
- reset: Date.now() + ttl
29
+ async subscribeChannel(channel, listener) {
30
+ this.subscriber ??= this.redis.duplicate();
31
+ const subscriber = await this.subscriber;
32
+ await subscriber.subscribe(channel, listener);
33
+ return async () => {
34
+ await subscriber.unsubscribe(channel, listener);
165
35
  };
166
36
  }
167
- async blockUntilReady(key, timeoutMs, weight) {
168
- const deadlineAtMs = Date.now() + timeoutMs;
169
- while (true) {
170
- const result = await this.checkLimit(key, weight);
171
- if (result.success || result.reset > deadlineAtMs) {
172
- return result;
173
- }
174
- await sleep(result.reset - Date.now());
37
+ async addStreamEntry(key, data, trim) {
38
+ if (!trim) {
39
+ return await this.redis.send("XADD", [key, "*", "data", data]);
175
40
  }
41
+ const [id] = await Promise.all([
42
+ this.redis.send("XADD", [key, "*", "data", data]),
43
+ this.redis.send("XTRIM", [key, "MINID", trim.exactness, trim.minId]),
44
+ this.redis.expire(key, trim.expireSeconds)
45
+ ]);
46
+ return id;
176
47
  }
177
- async executeScript(key, weight) {
178
- try {
179
- return await this.evalSha(key, weight);
180
- } catch (error) {
181
- if (error instanceof Error && error.message.startsWith("NOSCRIPT")) {
182
- this.scriptSha = void 0;
183
- return await this.evalSha(key, weight);
184
- }
185
- throw error;
186
- }
48
+ async readStreamEntries(key, lastId) {
49
+ const results = await this.redis.send("XREAD", ["STREAMS", key, lastId]);
50
+ const entries = results?.[key] ?? [];
51
+ return entries.map(([id, fields]) => ({ id, data: fields[1] }));
187
52
  }
188
- async evalSha(key, weight) {
189
- this.scriptSha ??= await this.redis.send("SCRIPT", [
190
- "LOAD",
191
- FIXED_WINDOW_RATELIMIT_SCRIPT
192
- ]);
193
- return await this.redis.send("EVALSHA", [
194
- this.scriptSha,
195
- "1",
196
- key,
197
- String(weight),
198
- String(this.window)
199
- ]);
53
+ }
54
+
55
+ class BunRedisRateLimiter extends BaseRedisRateLimiter {
56
+ constructor(redis, options) {
57
+ super(options);
58
+ this.redis = redis;
200
59
  }
201
- resolveWeight(options) {
202
- const weight = options?.weight ?? 1;
203
- if (!Number.isInteger(weight) || weight <= 0) {
204
- throw new TypeError(
205
- "Rate limit weight must be an integer greater than 0"
206
- );
207
- }
208
- return weight;
60
+ async evalScript(script, keys, args) {
61
+ return await this.redis.send("EVAL", [script, String(keys.length), ...keys, ...args]);
209
62
  }
210
63
  }
211
64
 
212
- export { BunRedisPublisher, BunRedisRateLimiter };
65
+ export { BunRedisPublisher, BunRedisRateLimiter, experimental_BunRedisLocker };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@orpc/bun",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.34",
5
- "description": "Bun integration for oRPC: Redis-backed pub/sub and rate limiting using Bun's built-in Redis client",
4
+ "version": "2.0.0-beta.36",
5
+ "description": "Bun integration for oRPC: Redis-backed pub/sub, rate limiting, and locking using Bun's built-in Redis client",
6
6
  "license": "MIT",
7
7
  "funding": [
8
8
  "https://github.com/sponsors/dinwwwh",
@@ -20,6 +20,7 @@
20
20
  "redis",
21
21
  "pubsub",
22
22
  "ratelimit",
23
+ "lock",
23
24
  "typescript"
24
25
  ],
25
26
  "sideEffects": false,
@@ -35,11 +36,12 @@
35
36
  "dist"
36
37
  ],
37
38
  "dependencies": {
38
- "@orpc/client": "2.0.0-beta.34",
39
- "@orpc/publisher": "2.0.0-beta.34",
40
- "@orpc/ratelimit": "2.0.0-beta.34",
41
- "@orpc/server": "2.0.0-beta.34",
42
- "@orpc/shared": "2.0.0-beta.34",
39
+ "@orpc/client": "2.0.0-beta.36",
40
+ "@orpc/experimental-lock": "2.0.0-beta.36",
41
+ "@orpc/publisher": "2.0.0-beta.36",
42
+ "@orpc/ratelimit": "2.0.0-beta.36",
43
+ "@orpc/server": "2.0.0-beta.36",
44
+ "@orpc/shared": "2.0.0-beta.36",
43
45
  "@standard-server/core": "~0.9.0"
44
46
  },
45
47
  "devDependencies": {