@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 +30 -20
- package/dist/index.d.mts +28 -96
- package/dist/index.d.ts +28 -96
- package/dist/index.mjs +45 -192
- package/package.json +9 -7
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
|
|
47
|
-
|
|
|
48
|
-
| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)
|
|
49
|
-
| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)
|
|
50
|
-
| [@orpc/
|
|
51
|
-
| [@orpc/
|
|
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
|
|
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).
|
|
58
|
-
| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk) | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.
|
|
59
|
-
| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query) | Integrate with [TanStack Query](https://tanstack.com/query/latest).
|
|
60
|
-
| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada) | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
|
|
61
|
-
| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/).
|
|
62
|
-
| [@orpc/experimental-msw](https://www.npmjs.com/package/@orpc/experimental-msw) | Mock procedures with [Mock Service Worker](https://mswjs.io/).
|
|
63
|
-
| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).
|
|
64
|
-
| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/).
|
|
65
|
-
| [@orpc/node](https://www.npmjs.com/package/@orpc/node) | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.
|
|
66
|
-
| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/).
|
|
67
|
-
| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare
|
|
68
|
-
| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc) | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
|
|
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&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>
|
|
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&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>
|
|
125
134
|
<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>
|
|
126
135
|
<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>
|
|
136
|
+
<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>
|
|
127
137
|
</tr>
|
|
128
138
|
</table>
|
|
129
139
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Promisable } from '@orpc/shared';
|
|
1
|
+
import { BaseRedisLocker, BaseRedisLockerOptions } from '@orpc/experimental-lock/base-redis';
|
|
3
2
|
import { RedisClient } from 'bun';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
|
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
|
|
36
|
+
declare class BunRedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
|
|
64
37
|
private readonly redis;
|
|
65
38
|
private subscriber;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import { Promisable } from '@orpc/shared';
|
|
1
|
+
import { BaseRedisLocker, BaseRedisLockerOptions } from '@orpc/experimental-lock/base-redis';
|
|
3
2
|
import { RedisClient } from 'bun';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
|
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
|
|
36
|
+
declare class BunRedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
|
|
64
37
|
private readonly redis;
|
|
65
38
|
private subscriber;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
149
|
-
this.maxRequests = options.maxRequests;
|
|
150
|
-
this.window = options.window;
|
|
151
|
-
this.blockingUntilReady = options.blockingUntilReady;
|
|
23
|
+
this.subscriber = subscriber;
|
|
152
24
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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.
|
|
5
|
-
"description": "Bun integration for oRPC: Redis-backed pub/sub
|
|
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.
|
|
39
|
-
"@orpc/
|
|
40
|
-
"@orpc/
|
|
41
|
-
"@orpc/
|
|
42
|
-
"@orpc/
|
|
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": {
|