@orpc/publisher 2.0.0-beta.33 → 2.0.0-beta.35
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 +15 -14
- package/dist/adapters/base-redis.d.mts +138 -0
- package/dist/adapters/base-redis.d.ts +138 -0
- package/dist/adapters/base-redis.mjs +104 -0
- package/dist/adapters/memory.mjs +1 -1
- package/dist/adapters/redis.d.mts +10 -59
- package/dist/adapters/redis.d.ts +10 -59
- package/dist/adapters/redis.mjs +32 -111
- package/dist/adapters/upstash.d.mts +13 -62
- package/dist/adapters/upstash.d.ts +13 -62
- package/dist/adapters/upstash.mjs +69 -164
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -55,20 +55,20 @@ You can read the documentation [here](https://orpc.dev).
|
|
|
55
55
|
|
|
56
56
|
**Framework & ecosystem integrations**
|
|
57
57
|
|
|
58
|
-
| Package | Purpose
|
|
59
|
-
| ------------------------------------------------------------------------------------ |
|
|
60
|
-
| [@orpc/next](https://www.npmjs.com/package/@orpc/next) | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data).
|
|
61
|
-
| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk) | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.
|
|
62
|
-
| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query) | Integrate with [TanStack Query](https://tanstack.com/query/latest).
|
|
63
|
-
| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada) | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
|
|
64
|
-
| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/).
|
|
65
|
-
| [@orpc/experimental-msw](https://www.npmjs.com/package/@orpc/experimental-msw) | Mock procedures with [Mock Service Worker](https://mswjs.io/).
|
|
66
|
-
| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).
|
|
67
|
-
| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/).
|
|
68
|
-
| [@orpc/node](https://www.npmjs.com/package/@orpc/node) | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.
|
|
69
|
-
| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/).
|
|
70
|
-
| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare
|
|
71
|
-
| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc) | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
|
|
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) |
|
|
72
72
|
|
|
73
73
|
**Observability**
|
|
74
74
|
|
|
@@ -127,6 +127,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
127
127
|
<td align="center"><a href="https://github.com/ellis-driscoll?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
|
|
128
128
|
<td align="center"><a href="https://github.com/hoangbn?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
|
|
129
129
|
<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>
|
|
130
|
+
<td align="center"><a href="https://automatio.ai/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Stefan Smiljkovic"><img src="https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&v=4" width="139" alt="Stefan Smiljkovic"/><br />Stefan Smiljkovic</a></td>
|
|
130
131
|
</tr>
|
|
131
132
|
</table>
|
|
132
133
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Public, ThrowableError } from '@orpc/shared';
|
|
2
|
+
import { Publisher, PublisherOptions, PublisherSubscribeListenerOptions } from '../index.mjs';
|
|
3
|
+
import { RPCJsonSerializer } from '@orpc/client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options shared by every Redis-backed publisher adapter.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
9
|
+
*/
|
|
10
|
+
interface BaseRedisPublisherOptions extends PublisherOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The prefix to use for Redis keys.
|
|
13
|
+
*
|
|
14
|
+
* @default ''
|
|
15
|
+
*/
|
|
16
|
+
prefix?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Serializer for serialize and deserialize payloads.
|
|
19
|
+
*
|
|
20
|
+
* @default RPCJsonSerializer
|
|
21
|
+
*/
|
|
22
|
+
serializer?: undefined | Public<RPCJsonSerializer>;
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for event resume support.
|
|
25
|
+
*
|
|
26
|
+
* When enabled, published events are temporarily stored so new
|
|
27
|
+
* subscribers can resume from a previous position using `lastEventId`.
|
|
28
|
+
*
|
|
29
|
+
* @default { enabled: false }
|
|
30
|
+
*/
|
|
31
|
+
resume?: {
|
|
32
|
+
/**
|
|
33
|
+
* Whether event resume support is enabled.
|
|
34
|
+
*
|
|
35
|
+
* When enabled, published events are temporarily stored so new
|
|
36
|
+
* subscribers can resume from a previous position using `lastEventId`.
|
|
37
|
+
*
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* How long (in seconds) to retain events for resume.
|
|
43
|
+
*
|
|
44
|
+
* Expired events are cleaned up lazily for performance reasons, so
|
|
45
|
+
* some events may remain available slightly longer than this period.
|
|
46
|
+
*
|
|
47
|
+
* @default 300 (5 min)
|
|
48
|
+
*/
|
|
49
|
+
seconds?: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* An entry of the Redis Stream that stores events for resume.
|
|
54
|
+
*
|
|
55
|
+
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
56
|
+
*/
|
|
57
|
+
interface RedisStreamEntry {
|
|
58
|
+
/**
|
|
59
|
+
* The entry ID assigned by Redis, which doubles as the event ID.
|
|
60
|
+
*/
|
|
61
|
+
id: string;
|
|
62
|
+
/**
|
|
63
|
+
* The `data` field, as JSON text or already parsed by clients that deserialize automatically.
|
|
64
|
+
*/
|
|
65
|
+
data: unknown;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Trimming to apply to a Redis Stream while adding an entry.
|
|
69
|
+
*
|
|
70
|
+
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
71
|
+
*/
|
|
72
|
+
interface RedisStreamTrimOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Entries with an ID lower than this one are removed (`XTRIM key MINID minId`).
|
|
75
|
+
*/
|
|
76
|
+
minId: string;
|
|
77
|
+
/**
|
|
78
|
+
* Whether trimming is exact (`=`) or approximate (`~`).
|
|
79
|
+
*/
|
|
80
|
+
exactness: '~' | '=';
|
|
81
|
+
/**
|
|
82
|
+
* Time to live to set on the stream key, in seconds (`EXPIRE key seconds`).
|
|
83
|
+
*/
|
|
84
|
+
expireSeconds: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Base class for Redis-backed publisher adapters. It owns the key naming, the
|
|
88
|
+
* message format, and the resume logic, so every adapter built on it can exchange
|
|
89
|
+
* events with the others regardless of the Redis client in use.
|
|
90
|
+
*
|
|
91
|
+
* Extend it and implement the abstract methods to support another Redis client.
|
|
92
|
+
*
|
|
93
|
+
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
94
|
+
*/
|
|
95
|
+
declare abstract class BaseRedisPublisher<T extends Record<string, object>> extends Publisher<T> {
|
|
96
|
+
protected readonly prefix: string;
|
|
97
|
+
protected readonly serializer: Public<RPCJsonSerializer>;
|
|
98
|
+
protected readonly resumeEnabled: boolean;
|
|
99
|
+
protected readonly resumeSeconds: number;
|
|
100
|
+
/**
|
|
101
|
+
* Tests switch this to `=` for deterministic trimming.
|
|
102
|
+
*/
|
|
103
|
+
protected readonly xTrimExactness: '~' | '=';
|
|
104
|
+
private readonly lastTrimTimes;
|
|
105
|
+
constructor({ prefix, serializer, resume, ...options }?: BaseRedisPublisherOptions);
|
|
106
|
+
/**
|
|
107
|
+
* Sends a message to a Pub/Sub channel (`PUBLISH channel message`).
|
|
108
|
+
*/
|
|
109
|
+
protected abstract publishMessage(channel: string, message: string): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Listens for messages on a Pub/Sub channel (`SUBSCRIBE channel`) and resolves
|
|
112
|
+
* with a function that stops listening (`UNSUBSCRIBE channel`).
|
|
113
|
+
*
|
|
114
|
+
* Messages reach the listener as JSON text, or already parsed by clients that
|
|
115
|
+
* deserialize automatically. The listener may be invoked as soon as the subscription is established, even
|
|
116
|
+
* before the returned promise settles. When the promise rejects, nothing may be
|
|
117
|
+
* left subscribed. `onError` reports failures of the established subscription,
|
|
118
|
+
* such as a lost connection.
|
|
119
|
+
*/
|
|
120
|
+
protected abstract subscribeChannel(channel: string, listener: (message: unknown) => void, onError?: (error: ThrowableError) => void): Promise<() => Promise<void>>;
|
|
121
|
+
/**
|
|
122
|
+
* Appends an entry to a stream (`XADD key * data <data>`) and resolves with its ID.
|
|
123
|
+
* When `trim` is provided, also trims the stream and refreshes its TTL,
|
|
124
|
+
* preferably within the same round trip.
|
|
125
|
+
*/
|
|
126
|
+
protected abstract addStreamEntry(key: string, data: string, trim?: RedisStreamTrimOptions): Promise<string>;
|
|
127
|
+
/**
|
|
128
|
+
* Reads the entries with an ID greater than `lastId` (`XREAD STREAMS key lastId`), oldest first.
|
|
129
|
+
*/
|
|
130
|
+
protected abstract readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
131
|
+
publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
|
|
132
|
+
protected subscribeListener<K extends keyof T & string>(event: K, listener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
133
|
+
private serializePayload;
|
|
134
|
+
private deserializePayload;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { BaseRedisPublisher };
|
|
138
|
+
export type { BaseRedisPublisherOptions, RedisStreamEntry, RedisStreamTrimOptions };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Public, ThrowableError } from '@orpc/shared';
|
|
2
|
+
import { Publisher, PublisherOptions, PublisherSubscribeListenerOptions } from '../index.js';
|
|
3
|
+
import { RPCJsonSerializer } from '@orpc/client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options shared by every Redis-backed publisher adapter.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
9
|
+
*/
|
|
10
|
+
interface BaseRedisPublisherOptions extends PublisherOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The prefix to use for Redis keys.
|
|
13
|
+
*
|
|
14
|
+
* @default ''
|
|
15
|
+
*/
|
|
16
|
+
prefix?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Serializer for serialize and deserialize payloads.
|
|
19
|
+
*
|
|
20
|
+
* @default RPCJsonSerializer
|
|
21
|
+
*/
|
|
22
|
+
serializer?: undefined | Public<RPCJsonSerializer>;
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for event resume support.
|
|
25
|
+
*
|
|
26
|
+
* When enabled, published events are temporarily stored so new
|
|
27
|
+
* subscribers can resume from a previous position using `lastEventId`.
|
|
28
|
+
*
|
|
29
|
+
* @default { enabled: false }
|
|
30
|
+
*/
|
|
31
|
+
resume?: {
|
|
32
|
+
/**
|
|
33
|
+
* Whether event resume support is enabled.
|
|
34
|
+
*
|
|
35
|
+
* When enabled, published events are temporarily stored so new
|
|
36
|
+
* subscribers can resume from a previous position using `lastEventId`.
|
|
37
|
+
*
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* How long (in seconds) to retain events for resume.
|
|
43
|
+
*
|
|
44
|
+
* Expired events are cleaned up lazily for performance reasons, so
|
|
45
|
+
* some events may remain available slightly longer than this period.
|
|
46
|
+
*
|
|
47
|
+
* @default 300 (5 min)
|
|
48
|
+
*/
|
|
49
|
+
seconds?: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* An entry of the Redis Stream that stores events for resume.
|
|
54
|
+
*
|
|
55
|
+
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
56
|
+
*/
|
|
57
|
+
interface RedisStreamEntry {
|
|
58
|
+
/**
|
|
59
|
+
* The entry ID assigned by Redis, which doubles as the event ID.
|
|
60
|
+
*/
|
|
61
|
+
id: string;
|
|
62
|
+
/**
|
|
63
|
+
* The `data` field, as JSON text or already parsed by clients that deserialize automatically.
|
|
64
|
+
*/
|
|
65
|
+
data: unknown;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Trimming to apply to a Redis Stream while adding an entry.
|
|
69
|
+
*
|
|
70
|
+
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
71
|
+
*/
|
|
72
|
+
interface RedisStreamTrimOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Entries with an ID lower than this one are removed (`XTRIM key MINID minId`).
|
|
75
|
+
*/
|
|
76
|
+
minId: string;
|
|
77
|
+
/**
|
|
78
|
+
* Whether trimming is exact (`=`) or approximate (`~`).
|
|
79
|
+
*/
|
|
80
|
+
exactness: '~' | '=';
|
|
81
|
+
/**
|
|
82
|
+
* Time to live to set on the stream key, in seconds (`EXPIRE key seconds`).
|
|
83
|
+
*/
|
|
84
|
+
expireSeconds: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Base class for Redis-backed publisher adapters. It owns the key naming, the
|
|
88
|
+
* message format, and the resume logic, so every adapter built on it can exchange
|
|
89
|
+
* events with the others regardless of the Redis client in use.
|
|
90
|
+
*
|
|
91
|
+
* Extend it and implement the abstract methods to support another Redis client.
|
|
92
|
+
*
|
|
93
|
+
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
94
|
+
*/
|
|
95
|
+
declare abstract class BaseRedisPublisher<T extends Record<string, object>> extends Publisher<T> {
|
|
96
|
+
protected readonly prefix: string;
|
|
97
|
+
protected readonly serializer: Public<RPCJsonSerializer>;
|
|
98
|
+
protected readonly resumeEnabled: boolean;
|
|
99
|
+
protected readonly resumeSeconds: number;
|
|
100
|
+
/**
|
|
101
|
+
* Tests switch this to `=` for deterministic trimming.
|
|
102
|
+
*/
|
|
103
|
+
protected readonly xTrimExactness: '~' | '=';
|
|
104
|
+
private readonly lastTrimTimes;
|
|
105
|
+
constructor({ prefix, serializer, resume, ...options }?: BaseRedisPublisherOptions);
|
|
106
|
+
/**
|
|
107
|
+
* Sends a message to a Pub/Sub channel (`PUBLISH channel message`).
|
|
108
|
+
*/
|
|
109
|
+
protected abstract publishMessage(channel: string, message: string): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Listens for messages on a Pub/Sub channel (`SUBSCRIBE channel`) and resolves
|
|
112
|
+
* with a function that stops listening (`UNSUBSCRIBE channel`).
|
|
113
|
+
*
|
|
114
|
+
* Messages reach the listener as JSON text, or already parsed by clients that
|
|
115
|
+
* deserialize automatically. The listener may be invoked as soon as the subscription is established, even
|
|
116
|
+
* before the returned promise settles. When the promise rejects, nothing may be
|
|
117
|
+
* left subscribed. `onError` reports failures of the established subscription,
|
|
118
|
+
* such as a lost connection.
|
|
119
|
+
*/
|
|
120
|
+
protected abstract subscribeChannel(channel: string, listener: (message: unknown) => void, onError?: (error: ThrowableError) => void): Promise<() => Promise<void>>;
|
|
121
|
+
/**
|
|
122
|
+
* Appends an entry to a stream (`XADD key * data <data>`) and resolves with its ID.
|
|
123
|
+
* When `trim` is provided, also trims the stream and refreshes its TTL,
|
|
124
|
+
* preferably within the same round trip.
|
|
125
|
+
*/
|
|
126
|
+
protected abstract addStreamEntry(key: string, data: string, trim?: RedisStreamTrimOptions): Promise<string>;
|
|
127
|
+
/**
|
|
128
|
+
* Reads the entries with an ID greater than `lastId` (`XREAD STREAMS key lastId`), oldest first.
|
|
129
|
+
*/
|
|
130
|
+
protected abstract readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
131
|
+
publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
|
|
132
|
+
protected subscribeListener<K extends keyof T & string>(event: K, listener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
133
|
+
private serializePayload;
|
|
134
|
+
private deserializePayload;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { BaseRedisPublisher };
|
|
138
|
+
export type { BaseRedisPublisherOptions, RedisStreamEntry, RedisStreamTrimOptions };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { RPCJsonSerializer } from '@orpc/client';
|
|
2
|
+
import { stringifyJSON, once, parseEmptyableJSON } from '@orpc/shared';
|
|
3
|
+
import { unwrapEvent, withEventMeta, getEventMeta } from '@standard-server/core';
|
|
4
|
+
import { P as Publisher } from '../shared/publisher.BSQf8ybR.mjs';
|
|
5
|
+
|
|
6
|
+
class BaseRedisPublisher extends Publisher {
|
|
7
|
+
prefix;
|
|
8
|
+
serializer;
|
|
9
|
+
resumeEnabled;
|
|
10
|
+
resumeSeconds;
|
|
11
|
+
/**
|
|
12
|
+
* Tests switch this to `=` for deterministic trimming.
|
|
13
|
+
*/
|
|
14
|
+
xTrimExactness = "~";
|
|
15
|
+
lastTrimTimes = /* @__PURE__ */ new Map();
|
|
16
|
+
constructor({ prefix, serializer, resume, ...options } = {}) {
|
|
17
|
+
super(options);
|
|
18
|
+
this.prefix = prefix ?? "";
|
|
19
|
+
this.serializer = serializer ?? new RPCJsonSerializer();
|
|
20
|
+
this.resumeEnabled = resume?.enabled ?? false;
|
|
21
|
+
this.resumeSeconds = resume?.seconds ?? 300;
|
|
22
|
+
}
|
|
23
|
+
async publish(event, payload) {
|
|
24
|
+
const channel = `${this.prefix}${event}`;
|
|
25
|
+
const data = this.serializePayload(payload);
|
|
26
|
+
let id;
|
|
27
|
+
if (this.resumeEnabled) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const windowMs = this.resumeSeconds * 1e3;
|
|
30
|
+
for (const [trimmedChannel, trimTime] of this.lastTrimTimes) {
|
|
31
|
+
if (trimTime + windowMs < now) {
|
|
32
|
+
this.lastTrimTimes.delete(trimmedChannel);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (this.lastTrimTimes.has(channel)) {
|
|
36
|
+
id = await this.addStreamEntry(channel, stringifyJSON(data));
|
|
37
|
+
} else {
|
|
38
|
+
this.lastTrimTimes.set(channel, now);
|
|
39
|
+
id = await this.addStreamEntry(channel, stringifyJSON(data), {
|
|
40
|
+
minId: `${now - windowMs}-0`,
|
|
41
|
+
exactness: this.xTrimExactness,
|
|
42
|
+
// 2x so entries added late in a window outlive it until the next trim refreshes the TTL.
|
|
43
|
+
expireSeconds: this.resumeSeconds * 2
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await this.publishMessage(channel, stringifyJSON({ data, id }));
|
|
48
|
+
}
|
|
49
|
+
async subscribeListener(event, listener, { lastEventId, onError } = {}) {
|
|
50
|
+
const channel = `${this.prefix}${event}`;
|
|
51
|
+
let pendingPayloads = [];
|
|
52
|
+
const resumedIds = /* @__PURE__ */ new Set();
|
|
53
|
+
const deduplicatingListener = (payload) => {
|
|
54
|
+
if (pendingPayloads) {
|
|
55
|
+
pendingPayloads.push(payload);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const id = getEventMeta(payload)?.id;
|
|
59
|
+
if (id === void 0 || !resumedIds.has(id)) {
|
|
60
|
+
listener(payload);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const messageListener = (message) => {
|
|
64
|
+
try {
|
|
65
|
+
const { id, data } = parseIfText(message);
|
|
66
|
+
deduplicatingListener(this.deserializePayload(id, data));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
onError?.(error);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const unsubscribe = await this.subscribeChannel(channel, messageListener, onError);
|
|
72
|
+
try {
|
|
73
|
+
if (this.resumeEnabled && lastEventId !== void 0) {
|
|
74
|
+
for (const { id, data } of await this.readStreamEntries(channel, lastEventId)) {
|
|
75
|
+
resumedIds.add(id);
|
|
76
|
+
listener(this.deserializePayload(id, parseIfText(data)));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const pending = pendingPayloads;
|
|
80
|
+
pendingPayloads = void 0;
|
|
81
|
+
pending.forEach(deduplicatingListener);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
await unsubscribe();
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return once(unsubscribe);
|
|
87
|
+
}
|
|
88
|
+
serializePayload(payload) {
|
|
89
|
+
const [original, meta] = unwrapEvent(payload);
|
|
90
|
+
const { json, meta: jsonMeta } = this.serializer.serialize(original);
|
|
91
|
+
return { payload: { json, meta: jsonMeta }, meta };
|
|
92
|
+
}
|
|
93
|
+
deserializePayload(id, { payload, meta }) {
|
|
94
|
+
return withEventMeta(
|
|
95
|
+
this.serializer.deserialize(payload),
|
|
96
|
+
id === void 0 ? { ...meta } : { ...meta, id }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function parseIfText(value) {
|
|
101
|
+
return typeof value === "string" ? parseEmptyableJSON(value) : value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { BaseRedisPublisher };
|
package/dist/adapters/memory.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SequentialIdGenerator, once, compareSequentialIds } from '@orpc/shared';
|
|
2
|
-
import { unwrapEvent, withEventMeta, getEventMeta } from '@
|
|
2
|
+
import { unwrapEvent, withEventMeta, getEventMeta } from '@standard-server/core';
|
|
3
3
|
import { P as Publisher } from '../shared/publisher.BSQf8ybR.mjs';
|
|
4
4
|
|
|
5
5
|
class MemoryPublisher extends Publisher {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { RedisClientType } from 'redis';
|
|
2
|
-
import {
|
|
3
|
-
import { RPCSerializer } from '@orpc/client';
|
|
2
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamTrimOptions, RedisStreamEntry } from './base-redis.mjs';
|
|
4
3
|
import '@orpc/shared';
|
|
4
|
+
import '../index.mjs';
|
|
5
|
+
import '@orpc/client';
|
|
5
6
|
|
|
6
|
-
interface RedisPublisherOptions extends
|
|
7
|
+
interface RedisPublisherOptions extends BaseRedisPublisherOptions {
|
|
7
8
|
/**
|
|
8
9
|
* Redis subscriber instance.
|
|
9
10
|
* Pub/Sub takes over the connection, so a client with subscriptions
|
|
@@ -12,46 +13,6 @@ interface RedisPublisherOptions extends PublisherOptions {
|
|
|
12
13
|
* @default redis.duplicate()
|
|
13
14
|
*/
|
|
14
15
|
subscriber?: undefined | RedisClientType<any, any, any, any, any>;
|
|
15
|
-
/**
|
|
16
|
-
* The prefix to use for Redis keys.
|
|
17
|
-
*
|
|
18
|
-
* @default ''
|
|
19
|
-
*/
|
|
20
|
-
prefix?: string;
|
|
21
|
-
/**
|
|
22
|
-
* Serializer for serialize and deserialize payloads.
|
|
23
|
-
*
|
|
24
|
-
* @default RPCSerializer
|
|
25
|
-
*/
|
|
26
|
-
serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
|
|
27
|
-
/**
|
|
28
|
-
* Configuration for event resume support.
|
|
29
|
-
*
|
|
30
|
-
* When enabled, published events are temporarily stored so new
|
|
31
|
-
* subscribers can resume from a previous position using `lastEventId`.
|
|
32
|
-
*
|
|
33
|
-
* @default { enabled: false }
|
|
34
|
-
*/
|
|
35
|
-
resume?: {
|
|
36
|
-
/**
|
|
37
|
-
* Whether event resume support is enabled.
|
|
38
|
-
*
|
|
39
|
-
* When enabled, published events are temporarily stored so new
|
|
40
|
-
* subscribers can resume from a previous position using `lastEventId`.
|
|
41
|
-
*
|
|
42
|
-
* @default false
|
|
43
|
-
*/
|
|
44
|
-
enabled: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* How long (in seconds) to retain events for resume.
|
|
47
|
-
*
|
|
48
|
-
* Expired events are cleaned up lazily for performance reasons, so
|
|
49
|
-
* some events may remain available slightly longer than this period.
|
|
50
|
-
*
|
|
51
|
-
* @default 300 (5 min)
|
|
52
|
-
*/
|
|
53
|
-
seconds?: number;
|
|
54
|
-
};
|
|
55
16
|
}
|
|
56
17
|
/**
|
|
57
18
|
* Publisher adapter for Redis. Distributes events across processes via
|
|
@@ -59,24 +20,14 @@ interface RedisPublisherOptions extends PublisherOptions {
|
|
|
59
20
|
*
|
|
60
21
|
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
61
22
|
*/
|
|
62
|
-
declare class RedisPublisher<T extends Record<string, object>> extends
|
|
23
|
+
declare class RedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
|
|
63
24
|
private readonly redis;
|
|
64
25
|
private readonly subscriber;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* The exactness of the `XTRIM` command.
|
|
71
|
-
* Used for testing purpose.
|
|
72
|
-
*/
|
|
73
|
-
private readonly xTrimExactness;
|
|
74
|
-
constructor(redis: RedisClientType<any, any, any, any, any>, options?: RedisPublisherOptions);
|
|
75
|
-
private readonly firstPublishTimeMap;
|
|
76
|
-
publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
|
|
77
|
-
protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
78
|
-
private serializePayload;
|
|
79
|
-
private deserializePayload;
|
|
26
|
+
constructor(redis: RedisClientType<any, any, any, any, any>, { subscriber, ...options }?: RedisPublisherOptions);
|
|
27
|
+
protected publishMessage(channel: string, message: string): Promise<void>;
|
|
28
|
+
protected subscribeChannel(channel: string, listener: (message: unknown) => void): Promise<() => Promise<void>>;
|
|
29
|
+
protected addStreamEntry(key: string, data: string, trim?: RedisStreamTrimOptions): Promise<string>;
|
|
30
|
+
protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
80
31
|
}
|
|
81
32
|
|
|
82
33
|
export { RedisPublisher };
|
package/dist/adapters/redis.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { RedisClientType } from 'redis';
|
|
2
|
-
import {
|
|
3
|
-
import { RPCSerializer } from '@orpc/client';
|
|
2
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamTrimOptions, RedisStreamEntry } from './base-redis.js';
|
|
4
3
|
import '@orpc/shared';
|
|
4
|
+
import '../index.js';
|
|
5
|
+
import '@orpc/client';
|
|
5
6
|
|
|
6
|
-
interface RedisPublisherOptions extends
|
|
7
|
+
interface RedisPublisherOptions extends BaseRedisPublisherOptions {
|
|
7
8
|
/**
|
|
8
9
|
* Redis subscriber instance.
|
|
9
10
|
* Pub/Sub takes over the connection, so a client with subscriptions
|
|
@@ -12,46 +13,6 @@ interface RedisPublisherOptions extends PublisherOptions {
|
|
|
12
13
|
* @default redis.duplicate()
|
|
13
14
|
*/
|
|
14
15
|
subscriber?: undefined | RedisClientType<any, any, any, any, any>;
|
|
15
|
-
/**
|
|
16
|
-
* The prefix to use for Redis keys.
|
|
17
|
-
*
|
|
18
|
-
* @default ''
|
|
19
|
-
*/
|
|
20
|
-
prefix?: string;
|
|
21
|
-
/**
|
|
22
|
-
* Serializer for serialize and deserialize payloads.
|
|
23
|
-
*
|
|
24
|
-
* @default RPCSerializer
|
|
25
|
-
*/
|
|
26
|
-
serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
|
|
27
|
-
/**
|
|
28
|
-
* Configuration for event resume support.
|
|
29
|
-
*
|
|
30
|
-
* When enabled, published events are temporarily stored so new
|
|
31
|
-
* subscribers can resume from a previous position using `lastEventId`.
|
|
32
|
-
*
|
|
33
|
-
* @default { enabled: false }
|
|
34
|
-
*/
|
|
35
|
-
resume?: {
|
|
36
|
-
/**
|
|
37
|
-
* Whether event resume support is enabled.
|
|
38
|
-
*
|
|
39
|
-
* When enabled, published events are temporarily stored so new
|
|
40
|
-
* subscribers can resume from a previous position using `lastEventId`.
|
|
41
|
-
*
|
|
42
|
-
* @default false
|
|
43
|
-
*/
|
|
44
|
-
enabled: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* How long (in seconds) to retain events for resume.
|
|
47
|
-
*
|
|
48
|
-
* Expired events are cleaned up lazily for performance reasons, so
|
|
49
|
-
* some events may remain available slightly longer than this period.
|
|
50
|
-
*
|
|
51
|
-
* @default 300 (5 min)
|
|
52
|
-
*/
|
|
53
|
-
seconds?: number;
|
|
54
|
-
};
|
|
55
16
|
}
|
|
56
17
|
/**
|
|
57
18
|
* Publisher adapter for Redis. Distributes events across processes via
|
|
@@ -59,24 +20,14 @@ interface RedisPublisherOptions extends PublisherOptions {
|
|
|
59
20
|
*
|
|
60
21
|
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
61
22
|
*/
|
|
62
|
-
declare class RedisPublisher<T extends Record<string, object>> extends
|
|
23
|
+
declare class RedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
|
|
63
24
|
private readonly redis;
|
|
64
25
|
private readonly subscriber;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* The exactness of the `XTRIM` command.
|
|
71
|
-
* Used for testing purpose.
|
|
72
|
-
*/
|
|
73
|
-
private readonly xTrimExactness;
|
|
74
|
-
constructor(redis: RedisClientType<any, any, any, any, any>, options?: RedisPublisherOptions);
|
|
75
|
-
private readonly firstPublishTimeMap;
|
|
76
|
-
publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
|
|
77
|
-
protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
78
|
-
private serializePayload;
|
|
79
|
-
private deserializePayload;
|
|
26
|
+
constructor(redis: RedisClientType<any, any, any, any, any>, { subscriber, ...options }?: RedisPublisherOptions);
|
|
27
|
+
protected publishMessage(channel: string, message: string): Promise<void>;
|
|
28
|
+
protected subscribeChannel(channel: string, listener: (message: unknown) => void): Promise<() => Promise<void>>;
|
|
29
|
+
protected addStreamEntry(key: string, data: string, trim?: RedisStreamTrimOptions): Promise<string>;
|
|
30
|
+
protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
80
31
|
}
|
|
81
32
|
|
|
82
33
|
export { RedisPublisher };
|
package/dist/adapters/redis.mjs
CHANGED
|
@@ -1,124 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import { BaseRedisPublisher } from './base-redis.mjs';
|
|
2
|
+
import '@orpc/client';
|
|
3
|
+
import '@orpc/shared';
|
|
4
|
+
import '@standard-server/core';
|
|
5
|
+
import '../shared/publisher.BSQf8ybR.mjs';
|
|
5
6
|
|
|
6
|
-
class RedisPublisher extends
|
|
7
|
-
constructor(redis, options = {}) {
|
|
7
|
+
class RedisPublisher extends BaseRedisPublisher {
|
|
8
|
+
constructor(redis, { subscriber, ...options } = {}) {
|
|
8
9
|
super(options);
|
|
9
10
|
this.redis = redis;
|
|
10
|
-
this.
|
|
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 ?? redis.duplicate();
|
|
11
|
+
this.subscriber = subscriber ?? redis.duplicate();
|
|
15
12
|
}
|
|
16
13
|
subscriber;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.redis.isOpen) {
|
|
32
|
-
await this.redis.connect();
|
|
33
|
-
}
|
|
34
|
-
if (this.resumeEnabled) {
|
|
35
|
-
const now = Date.now();
|
|
36
|
-
for (const [event2, firstPublishTime] of this.firstPublishTimeMap) {
|
|
37
|
-
if (firstPublishTime + this.resumeSeconds * 1e3 < now) {
|
|
38
|
-
this.firstPublishTimeMap.delete(event2);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (!this.firstPublishTimeMap.has(event)) {
|
|
42
|
-
this.firstPublishTimeMap.set(event, now);
|
|
43
|
-
const result = await this.redis.multi().xAdd(redisKey, "*", { data: stringifyJSON(data) }).xTrim(redisKey, "MINID", `${now - this.resumeSeconds * 1e3}-0`, { strategyModifier: this.xTrimExactness }).expire(redisKey, this.resumeSeconds * 2).exec();
|
|
44
|
-
id = result[0];
|
|
45
|
-
} else {
|
|
46
|
-
id = await this.redis.xAdd(redisKey, "*", { data: stringifyJSON(data) });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
await this.redis.publish(redisKey, stringifyJSON({ data, id }));
|
|
14
|
+
async publishMessage(channel, message) {
|
|
15
|
+
await connectIfNeeded(this.redis);
|
|
16
|
+
await this.redis.publish(channel, message);
|
|
50
17
|
}
|
|
51
|
-
async
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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);
|
|
18
|
+
async subscribeChannel(channel, listener) {
|
|
19
|
+
await connectIfNeeded(this.subscriber);
|
|
20
|
+
await this.subscriber.subscribe(channel, listener);
|
|
21
|
+
return async () => {
|
|
22
|
+
await this.subscriber.unsubscribe(channel, listener);
|
|
68
23
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} catch (error) {
|
|
75
|
-
onError?.(error);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
try {
|
|
79
|
-
const subscribePromise = this.subscriber.subscribe(redisKey, redisListener);
|
|
80
|
-
try {
|
|
81
|
-
if (this.resumeEnabled && lastEventId !== void 0) {
|
|
82
|
-
if (!this.redis.isOpen) {
|
|
83
|
-
await this.redis.connect();
|
|
84
|
-
}
|
|
85
|
-
const results = await this.redis.xRead({ key: redisKey, id: lastEventId });
|
|
86
|
-
if (results && results[0]) {
|
|
87
|
-
const { messages } = results[0];
|
|
88
|
-
for (const { id, message } of messages) {
|
|
89
|
-
const rawData = message.data;
|
|
90
|
-
const data = parseEmptyableJSON(rawData);
|
|
91
|
-
const payload = this.deserializePayload(id, data);
|
|
92
|
-
resumedIds.add(id);
|
|
93
|
-
originalListener(payload);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
} finally {
|
|
98
|
-
const pending = pendingPayloads;
|
|
99
|
-
pendingPayloads = void 0;
|
|
100
|
-
for (const payload of pending) {
|
|
101
|
-
deduplicatingListener(payload);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
await subscribePromise;
|
|
105
|
-
} catch (error) {
|
|
106
|
-
await this.subscriber.unsubscribe(redisKey, redisListener);
|
|
107
|
-
throw error;
|
|
24
|
+
}
|
|
25
|
+
async addStreamEntry(key, data, trim) {
|
|
26
|
+
await connectIfNeeded(this.redis);
|
|
27
|
+
if (!trim) {
|
|
28
|
+
return await this.redis.xAdd(key, "*", { data });
|
|
108
29
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
30
|
+
const [id] = await this.redis.multi().xAdd(key, "*", { data }).xTrim(key, "MINID", trim.minId, { strategyModifier: trim.exactness }).expire(key, trim.expireSeconds).exec();
|
|
31
|
+
return id;
|
|
112
32
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
33
|
+
async readStreamEntries(key, lastId) {
|
|
34
|
+
await connectIfNeeded(this.redis);
|
|
35
|
+
const results = await this.redis.xRead({ key, id: lastId });
|
|
36
|
+
const entries = results?.[0]?.messages ?? [];
|
|
37
|
+
return entries.map(({ id, message }) => ({ id, data: message.data }));
|
|
116
38
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
39
|
+
}
|
|
40
|
+
async function connectIfNeeded(client) {
|
|
41
|
+
if (!client.isOpen) {
|
|
42
|
+
await client.connect();
|
|
122
43
|
}
|
|
123
44
|
}
|
|
124
45
|
|
|
@@ -1,49 +1,10 @@
|
|
|
1
|
+
import { ThrowableError } from '@orpc/shared';
|
|
1
2
|
import { Redis } from '@upstash/redis';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import '@orpc/
|
|
3
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamTrimOptions, RedisStreamEntry } from './base-redis.mjs';
|
|
4
|
+
import '../index.mjs';
|
|
5
|
+
import '@orpc/client';
|
|
5
6
|
|
|
6
|
-
interface UpstashPublisherOptions extends
|
|
7
|
-
/**
|
|
8
|
-
* The prefix to use for Redis keys.
|
|
9
|
-
*
|
|
10
|
-
* @default ''
|
|
11
|
-
*/
|
|
12
|
-
prefix?: string;
|
|
13
|
-
/**
|
|
14
|
-
* Serializer for serialize and deserialize payloads.
|
|
15
|
-
*
|
|
16
|
-
* @default RPCSerializer
|
|
17
|
-
*/
|
|
18
|
-
serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
|
|
19
|
-
/**
|
|
20
|
-
* Configuration for event resume support.
|
|
21
|
-
*
|
|
22
|
-
* When enabled, published events are temporarily stored so new
|
|
23
|
-
* subscribers can resume from a previous position using `lastEventId`.
|
|
24
|
-
*
|
|
25
|
-
* @default { enabled: false }
|
|
26
|
-
*/
|
|
27
|
-
resume?: {
|
|
28
|
-
/**
|
|
29
|
-
* Whether event resume support is enabled.
|
|
30
|
-
*
|
|
31
|
-
* When enabled, published events are temporarily stored so new
|
|
32
|
-
* subscribers can resume from a previous position using `lastEventId`.
|
|
33
|
-
*
|
|
34
|
-
* @default false
|
|
35
|
-
*/
|
|
36
|
-
enabled: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* How long (in seconds) to retain events for resume.
|
|
39
|
-
*
|
|
40
|
-
* Expired events are cleaned up lazily for performance reasons, so
|
|
41
|
-
* some events may remain available slightly longer than this period.
|
|
42
|
-
*
|
|
43
|
-
* @default 300 (5 min)
|
|
44
|
-
*/
|
|
45
|
-
seconds?: number;
|
|
46
|
-
};
|
|
7
|
+
interface UpstashPublisherOptions extends BaseRedisPublisherOptions {
|
|
47
8
|
}
|
|
48
9
|
/**
|
|
49
10
|
* Publisher adapter for Upstash Redis. Distributes events across processes via
|
|
@@ -51,29 +12,19 @@ interface UpstashPublisherOptions extends PublisherOptions {
|
|
|
51
12
|
*
|
|
52
13
|
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
53
14
|
*/
|
|
54
|
-
declare class UpstashPublisher<T extends Record<string, object>> extends
|
|
15
|
+
declare class UpstashPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
|
|
55
16
|
private readonly redis;
|
|
56
|
-
private readonly prefix;
|
|
57
|
-
private readonly serializer;
|
|
58
17
|
private readonly listenersMap;
|
|
59
18
|
private readonly onErrorsMap;
|
|
60
19
|
private readonly subscriptionMap;
|
|
61
|
-
private readonly resumeEnabled;
|
|
62
|
-
private readonly resumeSeconds;
|
|
63
|
-
/**
|
|
64
|
-
* The exactness of the `XTRIM` command.
|
|
65
|
-
* Used for testing purpose.
|
|
66
|
-
*/
|
|
67
|
-
private readonly xTrimExactness;
|
|
68
|
-
constructor(redis: Redis, { resume, prefix, serializer, ...options }?: UpstashPublisherOptions);
|
|
69
|
-
private readonly firstPublishTimeMap;
|
|
70
|
-
publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
|
|
71
|
-
protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
72
20
|
private readonly pendingSubscriptionsMap;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
21
|
+
constructor(redis: Redis, options?: UpstashPublisherOptions);
|
|
22
|
+
protected publishMessage(channel: string, message: string): Promise<void>;
|
|
23
|
+
protected subscribeChannel(channel: string, listener: (message: unknown) => void, onError?: (error: ThrowableError) => void): Promise<() => Promise<void>>;
|
|
24
|
+
protected addStreamEntry(key: string, data: string, trim?: RedisStreamTrimOptions): Promise<string>;
|
|
25
|
+
protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
26
|
+
private subscribeIfNeeded;
|
|
27
|
+
private unsubscribeIfUnused;
|
|
77
28
|
}
|
|
78
29
|
|
|
79
30
|
export { UpstashPublisher };
|
|
@@ -1,49 +1,10 @@
|
|
|
1
|
+
import { ThrowableError } from '@orpc/shared';
|
|
1
2
|
import { Redis } from '@upstash/redis';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import '@orpc/
|
|
3
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamTrimOptions, RedisStreamEntry } from './base-redis.js';
|
|
4
|
+
import '../index.js';
|
|
5
|
+
import '@orpc/client';
|
|
5
6
|
|
|
6
|
-
interface UpstashPublisherOptions extends
|
|
7
|
-
/**
|
|
8
|
-
* The prefix to use for Redis keys.
|
|
9
|
-
*
|
|
10
|
-
* @default ''
|
|
11
|
-
*/
|
|
12
|
-
prefix?: string;
|
|
13
|
-
/**
|
|
14
|
-
* Serializer for serialize and deserialize payloads.
|
|
15
|
-
*
|
|
16
|
-
* @default RPCSerializer
|
|
17
|
-
*/
|
|
18
|
-
serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
|
|
19
|
-
/**
|
|
20
|
-
* Configuration for event resume support.
|
|
21
|
-
*
|
|
22
|
-
* When enabled, published events are temporarily stored so new
|
|
23
|
-
* subscribers can resume from a previous position using `lastEventId`.
|
|
24
|
-
*
|
|
25
|
-
* @default { enabled: false }
|
|
26
|
-
*/
|
|
27
|
-
resume?: {
|
|
28
|
-
/**
|
|
29
|
-
* Whether event resume support is enabled.
|
|
30
|
-
*
|
|
31
|
-
* When enabled, published events are temporarily stored so new
|
|
32
|
-
* subscribers can resume from a previous position using `lastEventId`.
|
|
33
|
-
*
|
|
34
|
-
* @default false
|
|
35
|
-
*/
|
|
36
|
-
enabled: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* How long (in seconds) to retain events for resume.
|
|
39
|
-
*
|
|
40
|
-
* Expired events are cleaned up lazily for performance reasons, so
|
|
41
|
-
* some events may remain available slightly longer than this period.
|
|
42
|
-
*
|
|
43
|
-
* @default 300 (5 min)
|
|
44
|
-
*/
|
|
45
|
-
seconds?: number;
|
|
46
|
-
};
|
|
7
|
+
interface UpstashPublisherOptions extends BaseRedisPublisherOptions {
|
|
47
8
|
}
|
|
48
9
|
/**
|
|
49
10
|
* Publisher adapter for Upstash Redis. Distributes events across processes via
|
|
@@ -51,29 +12,19 @@ interface UpstashPublisherOptions extends PublisherOptions {
|
|
|
51
12
|
*
|
|
52
13
|
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
53
14
|
*/
|
|
54
|
-
declare class UpstashPublisher<T extends Record<string, object>> extends
|
|
15
|
+
declare class UpstashPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
|
|
55
16
|
private readonly redis;
|
|
56
|
-
private readonly prefix;
|
|
57
|
-
private readonly serializer;
|
|
58
17
|
private readonly listenersMap;
|
|
59
18
|
private readonly onErrorsMap;
|
|
60
19
|
private readonly subscriptionMap;
|
|
61
|
-
private readonly resumeEnabled;
|
|
62
|
-
private readonly resumeSeconds;
|
|
63
|
-
/**
|
|
64
|
-
* The exactness of the `XTRIM` command.
|
|
65
|
-
* Used for testing purpose.
|
|
66
|
-
*/
|
|
67
|
-
private readonly xTrimExactness;
|
|
68
|
-
constructor(redis: Redis, { resume, prefix, serializer, ...options }?: UpstashPublisherOptions);
|
|
69
|
-
private readonly firstPublishTimeMap;
|
|
70
|
-
publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
|
|
71
|
-
protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
72
20
|
private readonly pendingSubscriptionsMap;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
21
|
+
constructor(redis: Redis, options?: UpstashPublisherOptions);
|
|
22
|
+
protected publishMessage(channel: string, message: string): Promise<void>;
|
|
23
|
+
protected subscribeChannel(channel: string, listener: (message: unknown) => void, onError?: (error: ThrowableError) => void): Promise<() => Promise<void>>;
|
|
24
|
+
protected addStreamEntry(key: string, data: string, trim?: RedisStreamTrimOptions): Promise<string>;
|
|
25
|
+
protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
26
|
+
private subscribeIfNeeded;
|
|
27
|
+
private unsubscribeIfUnused;
|
|
77
28
|
}
|
|
78
29
|
|
|
79
30
|
export { UpstashPublisher };
|
|
@@ -1,199 +1,104 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import { promiseWithResolvers } from '@orpc/shared';
|
|
2
|
+
import { BaseRedisPublisher } from './base-redis.mjs';
|
|
3
|
+
import '@orpc/client';
|
|
4
|
+
import '@standard-server/core';
|
|
5
|
+
import '../shared/publisher.BSQf8ybR.mjs';
|
|
5
6
|
|
|
6
|
-
class UpstashPublisher extends
|
|
7
|
-
constructor(redis,
|
|
7
|
+
class UpstashPublisher extends BaseRedisPublisher {
|
|
8
|
+
constructor(redis, options = {}) {
|
|
8
9
|
super(options);
|
|
9
10
|
this.redis = redis;
|
|
10
|
-
this.prefix = prefix ?? "";
|
|
11
|
-
this.resumeEnabled = resume?.enabled ?? false;
|
|
12
|
-
this.resumeSeconds = resume?.seconds ?? 300;
|
|
13
|
-
this.serializer = serializer ?? new RPCSerializer();
|
|
14
11
|
}
|
|
15
|
-
prefix;
|
|
16
|
-
serializer;
|
|
17
12
|
listenersMap = /* @__PURE__ */ new Map();
|
|
18
13
|
onErrorsMap = /* @__PURE__ */ new Map();
|
|
19
14
|
subscriptionMap = /* @__PURE__ */ new Map();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The exactness of the `XTRIM` command.
|
|
25
|
-
* Used for testing purpose.
|
|
26
|
-
*/
|
|
27
|
-
xTrimExactness = "~";
|
|
28
|
-
firstPublishTimeMap = /* @__PURE__ */ new Map();
|
|
29
|
-
async publish(event, payload) {
|
|
30
|
-
const redisKey = `${this.prefix}${event}`;
|
|
31
|
-
const data = this.serializePayload(payload);
|
|
32
|
-
let id;
|
|
33
|
-
if (this.resumeEnabled) {
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
for (const [event2, firstPublishTime] of this.firstPublishTimeMap) {
|
|
36
|
-
if (firstPublishTime + this.resumeSeconds * 1e3 < now) {
|
|
37
|
-
this.firstPublishTimeMap.delete(event2);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (!this.firstPublishTimeMap.has(event)) {
|
|
41
|
-
this.firstPublishTimeMap.set(event, now);
|
|
42
|
-
const results = await this.redis.multi().xadd(redisKey, "*", { data }).xtrim(redisKey, { strategy: "MINID", exactness: this.xTrimExactness, threshold: `${now - this.resumeSeconds * 1e3}-0` }).expire(redisKey, this.resumeSeconds * 2).exec();
|
|
43
|
-
id = results[0];
|
|
44
|
-
} else {
|
|
45
|
-
id = await this.redis.xadd(redisKey, "*", { data });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
await this.redis.publish(redisKey, { id, data });
|
|
15
|
+
pendingSubscriptionsMap = /* @__PURE__ */ new Map();
|
|
16
|
+
async publishMessage(channel, message) {
|
|
17
|
+
await this.redis.publish(channel, message);
|
|
49
18
|
}
|
|
50
|
-
async
|
|
51
|
-
|
|
52
|
-
let pendingPayloads = [];
|
|
53
|
-
const resumedIds = /* @__PURE__ */ new Set();
|
|
54
|
-
const deduplicatingListener = (payload) => {
|
|
55
|
-
if (pendingPayloads) {
|
|
56
|
-
pendingPayloads.push(payload);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const id = getEventMeta(payload)?.id;
|
|
60
|
-
if (id !== void 0 && resumedIds.has(id)) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
originalListener(payload);
|
|
64
|
-
};
|
|
65
|
-
let listeners = this.listenersMap.get(event);
|
|
66
|
-
if (!listeners) {
|
|
67
|
-
this.listenersMap.set(event, listeners = []);
|
|
68
|
-
}
|
|
69
|
-
listeners.push(deduplicatingListener);
|
|
19
|
+
async subscribeChannel(channel, listener, onError) {
|
|
20
|
+
push(this.listenersMap, channel, listener);
|
|
70
21
|
try {
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
if (this.resumeEnabled && lastEventId !== void 0) {
|
|
74
|
-
const results = await this.redis.xread(redisKey, lastEventId);
|
|
75
|
-
if (results && results[0]) {
|
|
76
|
-
const [_, items] = results[0];
|
|
77
|
-
for (const [id, fields] of items) {
|
|
78
|
-
const data = fields[1];
|
|
79
|
-
const payload = this.deserializePayload(id, data);
|
|
80
|
-
resumedIds.add(id);
|
|
81
|
-
originalListener(payload);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
} finally {
|
|
86
|
-
const pending = pendingPayloads;
|
|
87
|
-
pendingPayloads = void 0;
|
|
88
|
-
for (const payload of pending) {
|
|
89
|
-
deduplicatingListener(payload);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
await subscribeEventPromise;
|
|
22
|
+
await this.subscribeIfNeeded(channel);
|
|
93
23
|
} catch (error) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.listenersMap.delete(event);
|
|
97
|
-
}
|
|
98
|
-
await this.unsubscribeEvent(event);
|
|
24
|
+
remove(this.listenersMap, channel, listener);
|
|
25
|
+
await this.unsubscribeIfUnused(channel);
|
|
99
26
|
throw error;
|
|
100
27
|
}
|
|
101
28
|
if (onError) {
|
|
102
|
-
|
|
103
|
-
if (!onErrors) {
|
|
104
|
-
this.onErrorsMap.set(event, onErrors = []);
|
|
105
|
-
}
|
|
106
|
-
onErrors.push(onError);
|
|
29
|
+
push(this.onErrorsMap, channel, onError);
|
|
107
30
|
}
|
|
108
|
-
return
|
|
109
|
-
|
|
31
|
+
return async () => {
|
|
32
|
+
remove(this.listenersMap, channel, listener);
|
|
110
33
|
if (onError) {
|
|
111
|
-
|
|
112
|
-
if (onErrors) {
|
|
113
|
-
onErrors.splice(onErrors.indexOf(onError), 1);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (listeners.length === 0) {
|
|
117
|
-
this.listenersMap.delete(event);
|
|
118
|
-
this.onErrorsMap.delete(event);
|
|
34
|
+
remove(this.onErrorsMap, channel, onError);
|
|
119
35
|
}
|
|
120
|
-
await this.
|
|
121
|
-
}
|
|
36
|
+
await this.unsubscribeIfUnused(channel);
|
|
37
|
+
};
|
|
122
38
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const pending = this.pendingSubscriptionsMap.get(event);
|
|
127
|
-
if (pending) {
|
|
128
|
-
try {
|
|
129
|
-
await pending;
|
|
130
|
-
} catch {
|
|
131
|
-
}
|
|
39
|
+
async addStreamEntry(key, data, trim) {
|
|
40
|
+
if (!trim) {
|
|
41
|
+
return this.redis.xadd(key, "*", { data });
|
|
132
42
|
}
|
|
133
|
-
|
|
43
|
+
const [id] = await this.redis.multi().xadd(key, "*", { data }).xtrim(key, { strategy: "MINID", exactness: trim.exactness, threshold: trim.minId }).expire(key, trim.expireSeconds).exec();
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
async readStreamEntries(key, lastId) {
|
|
47
|
+
const results = await this.redis.xread(key, lastId);
|
|
48
|
+
return results?.[0]?.[1].map(([id, fields]) => ({ id, data: fields[1] })) ?? [];
|
|
49
|
+
}
|
|
50
|
+
async subscribeIfNeeded(channel) {
|
|
51
|
+
while (this.pendingSubscriptionsMap.has(channel)) {
|
|
52
|
+
await this.pendingSubscriptionsMap.get(channel).catch(() => {
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (this.subscriptionMap.has(channel)) {
|
|
134
56
|
return;
|
|
135
57
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
};
|
|
140
|
-
const subscription = this.redis.subscribe(redisKey);
|
|
141
|
-
subscription.on("message", (message) => {
|
|
142
|
-
try {
|
|
143
|
-
const listeners = this.listenersMap.get(event);
|
|
144
|
-
if (listeners) {
|
|
145
|
-
const { id, data } = message.message;
|
|
146
|
-
const payload = this.deserializePayload(id, data);
|
|
147
|
-
listeners.forEach((listener) => listener(payload));
|
|
148
|
-
}
|
|
149
|
-
} catch (error) {
|
|
150
|
-
dispatchErrorForEvent(error);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
let resolvePromise;
|
|
154
|
-
let rejectPromise;
|
|
155
|
-
const promise = new Promise((resolve, reject) => {
|
|
156
|
-
resolvePromise = resolve;
|
|
157
|
-
rejectPromise = reject;
|
|
158
|
-
});
|
|
58
|
+
const { promise, resolve, reject } = promiseWithResolvers();
|
|
59
|
+
const subscription = this.redis.subscribe(channel);
|
|
60
|
+
subscription.on("subscribe", () => resolve());
|
|
159
61
|
subscription.on("error", (error) => {
|
|
160
|
-
|
|
161
|
-
|
|
62
|
+
reject(error);
|
|
63
|
+
this.onErrorsMap.get(channel)?.slice().forEach((onError) => onError(error));
|
|
162
64
|
});
|
|
163
|
-
subscription.on("
|
|
164
|
-
|
|
65
|
+
subscription.on("message", ({ message }) => {
|
|
66
|
+
this.listenersMap.get(channel)?.slice().forEach((listener) => listener(message));
|
|
165
67
|
});
|
|
68
|
+
this.pendingSubscriptionsMap.set(channel, promise);
|
|
166
69
|
try {
|
|
167
|
-
this.pendingSubscriptionsMap.set(event, promise);
|
|
168
70
|
await promise;
|
|
169
|
-
this.subscriptionMap.set(
|
|
71
|
+
this.subscriptionMap.set(channel, subscription);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
await subscription.unsubscribe();
|
|
74
|
+
throw error;
|
|
170
75
|
} finally {
|
|
171
|
-
this.pendingSubscriptionsMap.delete(
|
|
76
|
+
this.pendingSubscriptionsMap.delete(channel);
|
|
172
77
|
}
|
|
173
78
|
}
|
|
174
|
-
async
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
await pending;
|
|
179
|
-
} catch {
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const subscription = this.subscriptionMap.get(event);
|
|
183
|
-
if (!this.listenersMap.has(event) && subscription) {
|
|
184
|
-
this.subscriptionMap.delete(event);
|
|
79
|
+
async unsubscribeIfUnused(channel) {
|
|
80
|
+
const subscription = this.subscriptionMap.get(channel);
|
|
81
|
+
if (subscription && !this.listenersMap.has(channel)) {
|
|
82
|
+
this.subscriptionMap.delete(channel);
|
|
185
83
|
await subscription.unsubscribe();
|
|
186
84
|
}
|
|
187
85
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
86
|
+
}
|
|
87
|
+
function push(map, key, value) {
|
|
88
|
+
let values = map.get(key);
|
|
89
|
+
if (!values) {
|
|
90
|
+
map.set(key, values = []);
|
|
91
|
+
}
|
|
92
|
+
values.push(value);
|
|
93
|
+
}
|
|
94
|
+
function remove(map, key, value) {
|
|
95
|
+
const values = map.get(key);
|
|
96
|
+
const index = values?.indexOf(value) ?? -1;
|
|
97
|
+
if (index !== -1) {
|
|
98
|
+
values.splice(index, 1);
|
|
191
99
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.serializer.deserialize(payload),
|
|
195
|
-
id === void 0 ? { ...meta } : { ...meta, id }
|
|
196
|
-
);
|
|
100
|
+
if (values?.length === 0) {
|
|
101
|
+
map.delete(key);
|
|
197
102
|
}
|
|
198
103
|
}
|
|
199
104
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orpc/publisher",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.35",
|
|
5
5
|
"description": "Typesafe pub/sub for oRPC event streaming, with memory, Redis, and Upstash adapters",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": [
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"import": "./dist/index.mjs",
|
|
34
34
|
"default": "./dist/index.mjs"
|
|
35
35
|
},
|
|
36
|
+
"./base-redis": {
|
|
37
|
+
"types": "./dist/adapters/base-redis.d.mts",
|
|
38
|
+
"import": "./dist/adapters/base-redis.mjs",
|
|
39
|
+
"default": "./dist/adapters/base-redis.mjs"
|
|
40
|
+
},
|
|
36
41
|
"./memory": {
|
|
37
42
|
"types": "./dist/adapters/memory.d.mts",
|
|
38
43
|
"import": "./dist/adapters/memory.mjs",
|
|
@@ -65,9 +70,9 @@
|
|
|
65
70
|
}
|
|
66
71
|
},
|
|
67
72
|
"dependencies": {
|
|
68
|
-
"@orpc/client": "2.0.0-beta.
|
|
69
|
-
"@orpc/shared": "2.0.0-beta.
|
|
70
|
-
"@
|
|
73
|
+
"@orpc/client": "2.0.0-beta.35",
|
|
74
|
+
"@orpc/shared": "2.0.0-beta.35",
|
|
75
|
+
"@standard-server/core": "~0.9.0"
|
|
71
76
|
},
|
|
72
77
|
"devDependencies": {
|
|
73
78
|
"@upstash/redis": "^1.38.2",
|