@orpc/publisher 2.0.0-beta.4 → 2.0.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -94
- package/dist/adapters/base-redis.d.mts +120 -0
- package/dist/adapters/base-redis.d.ts +120 -0
- package/dist/adapters/base-redis.mjs +106 -0
- package/dist/adapters/memory.d.mts +13 -7
- package/dist/adapters/memory.d.ts +13 -7
- package/dist/adapters/memory.mjs +21 -18
- package/dist/adapters/redis.d.mts +19 -55
- package/dist/adapters/redis.d.ts +19 -55
- package/dist/adapters/redis.mjs +38 -110
- package/dist/adapters/upstash.d.mts +19 -60
- package/dist/adapters/upstash.d.ts +19 -60
- package/dist/adapters/upstash.mjs +65 -164
- package/dist/index.d.mts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.mjs +1 -1
- package/dist/shared/{publisher.BSQf8ybR.mjs → publisher.DcPG7zl6.mjs} +47 -9
- package/package.json +25 -8
|
@@ -1,71 +1,30 @@
|
|
|
1
|
+
import { ThrowableError } from '@orpc/shared';
|
|
1
2
|
import { Redis } from '@upstash/redis';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import '@orpc/
|
|
3
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamEntry } from './base-redis.mjs';
|
|
4
|
+
import '../index.mjs';
|
|
5
|
+
import '@orpc/client';
|
|
5
6
|
|
|
6
|
-
interface UpstashPublisherOptions extends
|
|
7
|
-
/**
|
|
8
|
-
* The prefix to use for Redis keys.
|
|
9
|
-
*/
|
|
10
|
-
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
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 {
|
|
3
|
-
import
|
|
4
|
-
import '@orpc/
|
|
3
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamEntry } from './base-redis.js';
|
|
4
|
+
import '../index.js';
|
|
5
|
+
import '@orpc/client';
|
|
5
6
|
|
|
6
|
-
interface UpstashPublisherOptions extends
|
|
7
|
-
/**
|
|
8
|
-
* The prefix to use for Redis keys.
|
|
9
|
-
*/
|
|
10
|
-
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
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,100 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import { promiseWithResolvers } from '@orpc/shared';
|
|
2
|
+
import { BaseRedisPublisher } from './base-redis.mjs';
|
|
3
|
+
import '@orpc/client';
|
|
4
|
+
import '@standard-server/core';
|
|
5
|
+
import '../shared/publisher.DcPG7zl6.mjs';
|
|
5
6
|
|
|
6
|
-
class UpstashPublisher extends
|
|
7
|
-
constructor(redis,
|
|
7
|
+
class UpstashPublisher extends BaseRedisPublisher {
|
|
8
|
+
constructor(redis, options = {}) {
|
|
8
9
|
super(options);
|
|
9
10
|
this.redis = redis;
|
|
10
|
-
this.prefix = prefix ?? "";
|
|
11
|
-
this.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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The exactness of the `XTRIM` command.
|
|
25
|
-
* Used for testing purpose.
|
|
26
|
-
*/
|
|
27
|
-
xTrimExactness = "~";
|
|
28
|
-
firstPublishTimeMap = /* @__PURE__ */ new Map();
|
|
29
|
-
async publish(event, payload) {
|
|
30
|
-
const redisKey = `${this.prefix}${event}`;
|
|
31
|
-
const data = this.serializePayload(payload);
|
|
32
|
-
let id;
|
|
33
|
-
if (this.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
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
this.listenersMap.delete(event);
|
|
97
|
-
}
|
|
98
|
-
await this.unsubscribeEvent(event);
|
|
24
|
+
remove(this.listenersMap, channel, listener);
|
|
25
|
+
await this.unsubscribeIfUnused(channel);
|
|
99
26
|
throw error;
|
|
100
27
|
}
|
|
101
28
|
if (onError) {
|
|
102
|
-
|
|
103
|
-
if (!onErrors) {
|
|
104
|
-
this.onErrorsMap.set(event, onErrors = []);
|
|
105
|
-
}
|
|
106
|
-
onErrors.push(onError);
|
|
29
|
+
push(this.onErrorsMap, channel, onError);
|
|
107
30
|
}
|
|
108
|
-
return
|
|
109
|
-
|
|
31
|
+
return async () => {
|
|
32
|
+
remove(this.listenersMap, channel, listener);
|
|
110
33
|
if (onError) {
|
|
111
|
-
|
|
112
|
-
if (onErrors) {
|
|
113
|
-
onErrors.splice(onErrors.indexOf(onError), 1);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (listeners.length === 0) {
|
|
117
|
-
this.listenersMap.delete(event);
|
|
118
|
-
this.onErrorsMap.delete(event);
|
|
34
|
+
remove(this.onErrorsMap, channel, onError);
|
|
119
35
|
}
|
|
120
|
-
await this.
|
|
121
|
-
}
|
|
36
|
+
await this.unsubscribeIfUnused(channel);
|
|
37
|
+
};
|
|
122
38
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
39
|
+
async evalScript(script, keys, args) {
|
|
40
|
+
return await this.redis.eval(script, keys, args);
|
|
41
|
+
}
|
|
42
|
+
async readStreamEntries(key, lastId) {
|
|
43
|
+
const results = await this.redis.xread(key, lastId);
|
|
44
|
+
return results?.[0]?.[1].map(([id, fields]) => ({ id, data: fields[1] })) ?? [];
|
|
45
|
+
}
|
|
46
|
+
async subscribeIfNeeded(channel) {
|
|
47
|
+
while (this.pendingSubscriptionsMap.has(channel)) {
|
|
48
|
+
await this.pendingSubscriptionsMap.get(channel).catch(() => {
|
|
49
|
+
});
|
|
132
50
|
}
|
|
133
|
-
if (this.subscriptionMap.has(
|
|
51
|
+
if (this.subscriptionMap.has(channel)) {
|
|
134
52
|
return;
|
|
135
53
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
};
|
|
140
|
-
const subscription = this.redis.subscribe(redisKey);
|
|
141
|
-
subscription.on("message", (message) => {
|
|
142
|
-
try {
|
|
143
|
-
const listeners = this.listenersMap.get(event);
|
|
144
|
-
if (listeners) {
|
|
145
|
-
const { id, data } = message.message;
|
|
146
|
-
const payload = this.deserializePayload(id, data);
|
|
147
|
-
listeners.forEach((listener) => listener(payload));
|
|
148
|
-
}
|
|
149
|
-
} catch (error) {
|
|
150
|
-
dispatchErrorForEvent(error);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
let resolvePromise;
|
|
154
|
-
let rejectPromise;
|
|
155
|
-
const promise = new Promise((resolve, reject) => {
|
|
156
|
-
resolvePromise = resolve;
|
|
157
|
-
rejectPromise = reject;
|
|
158
|
-
});
|
|
54
|
+
const { promise, resolve, reject } = promiseWithResolvers();
|
|
55
|
+
const subscription = this.redis.subscribe(channel);
|
|
56
|
+
subscription.on("subscribe", () => resolve());
|
|
159
57
|
subscription.on("error", (error) => {
|
|
160
|
-
|
|
161
|
-
|
|
58
|
+
reject(error);
|
|
59
|
+
this.onErrorsMap.get(channel)?.slice().forEach((onError) => onError(error));
|
|
162
60
|
});
|
|
163
|
-
subscription.on("
|
|
164
|
-
|
|
61
|
+
subscription.on("message", ({ message }) => {
|
|
62
|
+
this.listenersMap.get(channel)?.slice().forEach((listener) => listener(message));
|
|
165
63
|
});
|
|
64
|
+
this.pendingSubscriptionsMap.set(channel, promise);
|
|
166
65
|
try {
|
|
167
|
-
this.pendingSubscriptionsMap.set(event, promise);
|
|
168
66
|
await promise;
|
|
169
|
-
this.subscriptionMap.set(
|
|
67
|
+
this.subscriptionMap.set(channel, subscription);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
await subscription.unsubscribe();
|
|
70
|
+
throw error;
|
|
170
71
|
} finally {
|
|
171
|
-
this.pendingSubscriptionsMap.delete(
|
|
72
|
+
this.pendingSubscriptionsMap.delete(channel);
|
|
172
73
|
}
|
|
173
74
|
}
|
|
174
|
-
async
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
await pending;
|
|
179
|
-
} catch {
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const subscription = this.subscriptionMap.get(event);
|
|
183
|
-
if (!this.listenersMap.has(event) && subscription) {
|
|
184
|
-
this.subscriptionMap.delete(event);
|
|
75
|
+
async unsubscribeIfUnused(channel) {
|
|
76
|
+
const subscription = this.subscriptionMap.get(channel);
|
|
77
|
+
if (subscription && !this.listenersMap.has(channel)) {
|
|
78
|
+
this.subscriptionMap.delete(channel);
|
|
185
79
|
await subscription.unsubscribe();
|
|
186
80
|
}
|
|
187
81
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
82
|
+
}
|
|
83
|
+
function push(map, key, value) {
|
|
84
|
+
let values = map.get(key);
|
|
85
|
+
if (!values) {
|
|
86
|
+
map.set(key, values = []);
|
|
87
|
+
}
|
|
88
|
+
values.push(value);
|
|
89
|
+
}
|
|
90
|
+
function remove(map, key, value) {
|
|
91
|
+
const values = map.get(key);
|
|
92
|
+
const index = values?.indexOf(value) ?? -1;
|
|
93
|
+
if (index !== -1) {
|
|
94
|
+
values.splice(index, 1);
|
|
191
95
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.serializer.deserialize(payload),
|
|
195
|
-
id === void 0 ? { ...meta } : { ...meta, id }
|
|
196
|
-
);
|
|
96
|
+
if (values?.length === 0) {
|
|
97
|
+
map.delete(key);
|
|
197
98
|
}
|
|
198
99
|
}
|
|
199
100
|
|
package/dist/index.d.mts
CHANGED
|
@@ -2,10 +2,12 @@ import { ThrowableError, AsyncIteratorClass } from '@orpc/shared';
|
|
|
2
2
|
|
|
3
3
|
interface PublisherOptions {
|
|
4
4
|
/**
|
|
5
|
-
* Maximum number of events to buffer for
|
|
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.
|
|
9
|
+
* Missed events the adapter replays for `lastEventId` while subscribing are all kept,
|
|
10
|
+
* and the limit then applies to how much further behind the consumer falls.
|
|
9
11
|
*
|
|
10
12
|
* Set to:
|
|
11
13
|
* - `0`: Disable buffering. Events must be consumed before the next one arrives.
|
|
@@ -42,6 +44,8 @@ declare abstract class Publisher<T extends Record<string, object>> {
|
|
|
42
44
|
/**
|
|
43
45
|
* Subscribes to a specific event using a callback function.
|
|
44
46
|
* Returns an unsubscribe function to remove the listener.
|
|
47
|
+
* Events missed since `lastEventId` should reach the listener before the returned promise
|
|
48
|
+
* resolves, so iterator subscribers keep them all regardless of `maxBufferedEvents`.
|
|
45
49
|
*
|
|
46
50
|
* @remarks
|
|
47
51
|
* This method should be protected to avoid conflicts with `subscribe` method
|
|
@@ -68,7 +72,7 @@ declare abstract class Publisher<T extends Record<string, object>> {
|
|
|
68
72
|
*/
|
|
69
73
|
subscribe<K extends keyof T & string>(event: K, listener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
70
74
|
/**
|
|
71
|
-
* Subscribes to a specific event using an
|
|
75
|
+
* Subscribes to a specific event using an AsyncIteratorObject.
|
|
72
76
|
* Useful for `for await...of` loops with optional buffering and abort support.
|
|
73
77
|
*
|
|
74
78
|
* @example
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { ThrowableError, AsyncIteratorClass } from '@orpc/shared';
|
|
|
2
2
|
|
|
3
3
|
interface PublisherOptions {
|
|
4
4
|
/**
|
|
5
|
-
* Maximum number of events to buffer for
|
|
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.
|
|
9
|
+
* Missed events the adapter replays for `lastEventId` while subscribing are all kept,
|
|
10
|
+
* and the limit then applies to how much further behind the consumer falls.
|
|
9
11
|
*
|
|
10
12
|
* Set to:
|
|
11
13
|
* - `0`: Disable buffering. Events must be consumed before the next one arrives.
|
|
@@ -42,6 +44,8 @@ declare abstract class Publisher<T extends Record<string, object>> {
|
|
|
42
44
|
/**
|
|
43
45
|
* Subscribes to a specific event using a callback function.
|
|
44
46
|
* Returns an unsubscribe function to remove the listener.
|
|
47
|
+
* Events missed since `lastEventId` should reach the listener before the returned promise
|
|
48
|
+
* resolves, so iterator subscribers keep them all regardless of `maxBufferedEvents`.
|
|
45
49
|
*
|
|
46
50
|
* @remarks
|
|
47
51
|
* This method should be protected to avoid conflicts with `subscribe` method
|
|
@@ -68,7 +72,7 @@ declare abstract class Publisher<T extends Record<string, object>> {
|
|
|
68
72
|
*/
|
|
69
73
|
subscribe<K extends keyof T & string>(event: K, listener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
70
74
|
/**
|
|
71
|
-
* Subscribes to a specific event using an
|
|
75
|
+
* Subscribes to a specific event using an AsyncIteratorObject.
|
|
72
76
|
* Useful for `for await...of` loops with optional buffering and abort support.
|
|
73
77
|
*
|
|
74
78
|
* @example
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { P as Publisher } from './shared/publisher.
|
|
1
|
+
export { P as Publisher } from './shared/publisher.DcPG7zl6.mjs';
|
|
2
2
|
import '@orpc/shared';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AsyncIteratorClass } from '@orpc/shared';
|
|
1
|
+
import { throwIfAborted, AsyncIteratorClass } from '@orpc/shared';
|
|
2
2
|
|
|
3
3
|
class Publisher {
|
|
4
4
|
maxBufferedEvents;
|
|
@@ -11,10 +11,13 @@ class Publisher {
|
|
|
11
11
|
}
|
|
12
12
|
const signal = listenerOrOptions?.signal;
|
|
13
13
|
const maxBufferedEvents = listenerOrOptions?.maxBufferedEvents ?? this.maxBufferedEvents;
|
|
14
|
-
|
|
15
|
-
const
|
|
14
|
+
throwIfAborted(signal);
|
|
15
|
+
const resuming = listenerOrOptions?.lastEventId !== void 0;
|
|
16
|
+
const bufferedEvents = new Queue();
|
|
16
17
|
const pullResolvers = [];
|
|
17
|
-
|
|
18
|
+
let bufferFloor = 0;
|
|
19
|
+
let bufferLimit = resuming ? Infinity : maxBufferedEvents;
|
|
20
|
+
const subscription = this.subscribeListener(event, (payload) => {
|
|
18
21
|
if (signal?.aborted) {
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
@@ -23,14 +26,18 @@ class Publisher {
|
|
|
23
26
|
resolver.resolve({ done: false, value: payload });
|
|
24
27
|
} else {
|
|
25
28
|
bufferedEvents.push(payload);
|
|
26
|
-
if (bufferedEvents.length >
|
|
29
|
+
if (bufferedEvents.length > bufferLimit) {
|
|
27
30
|
bufferedEvents.shift();
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
}, {
|
|
31
34
|
lastEventId: listenerOrOptions?.lastEventId,
|
|
32
35
|
onError: (error) => terminate({ kind: "error", error })
|
|
33
|
-
})
|
|
36
|
+
});
|
|
37
|
+
const unsubscribePromise = (resuming ? subscription.finally(() => {
|
|
38
|
+
bufferFloor = bufferedEvents.length;
|
|
39
|
+
bufferLimit = bufferFloor + maxBufferedEvents;
|
|
40
|
+
}) : subscription).catch((error) => terminate({ kind: "error", error }));
|
|
34
41
|
const abortListener = () => terminate({ kind: "error", error: signal.reason });
|
|
35
42
|
signal?.addEventListener("abort", abortListener);
|
|
36
43
|
let terminalState;
|
|
@@ -47,7 +54,7 @@ class Publisher {
|
|
|
47
54
|
}
|
|
48
55
|
if (signal?.aborted) {
|
|
49
56
|
pullResolvers.length = 0;
|
|
50
|
-
bufferedEvents.
|
|
57
|
+
bufferedEvents.clear();
|
|
51
58
|
}
|
|
52
59
|
await unsubscribePromise.then((unsubscribe) => unsubscribe?.());
|
|
53
60
|
}
|
|
@@ -56,8 +63,13 @@ class Publisher {
|
|
|
56
63
|
if (terminalState?.kind === "cancelled") {
|
|
57
64
|
return { done: true, value: void 0 };
|
|
58
65
|
}
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
const value = bufferedEvents.shift();
|
|
67
|
+
if (value !== void 0) {
|
|
68
|
+
if (bufferedEvents.length < bufferFloor) {
|
|
69
|
+
bufferFloor = bufferedEvents.length;
|
|
70
|
+
bufferLimit = bufferFloor + maxBufferedEvents;
|
|
71
|
+
}
|
|
72
|
+
return { done: false, value };
|
|
61
73
|
}
|
|
62
74
|
if (terminalState?.kind === "error") {
|
|
63
75
|
throw terminalState.error;
|
|
@@ -70,5 +82,31 @@ class Publisher {
|
|
|
70
82
|
);
|
|
71
83
|
}
|
|
72
84
|
}
|
|
85
|
+
class Queue {
|
|
86
|
+
items = [];
|
|
87
|
+
head = 0;
|
|
88
|
+
get length() {
|
|
89
|
+
return this.items.length - this.head;
|
|
90
|
+
}
|
|
91
|
+
push(item) {
|
|
92
|
+
this.items.push(item);
|
|
93
|
+
}
|
|
94
|
+
shift() {
|
|
95
|
+
if (this.head === this.items.length) {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
const item = this.items[this.head];
|
|
99
|
+
this.items[this.head++] = void 0;
|
|
100
|
+
if (this.head >= 32 && this.head * 2 >= this.items.length) {
|
|
101
|
+
this.items.splice(0, this.head);
|
|
102
|
+
this.head = 0;
|
|
103
|
+
}
|
|
104
|
+
return item;
|
|
105
|
+
}
|
|
106
|
+
clear() {
|
|
107
|
+
this.items.length = 0;
|
|
108
|
+
this.head = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
73
111
|
|
|
74
112
|
export { Publisher as P };
|