@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.
- package/dist/adapters/ioredis.d.mts +10 -6
- package/dist/adapters/ioredis.d.ts +10 -6
- package/dist/adapters/ioredis.mjs +77 -29
- package/dist/adapters/memory.d.mts +1 -1
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/adapters/memory.mjs +9 -9
- package/dist/adapters/upstash-redis.d.mts +5 -4
- package/dist/adapters/upstash-redis.d.ts +5 -4
- package/dist/adapters/upstash-redis.mjs +52 -18
- package/dist/index.d.mts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.mjs +1 -1
- package/dist/shared/{experimental-publisher.BOyYBkgn.mjs → experimental-publisher.BtlOkhPO.mjs} +19 -4
- package/package.json +1 -1
|
@@ -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
|
|
50
|
-
protected readonly listenersMap: Map<string,
|
|
51
|
-
protected
|
|
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
|
|
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,
|
|
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
|
|
50
|
-
protected readonly listenersMap: Map<string,
|
|
51
|
-
protected
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
12
|
+
subscriptionPromiseMap = /* @__PURE__ */ new Map();
|
|
13
13
|
listenersMap = /* @__PURE__ */ new Map();
|
|
14
|
-
|
|
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.
|
|
32
|
+
let size = this.redisListenerAndOnError ? 1 : 0;
|
|
32
33
|
for (const listeners of this.listenersMap) {
|
|
33
|
-
size += listeners[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
|
|
45
|
+
this.prefix = fallback(prefix, "orpc:publisher:");
|
|
42
46
|
this.retentionSeconds = resumeRetentionSeconds ?? Number.NaN;
|
|
43
47
|
this.serializer = new StandardRPCJsonSerializer(options);
|
|
44
48
|
}
|
|
45
|
-
|
|
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 [
|
|
56
|
+
for (const [mapKey, lastCleanupTime] of this.lastCleanupTimeMap) {
|
|
53
57
|
if (lastCleanupTime + this.retentionSeconds * 1e3 < now) {
|
|
54
|
-
this.
|
|
58
|
+
this.lastCleanupTimeMap.delete(mapKey);
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
|
-
if (!this.
|
|
58
|
-
this.
|
|
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,
|
|
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.
|
|
92
|
-
|
|
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
|
|
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.
|
|
133
|
+
this.subscriptionPromiseMap.set(key, promise);
|
|
113
134
|
await promise;
|
|
114
|
-
this.listenersMap.set(key, listeners =
|
|
135
|
+
this.listenersMap.set(key, listeners = []);
|
|
115
136
|
} finally {
|
|
116
|
-
this.
|
|
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
|
-
|
|
145
|
-
if (listeners.
|
|
190
|
+
cleanupListeners();
|
|
191
|
+
if (listeners.length === 0) {
|
|
146
192
|
this.listenersMap.delete(key);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.
|
|
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
|
|
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
|
|
22
|
+
private readonly eventsMap;
|
|
23
23
|
/**
|
|
24
24
|
* Useful for measuring memory usage.
|
|
25
25
|
*
|
package/dist/adapters/memory.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
35
|
+
let events = this.eventsMap.get(event);
|
|
36
36
|
if (!events) {
|
|
37
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
74
|
+
this.eventsMap.set(event, validEvents);
|
|
75
75
|
} else {
|
|
76
|
-
this.
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
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].
|
|
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
|
-
|
|
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 [
|
|
54
|
+
for (const [mapKey, lastCleanupTime] of this.lastCleanupTimeMap) {
|
|
51
55
|
if (lastCleanupTime + this.retentionSeconds * 1e3 < now) {
|
|
52
|
-
this.
|
|
56
|
+
this.lastCleanupTimeMap.delete(mapKey);
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
|
-
if (!this.
|
|
56
|
-
this.
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
149
|
-
if (listeners.
|
|
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.
|
|
1
|
+
export { P as Publisher } from './shared/experimental-publisher.BtlOkhPO.mjs';
|
|
2
2
|
import '@orpc/shared';
|
package/dist/shared/{experimental-publisher.BOyYBkgn.mjs → experimental-publisher.BtlOkhPO.mjs}
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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());
|