@orpc/experimental-publisher 0.0.0 → 0.0.1

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,8 +1,8 @@
1
1
  import { StandardRPCJsonSerializerOptions, StandardRPCJsonSerializer, StandardRPCJsonSerializedMetaItem } from '@orpc/client/standard';
2
+ import { ThrowableError } from '@orpc/shared';
2
3
  import Redis from 'ioredis';
3
4
  import { PublisherOptions, Publisher, PublisherSubscribeListenerOptions } from '../index.mjs';
4
5
  import { getEventMeta } from '@orpc/standard-server';
5
- import '@orpc/shared';
6
6
 
7
7
  type SerializedPayload = {
8
8
  json: object;
@@ -46,9 +46,13 @@ declare class IORedisPublisher<T extends Record<string, object>> extends Publish
46
46
  protected readonly prefix: string;
47
47
  protected readonly serializer: StandardRPCJsonSerializer;
48
48
  protected readonly retentionSeconds: number;
49
- protected readonly listenerPromiseMap: Map<string, Promise<any>>;
50
- protected readonly listenersMap: Map<string, Set<(payload: any) => void>>;
51
- protected redisListener: ((channel: string, message: string) => void) | undefined;
49
+ protected readonly subscriptionPromiseMap: Map<string, Promise<any>>;
50
+ protected readonly listenersMap: Map<string, ((payload: any) => void)[]>;
51
+ protected readonly onErrorsMap: Map<string, ((error: ThrowableError) => void)[]>;
52
+ protected redisListenerAndOnError: undefined | {
53
+ listener: (channel: string, message: string) => void;
54
+ onError: (error: ThrowableError) => void;
55
+ };
52
56
  protected get isResumeEnabled(): boolean;
53
57
  /**
54
58
  * The exactness of the `XTRIM` command.
@@ -64,9 +68,9 @@ declare class IORedisPublisher<T extends Record<string, object>> extends Publish
64
68
  */
65
69
  get size(): number;
66
70
  constructor({ commander, listener, resumeRetentionSeconds, prefix, ...options }: IORedisPublisherOptions);
67
- protected lastCleanupTimes: Map<string, number>;
71
+ protected lastCleanupTimeMap: Map<string, number>;
68
72
  publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
69
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
73
+ protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
70
74
  protected prefixKey(key: string): string;
71
75
  protected serializePayload(payload: object): SerializedPayload;
72
76
  protected deserializePayload(id: string | undefined, { json, meta, eventMeta }: SerializedPayload): any;
@@ -1,8 +1,8 @@
1
1
  import { StandardRPCJsonSerializerOptions, StandardRPCJsonSerializer, StandardRPCJsonSerializedMetaItem } from '@orpc/client/standard';
2
+ import { ThrowableError } from '@orpc/shared';
2
3
  import Redis from 'ioredis';
3
4
  import { PublisherOptions, Publisher, PublisherSubscribeListenerOptions } from '../index.js';
4
5
  import { getEventMeta } from '@orpc/standard-server';
5
- import '@orpc/shared';
6
6
 
7
7
  type SerializedPayload = {
8
8
  json: object;
@@ -46,9 +46,13 @@ declare class IORedisPublisher<T extends Record<string, object>> extends Publish
46
46
  protected readonly prefix: string;
47
47
  protected readonly serializer: StandardRPCJsonSerializer;
48
48
  protected readonly retentionSeconds: number;
49
- protected readonly listenerPromiseMap: Map<string, Promise<any>>;
50
- protected readonly listenersMap: Map<string, Set<(payload: any) => void>>;
51
- protected redisListener: ((channel: string, message: string) => void) | undefined;
49
+ protected readonly subscriptionPromiseMap: Map<string, Promise<any>>;
50
+ protected readonly listenersMap: Map<string, ((payload: any) => void)[]>;
51
+ protected readonly onErrorsMap: Map<string, ((error: ThrowableError) => void)[]>;
52
+ protected redisListenerAndOnError: undefined | {
53
+ listener: (channel: string, message: string) => void;
54
+ onError: (error: ThrowableError) => void;
55
+ };
52
56
  protected get isResumeEnabled(): boolean;
53
57
  /**
54
58
  * The exactness of the `XTRIM` command.
@@ -64,9 +68,9 @@ declare class IORedisPublisher<T extends Record<string, object>> extends Publish
64
68
  */
65
69
  get size(): number;
66
70
  constructor({ commander, listener, resumeRetentionSeconds, prefix, ...options }: IORedisPublisherOptions);
67
- protected lastCleanupTimes: Map<string, number>;
71
+ protected lastCleanupTimeMap: Map<string, number>;
68
72
  publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
69
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
73
+ protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
70
74
  protected prefixKey(key: string): string;
71
75
  protected serializePayload(payload: object): SerializedPayload;
72
76
  protected deserializePayload(id: string | undefined, { json, meta, eventMeta }: SerializedPayload): any;
@@ -1,7 +1,7 @@
1
1
  import { StandardRPCJsonSerializer } from '@orpc/client/standard';
2
- import { stringifyJSON } from '@orpc/shared';
2
+ import { fallback, stringifyJSON, once } from '@orpc/shared';
3
3
  import { getEventMeta, withEventMeta } from '@orpc/standard-server';
4
- import { P as Publisher } from '../shared/experimental-publisher.BOyYBkgn.mjs';
4
+ import { P as Publisher } from '../shared/experimental-publisher.BtlOkhPO.mjs';
5
5
 
6
6
  class IORedisPublisher extends Publisher {
7
7
  commander;
@@ -9,9 +9,10 @@ class IORedisPublisher extends Publisher {
9
9
  prefix;
10
10
  serializer;
11
11
  retentionSeconds;
12
- listenerPromiseMap = /* @__PURE__ */ new Map();
12
+ subscriptionPromiseMap = /* @__PURE__ */ new Map();
13
13
  listenersMap = /* @__PURE__ */ new Map();
14
- redisListener;
14
+ onErrorsMap = /* @__PURE__ */ new Map();
15
+ redisListenerAndOnError;
15
16
  get isResumeEnabled() {
16
17
  return Number.isFinite(this.retentionSeconds) && this.retentionSeconds > 0;
17
18
  }
@@ -28,9 +29,12 @@ class IORedisPublisher extends Publisher {
28
29
  *
29
30
  */
30
31
  get size() {
31
- let size = this.redisListener ? 1 : 0;
32
+ let size = this.redisListenerAndOnError ? 1 : 0;
32
33
  for (const listeners of this.listenersMap) {
33
- size += listeners[1].size || 1;
34
+ size += listeners[1].length || 1;
35
+ }
36
+ for (const onErrors of this.onErrorsMap) {
37
+ size += onErrors[1].length || 1;
34
38
  }
35
39
  return size;
36
40
  }
@@ -38,24 +42,24 @@ class IORedisPublisher extends Publisher {
38
42
  super(options);
39
43
  this.commander = commander;
40
44
  this.listener = listener;
41
- this.prefix = prefix ?? "orpc:publisher:";
45
+ this.prefix = fallback(prefix, "orpc:publisher:");
42
46
  this.retentionSeconds = resumeRetentionSeconds ?? Number.NaN;
43
47
  this.serializer = new StandardRPCJsonSerializer(options);
44
48
  }
45
- lastCleanupTimes = /* @__PURE__ */ new Map();
49
+ lastCleanupTimeMap = /* @__PURE__ */ new Map();
46
50
  async publish(event, payload) {
47
51
  const key = this.prefixKey(event);
48
52
  const serialized = this.serializePayload(payload);
49
53
  let id;
50
54
  if (this.isResumeEnabled) {
51
55
  const now = Date.now();
52
- for (const [key2, lastCleanupTime] of this.lastCleanupTimes) {
56
+ for (const [mapKey, lastCleanupTime] of this.lastCleanupTimeMap) {
53
57
  if (lastCleanupTime + this.retentionSeconds * 1e3 < now) {
54
- this.lastCleanupTimes.delete(key2);
58
+ this.lastCleanupTimeMap.delete(mapKey);
55
59
  }
56
60
  }
57
- if (!this.lastCleanupTimes.has(key)) {
58
- this.lastCleanupTimes.set(key, now);
61
+ if (!this.lastCleanupTimeMap.has(key)) {
62
+ this.lastCleanupTimeMap.set(key, now);
59
63
  const result = await this.commander.multi().xadd(key, "*", "data", stringifyJSON(serialized)).xtrim(key, "MINID", this.xtrimExactness, `${now - this.retentionSeconds * 1e3}-0`).expire(key, this.retentionSeconds * 2).exec();
60
64
  if (result) {
61
65
  for (const [error] of result) {
@@ -72,9 +76,8 @@ class IORedisPublisher extends Publisher {
72
76
  }
73
77
  await this.commander.publish(key, stringifyJSON({ ...serialized, id }));
74
78
  }
75
- async subscribeListener(event, originalListener, options) {
79
+ async subscribeListener(event, originalListener, { lastEventId, onError } = {}) {
76
80
  const key = this.prefixKey(event);
77
- const lastEventId = options?.lastEventId;
78
81
  let pendingPayloads = [];
79
82
  const resumePayloadIds = /* @__PURE__ */ new Set();
80
83
  const listener = (payload) => {
@@ -88,8 +91,15 @@ class IORedisPublisher extends Publisher {
88
91
  }
89
92
  originalListener(payload);
90
93
  };
91
- if (!this.redisListener) {
92
- this.redisListener = (channel, message) => {
94
+ if (!this.redisListenerAndOnError) {
95
+ const redisOnError = (error) => {
96
+ for (const [_, onErrors] of this.onErrorsMap) {
97
+ for (const onError2 of onErrors) {
98
+ onError2(error);
99
+ }
100
+ }
101
+ };
102
+ const redisListener = (channel, message) => {
93
103
  try {
94
104
  const listeners2 = this.listenersMap.get(channel);
95
105
  if (listeners2) {
@@ -99,24 +109,47 @@ class IORedisPublisher extends Publisher {
99
109
  listener2(payload);
100
110
  }
101
111
  }
102
- } catch {
112
+ } catch (error) {
113
+ const onErrors = this.onErrorsMap.get(channel);
114
+ if (onErrors) {
115
+ for (const onError2 of onErrors) {
116
+ onError2(error);
117
+ }
118
+ }
103
119
  }
104
120
  };
105
- this.listener.on("message", this.redisListener);
121
+ this.redisListenerAndOnError = { listener: redisListener, onError: redisOnError };
122
+ this.listener.on("message", redisListener);
123
+ this.listener.on("error", redisOnError);
124
+ }
125
+ const subscriptionPromise = this.subscriptionPromiseMap.get(key);
126
+ if (subscriptionPromise) {
127
+ await subscriptionPromise;
106
128
  }
107
- await this.listenerPromiseMap.get(key);
108
129
  let listeners = this.listenersMap.get(key);
109
130
  if (!listeners) {
110
131
  try {
111
132
  const promise = this.listener.subscribe(key);
112
- this.listenerPromiseMap.set(key, promise);
133
+ this.subscriptionPromiseMap.set(key, promise);
113
134
  await promise;
114
- this.listenersMap.set(key, listeners = /* @__PURE__ */ new Set());
135
+ this.listenersMap.set(key, listeners = []);
115
136
  } finally {
116
- this.listenerPromiseMap.delete(key);
137
+ this.subscriptionPromiseMap.delete(key);
138
+ if (this.listenersMap.size === 0) {
139
+ this.listener.off("message", this.redisListenerAndOnError.listener);
140
+ this.listener.off("error", this.redisListenerAndOnError.onError);
141
+ this.redisListenerAndOnError = void 0;
142
+ }
143
+ }
144
+ }
145
+ listeners.push(listener);
146
+ if (onError) {
147
+ let onErrors = this.onErrorsMap.get(key);
148
+ if (!onErrors) {
149
+ this.onErrorsMap.set(key, onErrors = []);
117
150
  }
151
+ onErrors.push(onError);
118
152
  }
119
- listeners.add(listener);
120
153
  void (async () => {
121
154
  try {
122
155
  if (this.isResumeEnabled && typeof lastEventId === "string") {
@@ -131,7 +164,8 @@ class IORedisPublisher extends Publisher {
131
164
  }
132
165
  }
133
166
  }
134
- } catch {
167
+ } catch (error) {
168
+ onError?.(error);
135
169
  } finally {
136
170
  const pending = pendingPayloads;
137
171
  pendingPayloads = void 0;
@@ -140,13 +174,27 @@ class IORedisPublisher extends Publisher {
140
174
  }
141
175
  }
142
176
  })();
177
+ const cleanupListeners = once(() => {
178
+ listeners.splice(listeners.indexOf(listener), 1);
179
+ if (onError) {
180
+ const onErrors = this.onErrorsMap.get(key);
181
+ if (onErrors) {
182
+ const index = onErrors.indexOf(onError);
183
+ if (index !== -1) {
184
+ onErrors.splice(index, 1);
185
+ }
186
+ }
187
+ }
188
+ });
143
189
  return async () => {
144
- listeners.delete(listener);
145
- if (listeners.size === 0) {
190
+ cleanupListeners();
191
+ if (listeners.length === 0) {
146
192
  this.listenersMap.delete(key);
147
- if (this.redisListener && this.listenersMap.size === 0) {
148
- this.listener.off("message", this.redisListener);
149
- this.redisListener = void 0;
193
+ this.onErrorsMap.delete(key);
194
+ if (this.redisListenerAndOnError && this.listenersMap.size === 0) {
195
+ this.listener.off("message", this.redisListenerAndOnError.listener);
196
+ this.listener.off("error", this.redisListenerAndOnError.onError);
197
+ this.redisListenerAndOnError = void 0;
150
198
  }
151
199
  await this.listener.unsubscribe(key);
152
200
  }
@@ -19,7 +19,7 @@ declare class MemoryPublisher<T extends Record<string, object>> extends Publishe
19
19
  private readonly eventPublisher;
20
20
  private readonly idGenerator;
21
21
  private readonly retentionSeconds;
22
- private readonly events;
22
+ private readonly eventsMap;
23
23
  /**
24
24
  * Useful for measuring memory usage.
25
25
  *
@@ -19,7 +19,7 @@ declare class MemoryPublisher<T extends Record<string, object>> extends Publishe
19
19
  private readonly eventPublisher;
20
20
  private readonly idGenerator;
21
21
  private readonly retentionSeconds;
22
- private readonly events;
22
+ private readonly eventsMap;
23
23
  /**
24
24
  * Useful for measuring memory usage.
25
25
  *
@@ -1,12 +1,12 @@
1
1
  import { EventPublisher, SequentialIdGenerator, compareSequentialIds } from '@orpc/shared';
2
2
  import { withEventMeta, getEventMeta } from '@orpc/standard-server';
3
- import { P as Publisher } from '../shared/experimental-publisher.BOyYBkgn.mjs';
3
+ import { P as Publisher } from '../shared/experimental-publisher.BtlOkhPO.mjs';
4
4
 
5
5
  class MemoryPublisher extends Publisher {
6
6
  eventPublisher = new EventPublisher();
7
7
  idGenerator = new SequentialIdGenerator();
8
8
  retentionSeconds;
9
- events = /* @__PURE__ */ new Map();
9
+ eventsMap = /* @__PURE__ */ new Map();
10
10
  /**
11
11
  * Useful for measuring memory usage.
12
12
  *
@@ -15,7 +15,7 @@ class MemoryPublisher extends Publisher {
15
15
  */
16
16
  get size() {
17
17
  let size = this.eventPublisher.size;
18
- for (const events of this.events) {
18
+ for (const events of this.eventsMap) {
19
19
  size += events[1].length || 1;
20
20
  }
21
21
  return size;
@@ -32,9 +32,9 @@ class MemoryPublisher extends Publisher {
32
32
  if (this.isResumeEnabled) {
33
33
  const now = Date.now();
34
34
  const expiresAt = now + this.retentionSeconds * 1e3;
35
- let events = this.events.get(event);
35
+ let events = this.eventsMap.get(event);
36
36
  if (!events) {
37
- this.events.set(event, events = []);
37
+ this.eventsMap.set(event, events = []);
38
38
  }
39
39
  payload = withEventMeta(payload, { ...getEventMeta(payload), id: this.idGenerator.generate() });
40
40
  events.push({ expiresAt, payload });
@@ -43,7 +43,7 @@ class MemoryPublisher extends Publisher {
43
43
  }
44
44
  async subscribeListener(event, listener, options) {
45
45
  if (this.isResumeEnabled && typeof options?.lastEventId === "string") {
46
- const events = this.events.get(event);
46
+ const events = this.eventsMap.get(event);
47
47
  if (events) {
48
48
  for (const { payload } of events) {
49
49
  const id = getEventMeta(payload)?.id;
@@ -68,12 +68,12 @@ class MemoryPublisher extends Publisher {
68
68
  return;
69
69
  }
70
70
  this.lastCleanupTime = now;
71
- for (const [event, events] of this.events) {
71
+ for (const [event, events] of this.eventsMap) {
72
72
  const validEvents = events.filter((event2) => event2.expiresAt > now);
73
73
  if (validEvents.length > 0) {
74
- this.events.set(event, validEvents);
74
+ this.eventsMap.set(event, validEvents);
75
75
  } else {
76
- this.events.delete(event);
76
+ this.eventsMap.delete(event);
77
77
  }
78
78
  }
79
79
  }
@@ -1,8 +1,8 @@
1
1
  import { StandardRPCJsonSerializerOptions, StandardRPCJsonSerializer, StandardRPCJsonSerializedMetaItem } from '@orpc/client/standard';
2
+ import { ThrowableError } from '@orpc/shared';
2
3
  import { Redis } from '@upstash/redis';
3
4
  import { PublisherOptions, Publisher, PublisherSubscribeListenerOptions } from '../index.mjs';
4
5
  import { getEventMeta } from '@orpc/standard-server';
5
- import '@orpc/shared';
6
6
 
7
7
  type SerializedPayload = {
8
8
  json: object;
@@ -34,7 +34,8 @@ declare class UpstashRedisPublisher<T extends Record<string, object>> extends Pu
34
34
  protected readonly prefix: string;
35
35
  protected readonly serializer: StandardRPCJsonSerializer;
36
36
  protected readonly retentionSeconds: number;
37
- protected readonly listenersMap: Map<string, Set<(payload: any) => void>>;
37
+ protected readonly listenersMap: Map<string, ((payload: any) => void)[]>;
38
+ protected readonly onErrorsMap: Map<string, ((error: ThrowableError) => void)[]>;
38
39
  protected readonly subscriptionPromiseMap: Map<string, Promise<void>>;
39
40
  protected readonly subscriptionsMap: Map<string, any>;
40
41
  protected get isResumeEnabled(): boolean;
@@ -52,9 +53,9 @@ declare class UpstashRedisPublisher<T extends Record<string, object>> extends Pu
52
53
  */
53
54
  get size(): number;
54
55
  constructor(redis: Redis, { resumeRetentionSeconds, prefix, ...options }?: UpstashRedisPublisherOptions);
55
- protected lastCleanupTimes: Map<string, number>;
56
+ protected lastCleanupTimeMap: Map<string, number>;
56
57
  publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
57
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
58
+ protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
58
59
  protected prefixKey(key: string): string;
59
60
  protected serializePayload(payload: object): SerializedPayload;
60
61
  protected deserializePayload(id: string | undefined, { json, meta, eventMeta }: SerializedPayload): any;
@@ -1,8 +1,8 @@
1
1
  import { StandardRPCJsonSerializerOptions, StandardRPCJsonSerializer, StandardRPCJsonSerializedMetaItem } from '@orpc/client/standard';
2
+ import { ThrowableError } from '@orpc/shared';
2
3
  import { Redis } from '@upstash/redis';
3
4
  import { PublisherOptions, Publisher, PublisherSubscribeListenerOptions } from '../index.js';
4
5
  import { getEventMeta } from '@orpc/standard-server';
5
- import '@orpc/shared';
6
6
 
7
7
  type SerializedPayload = {
8
8
  json: object;
@@ -34,7 +34,8 @@ declare class UpstashRedisPublisher<T extends Record<string, object>> extends Pu
34
34
  protected readonly prefix: string;
35
35
  protected readonly serializer: StandardRPCJsonSerializer;
36
36
  protected readonly retentionSeconds: number;
37
- protected readonly listenersMap: Map<string, Set<(payload: any) => void>>;
37
+ protected readonly listenersMap: Map<string, ((payload: any) => void)[]>;
38
+ protected readonly onErrorsMap: Map<string, ((error: ThrowableError) => void)[]>;
38
39
  protected readonly subscriptionPromiseMap: Map<string, Promise<void>>;
39
40
  protected readonly subscriptionsMap: Map<string, any>;
40
41
  protected get isResumeEnabled(): boolean;
@@ -52,9 +53,9 @@ declare class UpstashRedisPublisher<T extends Record<string, object>> extends Pu
52
53
  */
53
54
  get size(): number;
54
55
  constructor(redis: Redis, { resumeRetentionSeconds, prefix, ...options }?: UpstashRedisPublisherOptions);
55
- protected lastCleanupTimes: Map<string, number>;
56
+ protected lastCleanupTimeMap: Map<string, number>;
56
57
  publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
57
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
58
+ protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
58
59
  protected prefixKey(key: string): string;
59
60
  protected serializePayload(payload: object): SerializedPayload;
60
61
  protected deserializePayload(id: string | undefined, { json, meta, eventMeta }: SerializedPayload): any;
@@ -1,13 +1,13 @@
1
1
  import { StandardRPCJsonSerializer } from '@orpc/client/standard';
2
+ import { fallback, once } from '@orpc/shared';
2
3
  import { getEventMeta, withEventMeta } from '@orpc/standard-server';
3
- import { P as Publisher } from '../shared/experimental-publisher.BOyYBkgn.mjs';
4
- import '@orpc/shared';
4
+ import { P as Publisher } from '../shared/experimental-publisher.BtlOkhPO.mjs';
5
5
 
6
6
  class UpstashRedisPublisher extends Publisher {
7
7
  constructor(redis, { resumeRetentionSeconds, prefix, ...options } = {}) {
8
8
  super(options);
9
9
  this.redis = redis;
10
- this.prefix = prefix ?? "orpc:publisher:";
10
+ this.prefix = fallback(prefix, "orpc:publisher:");
11
11
  this.retentionSeconds = resumeRetentionSeconds ?? Number.NaN;
12
12
  this.serializer = new StandardRPCJsonSerializer(options);
13
13
  }
@@ -15,6 +15,7 @@ class UpstashRedisPublisher extends Publisher {
15
15
  serializer;
16
16
  retentionSeconds;
17
17
  listenersMap = /* @__PURE__ */ new Map();
18
+ onErrorsMap = /* @__PURE__ */ new Map();
18
19
  subscriptionPromiseMap = /* @__PURE__ */ new Map();
19
20
  subscriptionsMap = /* @__PURE__ */ new Map();
20
21
  // Upstash subscription objects
@@ -36,24 +37,27 @@ class UpstashRedisPublisher extends Publisher {
36
37
  get size() {
37
38
  let size = 0;
38
39
  for (const listeners of this.listenersMap) {
39
- size += listeners[1].size || 1;
40
+ size += listeners[1].length || 1;
41
+ }
42
+ for (const onErrors of this.onErrorsMap) {
43
+ size += onErrors[1].length || 1;
40
44
  }
41
45
  return size;
42
46
  }
43
- lastCleanupTimes = /* @__PURE__ */ new Map();
47
+ lastCleanupTimeMap = /* @__PURE__ */ new Map();
44
48
  async publish(event, payload) {
45
49
  const key = this.prefixKey(event);
46
50
  const serialized = this.serializePayload(payload);
47
51
  let id;
48
52
  if (this.isResumeEnabled) {
49
53
  const now = Date.now();
50
- for (const [key2, lastCleanupTime] of this.lastCleanupTimes) {
54
+ for (const [mapKey, lastCleanupTime] of this.lastCleanupTimeMap) {
51
55
  if (lastCleanupTime + this.retentionSeconds * 1e3 < now) {
52
- this.lastCleanupTimes.delete(key2);
56
+ this.lastCleanupTimeMap.delete(mapKey);
53
57
  }
54
58
  }
55
- if (!this.lastCleanupTimes.has(key)) {
56
- this.lastCleanupTimes.set(key, now);
59
+ if (!this.lastCleanupTimeMap.has(key)) {
60
+ this.lastCleanupTimeMap.set(key, now);
57
61
  const results = await this.redis.multi().xadd(key, "*", { data: serialized }).xtrim(key, { strategy: "MINID", exactness: this.xtrimExactness, threshold: `${now - this.retentionSeconds * 1e3}-0` }).expire(key, this.retentionSeconds * 2).exec();
58
62
  id = results[0];
59
63
  } else {
@@ -63,9 +67,8 @@ class UpstashRedisPublisher extends Publisher {
63
67
  }
64
68
  await this.redis.publish(key, { ...serialized, id });
65
69
  }
66
- async subscribeListener(event, originalListener, options) {
70
+ async subscribeListener(event, originalListener, { lastEventId, onError } = {}) {
67
71
  const key = this.prefixKey(event);
68
- const lastEventId = options?.lastEventId;
69
72
  let pendingPayloads = [];
70
73
  const resumePayloadIds = /* @__PURE__ */ new Set();
71
74
  const listener = (payload) => {
@@ -79,9 +82,20 @@ class UpstashRedisPublisher extends Publisher {
79
82
  }
80
83
  originalListener(payload);
81
84
  };
82
- await this.subscriptionPromiseMap.get(key);
85
+ const subscriptionPromise = this.subscriptionPromiseMap.get(key);
86
+ if (subscriptionPromise) {
87
+ await subscriptionPromise;
88
+ }
83
89
  let subscription = this.subscriptionsMap.get(key);
84
90
  if (!subscription) {
91
+ const dispatchErrorForKey = (error) => {
92
+ const onErrors = this.onErrorsMap.get(key);
93
+ if (onErrors) {
94
+ for (const onError2 of onErrors) {
95
+ onError2(error);
96
+ }
97
+ }
98
+ };
85
99
  subscription = this.redis.subscribe(key);
86
100
  subscription.on("message", (event2) => {
87
101
  try {
@@ -93,7 +107,8 @@ class UpstashRedisPublisher extends Publisher {
93
107
  listener2(payload);
94
108
  }
95
109
  }
96
- } catch {
110
+ } catch (error) {
111
+ dispatchErrorForKey(error);
97
112
  }
98
113
  });
99
114
  let resolvePromise;
@@ -104,6 +119,7 @@ class UpstashRedisPublisher extends Publisher {
104
119
  });
105
120
  subscription.on("error", (error) => {
106
121
  rejectPromise(error);
122
+ dispatchErrorForKey(error);
107
123
  });
108
124
  subscription.on("subscribe", () => {
109
125
  resolvePromise();
@@ -118,9 +134,16 @@ class UpstashRedisPublisher extends Publisher {
118
134
  }
119
135
  let listeners = this.listenersMap.get(key);
120
136
  if (!listeners) {
121
- this.listenersMap.set(key, listeners = /* @__PURE__ */ new Set());
137
+ this.listenersMap.set(key, listeners = []);
138
+ }
139
+ listeners.push(listener);
140
+ if (onError) {
141
+ let onErrors = this.onErrorsMap.get(key);
142
+ if (!onErrors) {
143
+ this.onErrorsMap.set(key, onErrors = []);
144
+ }
145
+ onErrors.push(onError);
122
146
  }
123
- listeners.add(listener);
124
147
  void (async () => {
125
148
  try {
126
149
  if (this.isResumeEnabled && typeof lastEventId === "string") {
@@ -135,7 +158,8 @@ class UpstashRedisPublisher extends Publisher {
135
158
  }
136
159
  }
137
160
  }
138
- } catch {
161
+ } catch (error) {
162
+ onError?.(error);
139
163
  } finally {
140
164
  const pending = pendingPayloads;
141
165
  pendingPayloads = void 0;
@@ -144,10 +168,20 @@ class UpstashRedisPublisher extends Publisher {
144
168
  }
145
169
  }
146
170
  })();
171
+ const cleanupListeners = once(() => {
172
+ listeners.splice(listeners.indexOf(listener), 1);
173
+ if (onError) {
174
+ const onErrors = this.onErrorsMap.get(key);
175
+ if (onErrors) {
176
+ onErrors.splice(onErrors.indexOf(onError), 1);
177
+ }
178
+ }
179
+ });
147
180
  return async () => {
148
- listeners.delete(listener);
149
- if (listeners.size === 0) {
181
+ cleanupListeners();
182
+ if (listeners.length === 0) {
150
183
  this.listenersMap.delete(key);
184
+ this.onErrorsMap.delete(key);
151
185
  const subscription2 = this.subscriptionsMap.get(key);
152
186
  if (subscription2) {
153
187
  this.subscriptionsMap.delete(key);
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AsyncIteratorClass } from '@orpc/shared';
1
+ import { ThrowableError, AsyncIteratorClass } from '@orpc/shared';
2
2
 
3
3
  interface PublisherOptions {
4
4
  /**
@@ -21,8 +21,12 @@ interface PublisherSubscribeListenerOptions {
21
21
  * Resume from a specific event ID
22
22
  */
23
23
  lastEventId?: string | undefined;
24
+ /**
25
+ * Triggered when an error occur
26
+ */
27
+ onError?: (error: ThrowableError) => void;
24
28
  }
25
- interface PublisherSubscribeIteratorOptions extends PublisherSubscribeListenerOptions, PublisherOptions {
29
+ interface PublisherSubscribeIteratorOptions extends Pick<PublisherSubscribeListenerOptions, 'lastEventId'>, Pick<PublisherOptions, 'maxBufferedEvents'> {
26
30
  /**
27
31
  * Abort signal, automatically unsubscribes on abort
28
32
  */
@@ -51,6 +55,11 @@ declare abstract class Publisher<T extends Record<string, object>> {
51
55
  * ```ts
52
56
  * const unsubscribe = publisher.subscribe('event', (payload) => {
53
57
  * console.log(payload)
58
+ * }, {
59
+ * lastEventId,
60
+ * onError: (error) => {
61
+ * // handle error (consider unsubscribe if error can't be recovered)
62
+ * }
54
63
  * })
55
64
  *
56
65
  * // Later
@@ -64,7 +73,7 @@ declare abstract class Publisher<T extends Record<string, object>> {
64
73
  *
65
74
  * @example
66
75
  * ```ts
67
- * for await (const payload of publisher.subscribe('event', { signal })) {
76
+ * for await (const payload of publisher.subscribe('event', { signal, lastEventId })) {
68
77
  * console.log(payload)
69
78
  * }
70
79
  * ```
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AsyncIteratorClass } from '@orpc/shared';
1
+ import { ThrowableError, AsyncIteratorClass } from '@orpc/shared';
2
2
 
3
3
  interface PublisherOptions {
4
4
  /**
@@ -21,8 +21,12 @@ interface PublisherSubscribeListenerOptions {
21
21
  * Resume from a specific event ID
22
22
  */
23
23
  lastEventId?: string | undefined;
24
+ /**
25
+ * Triggered when an error occur
26
+ */
27
+ onError?: (error: ThrowableError) => void;
24
28
  }
25
- interface PublisherSubscribeIteratorOptions extends PublisherSubscribeListenerOptions, PublisherOptions {
29
+ interface PublisherSubscribeIteratorOptions extends Pick<PublisherSubscribeListenerOptions, 'lastEventId'>, Pick<PublisherOptions, 'maxBufferedEvents'> {
26
30
  /**
27
31
  * Abort signal, automatically unsubscribes on abort
28
32
  */
@@ -51,6 +55,11 @@ declare abstract class Publisher<T extends Record<string, object>> {
51
55
  * ```ts
52
56
  * const unsubscribe = publisher.subscribe('event', (payload) => {
53
57
  * console.log(payload)
58
+ * }, {
59
+ * lastEventId,
60
+ * onError: (error) => {
61
+ * // handle error (consider unsubscribe if error can't be recovered)
62
+ * }
54
63
  * })
55
64
  *
56
65
  * // Later
@@ -64,7 +73,7 @@ declare abstract class Publisher<T extends Record<string, object>> {
64
73
  *
65
74
  * @example
66
75
  * ```ts
67
- * for await (const payload of publisher.subscribe('event', { signal })) {
76
+ * for await (const payload of publisher.subscribe('event', { signal, lastEventId })) {
68
77
  * console.log(payload)
69
78
  * }
70
79
  * ```
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export { P as Publisher } from './shared/experimental-publisher.BOyYBkgn.mjs';
1
+ export { P as Publisher } from './shared/experimental-publisher.BtlOkhPO.mjs';
2
2
  import '@orpc/shared';
@@ -14,6 +14,7 @@ class Publisher {
14
14
  signal?.throwIfAborted();
15
15
  const bufferedEvents = [];
16
16
  const pullResolvers = [];
17
+ let subscriptionError;
17
18
  const unsubscribePromise = this.subscribe(event, (payload) => {
18
19
  const resolver = pullResolvers.shift();
19
20
  if (resolver) {
@@ -24,20 +25,34 @@ class Publisher {
24
25
  bufferedEvents.shift();
25
26
  }
26
27
  }
28
+ }, {
29
+ lastEventId: listenerOrOptions?.lastEventId,
30
+ onError: (error) => {
31
+ subscriptionError = { error };
32
+ pullResolvers.forEach((resolver) => resolver[1](error));
33
+ signal?.removeEventListener("abort", abortListener);
34
+ pullResolvers.length = 0;
35
+ bufferedEvents.length = 0;
36
+ unsubscribePromise.then((unsubscribe) => unsubscribe()).catch(() => {
37
+ });
38
+ }
27
39
  });
28
- const abortListener = (event2) => {
40
+ function abortListener(event2) {
29
41
  pullResolvers.forEach((resolver) => resolver[1](event2.target.reason));
30
42
  pullResolvers.length = 0;
31
43
  bufferedEvents.length = 0;
32
44
  unsubscribePromise.then((unsubscribe) => unsubscribe()).catch(() => {
33
45
  });
34
- };
46
+ }
35
47
  signal?.addEventListener("abort", abortListener, { once: true });
36
48
  return new AsyncIteratorClass(async () => {
37
- await unsubscribePromise;
49
+ if (subscriptionError) {
50
+ throw subscriptionError.error;
51
+ }
38
52
  if (signal?.aborted) {
39
53
  throw signal.reason;
40
54
  }
55
+ await unsubscribePromise;
41
56
  if (bufferedEvents.length > 0) {
42
57
  return { done: false, value: bufferedEvents.shift() };
43
58
  }
@@ -45,8 +60,8 @@ class Publisher {
45
60
  pullResolvers.push([resolve, reject]);
46
61
  });
47
62
  }, async () => {
48
- signal?.removeEventListener("abort", abortListener);
49
63
  pullResolvers.forEach((resolver) => resolver[0]({ done: true, value: void 0 }));
64
+ signal?.removeEventListener("abort", abortListener);
50
65
  pullResolvers.length = 0;
51
66
  bufferedEvents.length = 0;
52
67
  await unsubscribePromise.then((unsubscribe) => unsubscribe());
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/experimental-publisher",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.0.1",
5
5
  "license": "MIT",
6
6
  "homepage": "https://orpc.unnoq.com",
7
7
  "repository": {