@orpc/publisher 2.0.0-beta.4 → 2.0.0-beta.40

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.
@@ -1,124 +1,45 @@
1
- import { RPCSerializer } from '@orpc/client';
2
- import { stringifyJSON, parseEmptyableJSON, once } from '@orpc/shared';
3
- import { unwrapEvent, withEventMeta, getEventMeta } from '@standardserver/core';
4
- import { P as Publisher } from '../shared/publisher.BSQf8ybR.mjs';
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 Publisher {
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.prefix = options.prefix ?? "";
11
- this.serializer = options.serializer ?? new RPCSerializer();
12
- this.replayEnabled = options.replay?.enabled ?? false;
13
- this.replaySeconds = options.replay?.seconds ?? 300;
14
- this.subscriber = options.subscriber ?? redis.duplicate();
11
+ this.subscriber = subscriber ?? redis.duplicate();
15
12
  }
16
13
  subscriber;
17
- prefix;
18
- serializer;
19
- replayEnabled;
20
- replaySeconds;
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.replayEnabled) {
35
- const now = Date.now();
36
- for (const [event2, firstPublishTime] of this.firstPublishTimeMap) {
37
- if (firstPublishTime + this.replaySeconds * 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.replaySeconds * 1e3}-0`, { strategyModifier: this.xTrimExactness }).expire(redisKey, this.replaySeconds * 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 subscribeListener(event, originalListener, { lastEventId, onError } = {}) {
52
- const redisKey = `${this.prefix}${event}`;
53
- if (!this.subscriber.isOpen) {
54
- await this.subscriber.connect();
55
- }
56
- let pendingPayloads = [];
57
- const replayedIds = /* @__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 && replayedIds.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
- 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
- try {
79
- const subscribePromise = this.subscriber.subscribe(redisKey, redisListener);
80
- try {
81
- if (this.replayEnabled && 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
- replayedIds.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
- return once(async () => {
110
- await this.subscriber.unsubscribe(redisKey, redisListener);
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
- serializePayload(payload) {
114
- const [original, meta] = unwrapEvent(payload);
115
- return { payload: this.serializer.serialize(original, { useFormDataForBlobFields: false }), meta };
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
- deserializePayload(id, { payload, meta }) {
118
- return withEventMeta(
119
- this.serializer.deserialize(payload),
120
- id === void 0 ? { ...meta } : { ...meta, id }
121
- );
39
+ }
40
+ async function connectIfNeeded(client) {
41
+ if (!client.isOpen) {
42
+ await client.connect();
122
43
  }
123
44
  }
124
45
 
@@ -1,71 +1,30 @@
1
+ import { ThrowableError } from '@orpc/shared';
1
2
  import { Redis } from '@upstash/redis';
2
- import { Publisher, PublisherOptions, PublisherSubscribeListenerOptions } from '../index.mjs';
3
- import { RPCSerializer } from '@orpc/client';
4
- import '@orpc/shared';
3
+ import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamTrimOptions, RedisStreamEntry } from './base-redis.mjs';
4
+ import '../index.mjs';
5
+ import '@orpc/client';
5
6
 
6
- interface UpstashPublisherOptions extends PublisherOptions {
7
- /**
8
- * The prefix to use for Redis keys.
9
- */
10
- prefix?: string;
11
- /**
12
- * Serializer for serialize and deserialize payloads.
13
- *
14
- * @default RPCSerializer
15
- */
16
- serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
17
- /**
18
- * Configuration for event replay support.
19
- *
20
- * When enabled, published events are temporarily stored so new
21
- * subscribers can resume from a previous position using `lastEventId`.
22
- *
23
- * @default { enabled: false }
24
- */
25
- replay?: {
26
- /**
27
- * Whether event replay support is enabled.
28
- *
29
- * When enabled, published events are temporarily stored so new
30
- * subscribers can resume from a previous position using `lastEventId`.
31
- *
32
- * @default false
33
- */
34
- enabled: boolean;
35
- /**
36
- * How long (in seconds) to retain events for replay.
37
- *
38
- * Expired events are cleaned up lazily for performance reasons, so
39
- * some events may remain available slightly longer than this period.
40
- *
41
- * @default 300 (5 min)
42
- */
43
- seconds?: number;
44
- };
7
+ interface UpstashPublisherOptions extends BaseRedisPublisherOptions {
45
8
  }
46
- declare class UpstashPublisher<T extends Record<string, object>> extends Publisher<T> {
9
+ /**
10
+ * Publisher adapter for Upstash Redis. Distributes events across processes via
11
+ * Upstash Pub/Sub, with optional resume support backed by Redis Streams.
12
+ *
13
+ * @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
14
+ */
15
+ declare class UpstashPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
47
16
  private readonly redis;
48
- private readonly prefix;
49
- private readonly serializer;
50
17
  private readonly listenersMap;
51
18
  private readonly onErrorsMap;
52
19
  private readonly subscriptionMap;
53
- private readonly replayEnabled;
54
- private readonly replaySeconds;
55
- /**
56
- * The exactness of the `XTRIM` command.
57
- * Used for testing purpose.
58
- */
59
- private readonly xTrimExactness;
60
- constructor(redis: Redis, { replay, prefix, serializer, ...options }?: UpstashPublisherOptions);
61
- private readonly firstPublishTimeMap;
62
- publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
63
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
64
20
  private readonly pendingSubscriptionsMap;
65
- private subscribeEvent;
66
- private unsubscribeEvent;
67
- private serializePayload;
68
- private deserializePayload;
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;
69
28
  }
70
29
 
71
30
  export { UpstashPublisher };
@@ -1,71 +1,30 @@
1
+ import { ThrowableError } from '@orpc/shared';
1
2
  import { Redis } from '@upstash/redis';
2
- import { Publisher, PublisherOptions, PublisherSubscribeListenerOptions } from '../index.js';
3
- import { RPCSerializer } from '@orpc/client';
4
- import '@orpc/shared';
3
+ import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamTrimOptions, RedisStreamEntry } from './base-redis.js';
4
+ import '../index.js';
5
+ import '@orpc/client';
5
6
 
6
- interface UpstashPublisherOptions extends PublisherOptions {
7
- /**
8
- * The prefix to use for Redis keys.
9
- */
10
- prefix?: string;
11
- /**
12
- * Serializer for serialize and deserialize payloads.
13
- *
14
- * @default RPCSerializer
15
- */
16
- serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
17
- /**
18
- * Configuration for event replay support.
19
- *
20
- * When enabled, published events are temporarily stored so new
21
- * subscribers can resume from a previous position using `lastEventId`.
22
- *
23
- * @default { enabled: false }
24
- */
25
- replay?: {
26
- /**
27
- * Whether event replay support is enabled.
28
- *
29
- * When enabled, published events are temporarily stored so new
30
- * subscribers can resume from a previous position using `lastEventId`.
31
- *
32
- * @default false
33
- */
34
- enabled: boolean;
35
- /**
36
- * How long (in seconds) to retain events for replay.
37
- *
38
- * Expired events are cleaned up lazily for performance reasons, so
39
- * some events may remain available slightly longer than this period.
40
- *
41
- * @default 300 (5 min)
42
- */
43
- seconds?: number;
44
- };
7
+ interface UpstashPublisherOptions extends BaseRedisPublisherOptions {
45
8
  }
46
- declare class UpstashPublisher<T extends Record<string, object>> extends Publisher<T> {
9
+ /**
10
+ * Publisher adapter for Upstash Redis. Distributes events across processes via
11
+ * Upstash Pub/Sub, with optional resume support backed by Redis Streams.
12
+ *
13
+ * @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
14
+ */
15
+ declare class UpstashPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
47
16
  private readonly redis;
48
- private readonly prefix;
49
- private readonly serializer;
50
17
  private readonly listenersMap;
51
18
  private readonly onErrorsMap;
52
19
  private readonly subscriptionMap;
53
- private readonly replayEnabled;
54
- private readonly replaySeconds;
55
- /**
56
- * The exactness of the `XTRIM` command.
57
- * Used for testing purpose.
58
- */
59
- private readonly xTrimExactness;
60
- constructor(redis: Redis, { replay, prefix, serializer, ...options }?: UpstashPublisherOptions);
61
- private readonly firstPublishTimeMap;
62
- publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
63
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
64
20
  private readonly pendingSubscriptionsMap;
65
- private subscribeEvent;
66
- private unsubscribeEvent;
67
- private serializePayload;
68
- private deserializePayload;
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;
69
28
  }
70
29
 
71
30
  export { UpstashPublisher };
@@ -1,199 +1,104 @@
1
- import { RPCSerializer } from '@orpc/client';
2
- import { once } from '@orpc/shared';
3
- import { unwrapEvent, withEventMeta, getEventMeta } from '@standardserver/core';
4
- import { P as Publisher } from '../shared/publisher.BSQf8ybR.mjs';
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 Publisher {
7
- constructor(redis, { replay, prefix, serializer, ...options } = {}) {
7
+ class UpstashPublisher extends BaseRedisPublisher {
8
+ constructor(redis, options = {}) {
8
9
  super(options);
9
10
  this.redis = redis;
10
- this.prefix = prefix ?? "";
11
- this.replayEnabled = replay?.enabled ?? false;
12
- this.replaySeconds = replay?.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
- // Upstash subscription objects
21
- replayEnabled;
22
- replaySeconds;
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.replayEnabled) {
34
- const now = Date.now();
35
- for (const [event2, firstPublishTime] of this.firstPublishTimeMap) {
36
- if (firstPublishTime + this.replaySeconds * 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.replaySeconds * 1e3}-0` }).expire(redisKey, this.replaySeconds * 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 subscribeListener(event, originalListener, { lastEventId, onError } = {}) {
51
- const redisKey = `${this.prefix}${event}`;
52
- let pendingPayloads = [];
53
- const replayedIds = /* @__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 && replayedIds.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
- const subscribeEventPromise = this.subscribeEvent(event);
72
- try {
73
- if (this.replayEnabled && 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
- replayedIds.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
- listeners.splice(listeners.indexOf(deduplicatingListener), 1);
95
- if (listeners.length === 0) {
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
- let onErrors = this.onErrorsMap.get(event);
103
- if (!onErrors) {
104
- this.onErrorsMap.set(event, onErrors = []);
105
- }
106
- onErrors.push(onError);
29
+ push(this.onErrorsMap, channel, onError);
107
30
  }
108
- return once(async () => {
109
- listeners.splice(listeners.indexOf(deduplicatingListener), 1);
31
+ return async () => {
32
+ remove(this.listenersMap, channel, listener);
110
33
  if (onError) {
111
- const onErrors = this.onErrorsMap.get(event);
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.unsubscribeEvent(event);
121
- });
36
+ await this.unsubscribeIfUnused(channel);
37
+ };
122
38
  }
123
- pendingSubscriptionsMap = /* @__PURE__ */ new Map();
124
- async subscribeEvent(event) {
125
- const redisKey = `${this.prefix}${event}`;
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
- if (this.subscriptionMap.has(event)) {
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 dispatchErrorForEvent = (error) => {
137
- const onErrors = this.onErrorsMap.get(event);
138
- onErrors?.forEach((onError) => onError(error));
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
- rejectPromise(error);
161
- dispatchErrorForEvent(error);
62
+ reject(error);
63
+ this.onErrorsMap.get(channel)?.slice().forEach((onError) => onError(error));
162
64
  });
163
- subscription.on("subscribe", () => {
164
- resolvePromise();
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(event, subscription);
71
+ this.subscriptionMap.set(channel, subscription);
72
+ } catch (error) {
73
+ await subscription.unsubscribe();
74
+ throw error;
170
75
  } finally {
171
- this.pendingSubscriptionsMap.delete(event);
76
+ this.pendingSubscriptionsMap.delete(channel);
172
77
  }
173
78
  }
174
- async unsubscribeEvent(event) {
175
- const pending = this.pendingSubscriptionsMap.get(event);
176
- if (pending) {
177
- try {
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
- serializePayload(payload) {
189
- const [original, meta] = unwrapEvent(payload);
190
- return { payload: this.serializer.serialize(original, { useFormDataForBlobFields: false }), meta };
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
- deserializePayload(id, { payload, meta }) {
193
- return withEventMeta(
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/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@ import { ThrowableError, AsyncIteratorClass } from '@orpc/shared';
2
2
 
3
3
  interface PublisherOptions {
4
4
  /**
5
- * Maximum number of events to buffer for async iterator subscribers.
5
+ * Maximum number of events to buffer for AsyncIteratorObject subscribers.
6
6
  *
7
7
  * If the buffer exceeds this limit, the oldest event is dropped.
8
8
  * This prevents unbounded memory growth if consumers process events slowly.
@@ -68,7 +68,7 @@ declare abstract class Publisher<T extends Record<string, object>> {
68
68
  */
69
69
  subscribe<K extends keyof T & string>(event: K, listener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
70
70
  /**
71
- * Subscribes to a specific event using an async iterator.
71
+ * Subscribes to a specific event using an AsyncIteratorObject.
72
72
  * Useful for `for await...of` loops with optional buffering and abort support.
73
73
  *
74
74
  * @example
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { ThrowableError, AsyncIteratorClass } from '@orpc/shared';
2
2
 
3
3
  interface PublisherOptions {
4
4
  /**
5
- * Maximum number of events to buffer for async iterator subscribers.
5
+ * Maximum number of events to buffer for AsyncIteratorObject subscribers.
6
6
  *
7
7
  * If the buffer exceeds this limit, the oldest event is dropped.
8
8
  * This prevents unbounded memory growth if consumers process events slowly.
@@ -68,7 +68,7 @@ declare abstract class Publisher<T extends Record<string, object>> {
68
68
  */
69
69
  subscribe<K extends keyof T & string>(event: K, listener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
70
70
  /**
71
- * Subscribes to a specific event using an async iterator.
71
+ * Subscribes to a specific event using an AsyncIteratorObject.
72
72
  * Useful for `for await...of` loops with optional buffering and abort support.
73
73
  *
74
74
  * @example