@procedures/client 0.6.0 → 0.8.0
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/CHANGELOG.md +66 -0
- package/README.md +63 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +5 -0
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +10 -4
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/server/ndjson.d.ts +73 -0
- package/dist/adapters/http/server/ndjson.d.ts.map +1 -0
- package/dist/adapters/http/server/ndjson.js +228 -0
- package/dist/adapters/http/server/ndjson.js.map +1 -0
- package/dist/adapters/http/server/transport.d.ts +15 -4
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +29 -69
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/server/types.d.ts +15 -0
- package/dist/adapters/http/server/types.d.ts.map +1 -1
- package/dist/adapters/sequence.d.ts +14 -0
- package/dist/adapters/sequence.d.ts.map +1 -1
- package/dist/adapters/sequence.js +16 -0
- package/dist/adapters/sequence.js.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts +7 -3
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +9 -3
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +10 -0
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +22 -2
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/browser.d.ts +5 -5
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +4 -3
- package/dist/browser.js.map +1 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +1 -0
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/middleware/retry.d.ts +4 -0
- package/dist/client/middleware/retry.d.ts.map +1 -1
- package/dist/client/middleware/retry.js +86 -78
- package/dist/client/middleware/retry.js.map +1 -1
- package/dist/events/bus.d.ts +32 -14
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +119 -65
- package/dist/events/bus.js.map +1 -1
- package/dist/events/index.d.ts +3 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js.map +1 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +20 -6
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/remote.d.ts +80 -18
- package/dist/events/remote.d.ts.map +1 -1
- package/dist/events/remote.js +448 -58
- package/dist/events/remote.js.map +1 -1
- package/dist/events/types.d.ts +13 -2
- package/dist/events/types.d.ts.map +1 -1
- package/dist/node.d.ts +6 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +5 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/caller.d.ts +33 -0
- package/dist/procedures/caller.d.ts.map +1 -0
- package/dist/procedures/caller.js +27 -0
- package/dist/procedures/caller.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +2 -2
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +1 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +21 -1
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +9 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +28 -0
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +6 -0
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/index.d.ts +3 -1
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +2 -0
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +3 -0
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/types.d.ts +50 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -0
- package/dist/protocol.js.map +1 -1
- package/dist/server/procedure-server.d.ts +41 -5
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +94 -9
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/timers.d.ts +9 -0
- package/dist/timers.d.ts.map +1 -1
- package/dist/timers.js +6 -1
- package/dist/timers.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/http/client/transport.ts +10 -4
- package/src/adapters/http/server/ndjson.ts +259 -0
- package/src/adapters/http/server/transport.ts +31 -69
- package/src/adapters/http/server/types.ts +17 -0
- package/src/adapters/sequence.ts +18 -0
- package/src/adapters/websocket/client/transport.ts +9 -3
- package/src/adapters/websocket/server/transport.ts +23 -2
- package/src/browser.ts +9 -2
- package/src/client/errors/registry.ts +1 -0
- package/src/client/middleware/retry.ts +89 -78
- package/src/events/bus.ts +141 -73
- package/src/events/index.ts +3 -0
- package/src/events/publisher.ts +23 -6
- package/src/events/remote.ts +575 -73
- package/src/events/types.ts +15 -2
- package/src/node.ts +6 -0
- package/src/procedures/caller.ts +45 -0
- package/src/procedures/collection/index.ts +3 -0
- package/src/procedures/collection/procedures.ts +22 -0
- package/src/procedures/collection/schemas.ts +37 -0
- package/src/procedures/collection/storage.ts +6 -0
- package/src/procedures/index.ts +4 -0
- package/src/procedures/invoke.ts +2 -0
- package/src/procedures/types.ts +53 -0
- package/src/protocol.ts +3 -1
- package/src/server/procedure-server.ts +111 -11
- package/src/timers.ts +15 -1
package/dist/events/remote.js
CHANGED
|
@@ -10,13 +10,21 @@
|
|
|
10
10
|
* - `remoteEvents()` gives an async iterator of the events.
|
|
11
11
|
* - `subscribeRemote()` calls a handler for each event, and gives the unsubscribe function.
|
|
12
12
|
*
|
|
13
|
+
* The subscribers of the same topics with the same options on one transport share one call: one
|
|
14
|
+
* wire subscription with a reference count. The last unsubscribe sends the cancel. Each subscriber
|
|
15
|
+
* has its own queue, its own `seq`, its own `ready` and its own end.
|
|
16
|
+
*
|
|
13
17
|
* After a failure that a retry can repair (a closed connection, `UNAVAILABLE`, `TIMEOUT`), the
|
|
14
|
-
* subscriber sends the call again after a backoff
|
|
15
|
-
*
|
|
18
|
+
* subscriber sends the call again after a backoff, at most `maxAttempts` times without a
|
|
19
|
+
* confirmation. The delivery is at most once: the events between the failure and the new
|
|
20
|
+
* confirmation are lost. Each other error ends the subscription.
|
|
16
21
|
*
|
|
17
|
-
* This file imports only types and the
|
|
22
|
+
* This file imports only types, the backoff, the clock and the durations, so a page or a worker
|
|
23
|
+
* that subscribes stays small.
|
|
18
24
|
*/
|
|
19
25
|
import { backoffDelay } from "@mark1russell7/service/backoff";
|
|
26
|
+
import { realClock } from "../clock.js";
|
|
27
|
+
import { toMs } from "../duration.js";
|
|
20
28
|
/** The path of the procedure of a subscription. */
|
|
21
29
|
export const EVENTS_SUBSCRIBE_PATH = Object.freeze(["events", "subscribe"]);
|
|
22
30
|
/** The method of the procedure of a subscription. */
|
|
@@ -42,9 +50,22 @@ export class SubscriptionError extends Error {
|
|
|
42
50
|
}
|
|
43
51
|
/** The codes after which a resubscribe can succeed, also when the item does not say retryable. */
|
|
44
52
|
const RESUBSCRIBE_CODES = new Set(["UNAVAILABLE", "TIMEOUT", "CONNECTION_CLOSED", "ABORTED"]);
|
|
45
|
-
/** The default
|
|
53
|
+
/** The default number of resubscribe tries (EVENTS.md). */
|
|
54
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
55
|
+
/** The default backoff, in milliseconds: from 250 ms, twice as long at each try, at most 30 s, with full jitter. */
|
|
56
|
+
const DEFAULT_BACKOFF = { initialDelayMs: 250, maxDelayMs: 30_000, backoffMultiplier: 2, jitter: 1 };
|
|
57
|
+
/**
|
|
58
|
+
* The number of events that the queue of one subscriber holds when another subscriber of its
|
|
59
|
+
* shared subscription reads faster. When it is full, the oldest event goes, as at the publisher.
|
|
60
|
+
*/
|
|
61
|
+
const SUBSCRIBER_BUFFER = 1024;
|
|
62
|
+
/** The default backoff of `DEFAULT_BACKOFF`. */
|
|
46
63
|
function defaultBackoff(attempt) {
|
|
47
|
-
return
|
|
64
|
+
return exponentialBackoff([DEFAULT_BACKOFF.initialDelayMs, DEFAULT_BACKOFF.maxDelayMs, DEFAULT_BACKOFF.backoffMultiplier, DEFAULT_BACKOFF.jitter], attempt);
|
|
65
|
+
}
|
|
66
|
+
/** The delay before try `attempt`: an exponential backoff of `[base, cap, factor, jitter]`. */
|
|
67
|
+
function exponentialBackoff([base, cap, factor, jitter], attempt) {
|
|
68
|
+
return backoffDelay({ base, factor, cap, jitter: { kind: "subtract", fraction: jitter } }, attempt);
|
|
48
69
|
}
|
|
49
70
|
let subscriptions = 0;
|
|
50
71
|
/** The topics of a subscription as a list. */
|
|
@@ -69,17 +90,18 @@ function linkSignals(signals) {
|
|
|
69
90
|
},
|
|
70
91
|
};
|
|
71
92
|
}
|
|
72
|
-
/** This function waits for a time, or until the signal aborts. */
|
|
73
|
-
function pause(ms, signal) {
|
|
93
|
+
/** This function waits for a time on the clock, or until the signal aborts. */
|
|
94
|
+
function pause(ms, signal, clock) {
|
|
74
95
|
if (signal.aborted || !(ms > 0))
|
|
75
96
|
return Promise.resolve();
|
|
76
97
|
return new Promise((resolve) => {
|
|
98
|
+
let cancel;
|
|
77
99
|
const done = () => {
|
|
78
|
-
|
|
100
|
+
cancel?.();
|
|
79
101
|
signal.removeEventListener("abort", done);
|
|
80
102
|
resolve();
|
|
81
103
|
};
|
|
82
|
-
|
|
104
|
+
cancel = clock.after(ms, done);
|
|
83
105
|
signal.addEventListener("abort", done, { once: true });
|
|
84
106
|
});
|
|
85
107
|
}
|
|
@@ -99,11 +121,66 @@ function errorOfFailure(error) {
|
|
|
99
121
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
122
|
return new SubscriptionError("CONNECTION_CLOSED", message, true);
|
|
101
123
|
}
|
|
102
|
-
/** The
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
/** The error that ends a subscription when its resubscribe tries ran out. It keeps the code of the last failure. */
|
|
125
|
+
function exhaustedError(last, tries) {
|
|
126
|
+
return new SubscriptionError(last.code, `The subscription stopped after ${tries} resubscribe tries without a confirmation: ${last.message}`, last.retryable, last.reason);
|
|
127
|
+
}
|
|
128
|
+
/** The ids of the clocks of the options, for the keys of the shared subscriptions. */
|
|
129
|
+
const clockIds = new WeakMap();
|
|
130
|
+
let clocks = 0;
|
|
131
|
+
/** The id of the clock of the options: 0 without one. */
|
|
132
|
+
function clockIdOf(clock) {
|
|
133
|
+
if (!clock)
|
|
134
|
+
return 0;
|
|
135
|
+
let id = clockIds.get(clock);
|
|
136
|
+
if (id === undefined) {
|
|
137
|
+
id = ++clocks;
|
|
138
|
+
clockIds.set(clock, id);
|
|
139
|
+
}
|
|
140
|
+
return id;
|
|
141
|
+
}
|
|
142
|
+
/** The milliseconds of a backoff of the options. A plain number or a function throws a `TypeError`. */
|
|
143
|
+
function backoffSettings(backoff) {
|
|
144
|
+
if (backoff === null || typeof backoff !== "object") {
|
|
145
|
+
const given = typeof backoff === "function" ? "a function" : JSON.stringify(backoff);
|
|
146
|
+
throw new TypeError(`backoff must be an object of durations such as { initialDelay: ms(250), maxDelay: seconds(30) }, got ${given}. The backoff is no longer a function of milliseconds (EVENTS.md).`);
|
|
147
|
+
}
|
|
148
|
+
const base = backoff.initialDelay === undefined ? DEFAULT_BACKOFF.initialDelayMs : toMs(backoff.initialDelay, "backoff.initialDelay");
|
|
149
|
+
const cap = backoff.maxDelay === undefined ? DEFAULT_BACKOFF.maxDelayMs : toMs(backoff.maxDelay, "backoff.maxDelay");
|
|
150
|
+
const factor = backoff.backoffMultiplier ?? DEFAULT_BACKOFF.backoffMultiplier;
|
|
151
|
+
const jitter = Math.min(1, Math.max(0, backoff.jitter ?? DEFAULT_BACKOFF.jitter));
|
|
152
|
+
return [base, cap, factor, jitter];
|
|
153
|
+
}
|
|
154
|
+
/** The resubscribe rules of the options and the hooks. A bad option throws a `TypeError` at once. */
|
|
155
|
+
function policyOf(options, hooks) {
|
|
156
|
+
const maxAttempts = options.maxAttempts ?? hooks.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
157
|
+
if (typeof maxAttempts !== "number" || Number.isNaN(maxAttempts) || maxAttempts < 0) {
|
|
158
|
+
throw new TypeError(`maxAttempts must be a number from 0 (Infinity for no limit), got ${JSON.stringify(maxAttempts)}`);
|
|
159
|
+
}
|
|
160
|
+
let backoff;
|
|
161
|
+
let backoffKey;
|
|
162
|
+
if (options.backoff !== undefined) {
|
|
163
|
+
const settings = backoffSettings(options.backoff);
|
|
164
|
+
backoff = (attempt) => exponentialBackoff(settings, attempt);
|
|
165
|
+
backoffKey = settings;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
backoff = hooks.backoff ?? defaultBackoff;
|
|
169
|
+
backoffKey = "transport";
|
|
170
|
+
}
|
|
105
171
|
const resubscribe = hooks.resubscribe !== false && options.resubscribe !== false;
|
|
106
|
-
|
|
172
|
+
return {
|
|
173
|
+
resubscribe,
|
|
174
|
+
maxAttempts,
|
|
175
|
+
backoff,
|
|
176
|
+
clock: options.clock ?? hooks.clock ?? realClock,
|
|
177
|
+
key: [resubscribe, maxAttempts === Number.POSITIVE_INFINITY ? "Infinity" : maxAttempts, backoffKey, clockIdOf(options.clock)],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** The events of a wire subscription, with each resubscribe. The generator ends when `stop` aborts. */
|
|
181
|
+
async function* wireEvents(transport, call, stop) {
|
|
182
|
+
const { policy, hooks } = call;
|
|
183
|
+
const { signal, unlink } = linkSignals([stop, hooks.lifetime]);
|
|
107
184
|
let attempt = 0;
|
|
108
185
|
let tries = 0;
|
|
109
186
|
try {
|
|
@@ -116,10 +193,10 @@ async function* eventsOf(transport, topics, options, hooks, id) {
|
|
|
116
193
|
return;
|
|
117
194
|
}
|
|
118
195
|
const message = {
|
|
119
|
-
id: `${id}.${++tries}`,
|
|
196
|
+
id: `${call.id}.${++tries}`,
|
|
120
197
|
method: SUBSCRIBE_METHOD,
|
|
121
|
-
payload: { topics: [...topics] },
|
|
122
|
-
metadata: { ...
|
|
198
|
+
payload: { topics: [...call.topics] },
|
|
199
|
+
metadata: { ...call.metadata },
|
|
123
200
|
signal,
|
|
124
201
|
};
|
|
125
202
|
let failure;
|
|
@@ -134,18 +211,10 @@ async function* eventsOf(transport, topics, options, hooks, id) {
|
|
|
134
211
|
continue;
|
|
135
212
|
if (payload.type === "subscribed") {
|
|
136
213
|
attempt = 0;
|
|
137
|
-
|
|
214
|
+
call.onSubscribed(payload.topics);
|
|
138
215
|
}
|
|
139
216
|
else if (payload.type === "event") {
|
|
140
|
-
|
|
141
|
-
topic: payload.topic,
|
|
142
|
-
data: payload.data,
|
|
143
|
-
seq: payload.seq,
|
|
144
|
-
dropped: payload.dropped ?? 0,
|
|
145
|
-
subscription: id,
|
|
146
|
-
};
|
|
147
|
-
hooks.onEvent?.(event);
|
|
148
|
-
yield event;
|
|
217
|
+
yield payload;
|
|
149
218
|
}
|
|
150
219
|
}
|
|
151
220
|
}
|
|
@@ -158,48 +227,371 @@ async function* eventsOf(transport, topics, options, hooks, id) {
|
|
|
158
227
|
// The publisher ended the subscription
|
|
159
228
|
if (!failure)
|
|
160
229
|
return;
|
|
161
|
-
if (!resubscribe || !failure.error.retryable)
|
|
230
|
+
if (!policy.resubscribe || !failure.error.retryable || policy.maxAttempts === 0)
|
|
162
231
|
throw failure.error;
|
|
232
|
+
// The tries ran out: before, HTTP resubscribed forever
|
|
233
|
+
if (attempt >= policy.maxAttempts)
|
|
234
|
+
throw exhaustedError(failure.error, attempt);
|
|
163
235
|
attempt++;
|
|
236
|
+
call.onRetry();
|
|
164
237
|
// After a lost connection, the transport waits for its reconnect: no second backoff
|
|
165
238
|
if (!(failure.lost && hooks.ready))
|
|
166
|
-
await pause(backoff(attempt), signal);
|
|
239
|
+
await pause(policy.backoff(attempt), signal, policy.clock);
|
|
167
240
|
}
|
|
168
241
|
}
|
|
169
242
|
finally {
|
|
170
243
|
unlink();
|
|
171
244
|
}
|
|
172
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* One subscriber of a shared subscription: its queue, and its numbers. Its `seq` counts from the
|
|
248
|
+
* last event that the shared subscription got before the subscriber joined.
|
|
249
|
+
*/
|
|
250
|
+
class Subscriber {
|
|
251
|
+
id;
|
|
252
|
+
/** The `seq` of the wire subscription before the first event of this subscriber. */
|
|
253
|
+
base = 0;
|
|
254
|
+
/** The error that ends this subscriber, after the entries of its queue. */
|
|
255
|
+
failure;
|
|
256
|
+
/** This function tells the shared subscription that the subscriber waits. */
|
|
257
|
+
onWait = () => undefined;
|
|
258
|
+
queue = [];
|
|
259
|
+
events = 0;
|
|
260
|
+
waiter;
|
|
261
|
+
ended = false;
|
|
262
|
+
constructor(id) {
|
|
263
|
+
this.id = id;
|
|
264
|
+
}
|
|
265
|
+
/** True when the subscriber waits for an entry. */
|
|
266
|
+
get waiting() {
|
|
267
|
+
return this.waiter !== undefined;
|
|
268
|
+
}
|
|
269
|
+
/** This function gives the subscriber one event of the wire, with its own `seq`. */
|
|
270
|
+
pushEvent(event) {
|
|
271
|
+
this.push({ kind: "event", topic: event.topic, data: event.data, seq: event.seq - this.base, dropped: event.dropped ?? 0 });
|
|
272
|
+
}
|
|
273
|
+
/** This function gives the subscriber one entry. A full queue drops its oldest event. */
|
|
274
|
+
push(entry) {
|
|
275
|
+
if (this.ended)
|
|
276
|
+
return;
|
|
277
|
+
const waiter = this.waiter;
|
|
278
|
+
if (waiter) {
|
|
279
|
+
this.waiter = undefined;
|
|
280
|
+
waiter(entry);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (entry.kind === "event") {
|
|
284
|
+
if (this.events >= SUBSCRIBER_BUFFER) {
|
|
285
|
+
// The next event of the same wire stream tells the loss. After a confirmation, a new stream starts.
|
|
286
|
+
const index = this.queue.findIndex((queued) => queued.kind === "event");
|
|
287
|
+
const lost = this.queue.splice(index, 1)[0];
|
|
288
|
+
this.events--;
|
|
289
|
+
const next = index < this.queue.length ? this.queue[index] : entry;
|
|
290
|
+
if (next.kind === "event")
|
|
291
|
+
next.dropped += 1 + lost.dropped;
|
|
292
|
+
}
|
|
293
|
+
this.events++;
|
|
294
|
+
}
|
|
295
|
+
this.queue.push(entry);
|
|
296
|
+
}
|
|
297
|
+
/** The next entry, or undefined at the end. */
|
|
298
|
+
take() {
|
|
299
|
+
const entry = this.queue.shift();
|
|
300
|
+
if (entry) {
|
|
301
|
+
if (entry.kind === "event")
|
|
302
|
+
this.events--;
|
|
303
|
+
return Promise.resolve(entry);
|
|
304
|
+
}
|
|
305
|
+
if (this.ended)
|
|
306
|
+
return Promise.resolve(undefined);
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
this.waiter = resolve;
|
|
309
|
+
this.onWait();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* This function ends the subscriber. Without `discard`, it first gives the entries of its queue.
|
|
314
|
+
* The shared subscription ends it with its error, and an unsubscribe ends it with `discard`.
|
|
315
|
+
*/
|
|
316
|
+
end(failure, discard = false) {
|
|
317
|
+
if (this.ended)
|
|
318
|
+
return;
|
|
319
|
+
this.ended = true;
|
|
320
|
+
this.failure = failure;
|
|
321
|
+
if (discard) {
|
|
322
|
+
this.queue.length = 0;
|
|
323
|
+
this.events = 0;
|
|
324
|
+
}
|
|
325
|
+
const waiter = this.waiter;
|
|
326
|
+
this.waiter = undefined;
|
|
327
|
+
waiter?.(undefined);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* One wire subscription and its subscribers. It reads the next item of the wire only when a
|
|
332
|
+
* subscriber waits, so a single slow subscriber holds the credit, as before the share. It ends
|
|
333
|
+
* when its last subscriber leaves, or with the end of the wire subscription for each subscriber.
|
|
334
|
+
*/
|
|
335
|
+
class SharedSubscription {
|
|
336
|
+
shares;
|
|
337
|
+
key;
|
|
338
|
+
transport;
|
|
339
|
+
call;
|
|
340
|
+
members = new Set();
|
|
341
|
+
stop = new AbortController();
|
|
342
|
+
/** The topics of the current confirmation, or undefined before it and during a resubscribe. */
|
|
343
|
+
confirmed;
|
|
344
|
+
/** The `seq` of the last event of the wire. */
|
|
345
|
+
lastSeq = 0;
|
|
346
|
+
demand;
|
|
347
|
+
started = false;
|
|
348
|
+
finished = false;
|
|
349
|
+
constructor(shares, key, transport, call) {
|
|
350
|
+
this.shares = shares;
|
|
351
|
+
this.key = key;
|
|
352
|
+
this.transport = transport;
|
|
353
|
+
this.call = call;
|
|
354
|
+
}
|
|
355
|
+
/** True while a new subscriber can join. */
|
|
356
|
+
get open() {
|
|
357
|
+
return !this.finished && !this.stop.signal.aborted;
|
|
358
|
+
}
|
|
359
|
+
/** This function adds a subscriber. On a confirmed subscription, it gets the confirmation at once. */
|
|
360
|
+
join(subscriber) {
|
|
361
|
+
this.members.add(subscriber);
|
|
362
|
+
subscriber.base = this.lastSeq;
|
|
363
|
+
subscriber.onWait = () => this.wake();
|
|
364
|
+
if (this.confirmed)
|
|
365
|
+
subscriber.push({ kind: "subscribed", topics: this.confirmed });
|
|
366
|
+
if (!this.started) {
|
|
367
|
+
this.started = true;
|
|
368
|
+
void this.run();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** This function removes a subscriber. The last one ends the wire subscription: the transport sends the cancel. */
|
|
372
|
+
leave(subscriber) {
|
|
373
|
+
if (!this.members.delete(subscriber) || this.members.size > 0)
|
|
374
|
+
return;
|
|
375
|
+
this.forget();
|
|
376
|
+
this.stop.abort();
|
|
377
|
+
this.wake();
|
|
378
|
+
}
|
|
379
|
+
wake() {
|
|
380
|
+
const demand = this.demand;
|
|
381
|
+
this.demand = undefined;
|
|
382
|
+
demand?.();
|
|
383
|
+
}
|
|
384
|
+
/** This function removes the subscription from the shares of its transport. */
|
|
385
|
+
forget() {
|
|
386
|
+
if (this.shares?.get(this.key) === this)
|
|
387
|
+
this.shares.delete(this.key);
|
|
388
|
+
}
|
|
389
|
+
stopped() {
|
|
390
|
+
return this.stop.signal.aborted || this.call.hooks.lifetime?.aborted === true;
|
|
391
|
+
}
|
|
392
|
+
wanted() {
|
|
393
|
+
for (const member of this.members)
|
|
394
|
+
if (member.waiting)
|
|
395
|
+
return true;
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
async run() {
|
|
399
|
+
const events = wireEvents(this.transport, {
|
|
400
|
+
...this.call,
|
|
401
|
+
onSubscribed: (topics) => {
|
|
402
|
+
this.confirmed = topics;
|
|
403
|
+
this.lastSeq = 0;
|
|
404
|
+
for (const member of this.members) {
|
|
405
|
+
member.base = 0;
|
|
406
|
+
member.push({ kind: "subscribed", topics });
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
onRetry: () => {
|
|
410
|
+
this.confirmed = undefined;
|
|
411
|
+
},
|
|
412
|
+
}, this.stop.signal);
|
|
413
|
+
// The end of the transport ends the wait for a subscriber too
|
|
414
|
+
const lifetime = this.call.hooks.lifetime;
|
|
415
|
+
const onLifetime = () => this.wake();
|
|
416
|
+
lifetime?.addEventListener("abort", onLifetime, { once: true });
|
|
417
|
+
let failure;
|
|
418
|
+
try {
|
|
419
|
+
for (;;) {
|
|
420
|
+
while (!this.stopped() && !this.wanted()) {
|
|
421
|
+
await new Promise((resolve) => {
|
|
422
|
+
this.demand = resolve;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (this.stopped())
|
|
426
|
+
break;
|
|
427
|
+
const result = await events.next();
|
|
428
|
+
if (result.done)
|
|
429
|
+
break;
|
|
430
|
+
this.lastSeq = result.value.seq;
|
|
431
|
+
for (const member of this.members)
|
|
432
|
+
member.pushEvent(result.value);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
failure = errorOfFailure(error);
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
this.finished = true;
|
|
440
|
+
this.forget();
|
|
441
|
+
lifetime?.removeEventListener("abort", onLifetime);
|
|
442
|
+
await events.return(undefined).catch(() => undefined);
|
|
443
|
+
}
|
|
444
|
+
// The end of the wire subscription goes to each subscriber
|
|
445
|
+
for (const member of [...this.members])
|
|
446
|
+
member.end(failure);
|
|
447
|
+
this.members.clear();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** The shared subscriptions of each transport, by key. */
|
|
451
|
+
const sharesOf = new WeakMap();
|
|
452
|
+
/** The key of a shared subscription: the set of topics, the metadata and the resubscribe rules. */
|
|
453
|
+
function keyOf(topics, metadata, policy) {
|
|
454
|
+
try {
|
|
455
|
+
return JSON.stringify([[...new Set(topics)].sort(), metadata, policy.key]);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Metadata that is not JSON: the subscription is not shared
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/** The shared subscription of a new subscriber: an open one with the same key, or a new one with the id of the subscriber. */
|
|
463
|
+
function sharedFor(transport, topics, options, hooks, policy, id) {
|
|
464
|
+
const metadata = { ...(options.metadata ?? {}) };
|
|
465
|
+
const key = keyOf(topics, metadata, policy);
|
|
466
|
+
let shares = sharesOf.get(transport);
|
|
467
|
+
if (!shares) {
|
|
468
|
+
shares = new Map();
|
|
469
|
+
sharesOf.set(transport, shares);
|
|
470
|
+
}
|
|
471
|
+
const existing = key === undefined ? undefined : shares.get(key);
|
|
472
|
+
if (existing?.open)
|
|
473
|
+
return existing;
|
|
474
|
+
const shared = new SharedSubscription(key === undefined ? undefined : shares, key ?? "", transport, { topics, metadata, policy, hooks, id });
|
|
475
|
+
if (key !== undefined)
|
|
476
|
+
shares.set(key, shared);
|
|
477
|
+
return shared;
|
|
478
|
+
}
|
|
479
|
+
/** The event that a subscriber gets from an entry of its queue. */
|
|
480
|
+
function remoteEventOf(entry, id) {
|
|
481
|
+
return { topic: entry.topic, data: entry.data, seq: entry.seq, dropped: entry.dropped, subscription: id };
|
|
482
|
+
}
|
|
483
|
+
/** This function calls `onSubscribed`. An error of it goes to `console.error`, and the subscription goes on. */
|
|
484
|
+
function confirmTo(options, topics) {
|
|
485
|
+
try {
|
|
486
|
+
options.onSubscribed?.(topics);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.error("[events] The onSubscribed handler failed:", error);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** This function joins a new subscriber to its shared subscription, and gives the function that leaves it. */
|
|
493
|
+
function attach(subscriber, transport, topics, options, hooks, policy) {
|
|
494
|
+
if (options.signal?.aborted) {
|
|
495
|
+
subscriber.end(undefined, true);
|
|
496
|
+
return () => undefined;
|
|
497
|
+
}
|
|
498
|
+
const shared = sharedFor(transport, topics, options, hooks, policy, subscriber.id);
|
|
499
|
+
let left = false;
|
|
500
|
+
const leave = () => {
|
|
501
|
+
if (left)
|
|
502
|
+
return;
|
|
503
|
+
left = true;
|
|
504
|
+
options.signal?.removeEventListener("abort", leave);
|
|
505
|
+
shared.leave(subscriber);
|
|
506
|
+
subscriber.end(undefined, true);
|
|
507
|
+
};
|
|
508
|
+
options.signal?.addEventListener("abort", leave, { once: true });
|
|
509
|
+
shared.join(subscriber);
|
|
510
|
+
return leave;
|
|
511
|
+
}
|
|
173
512
|
/**
|
|
174
513
|
* This function gives the events of a subscription to topics of the bus of the other side. The
|
|
175
|
-
* iteration
|
|
176
|
-
*
|
|
514
|
+
* iteration joins the subscription, so it sends the call when no subscriber shares one. A `break`
|
|
515
|
+
* of the reader, or the abort of the signal, leaves it: the last subscriber sends the cancel. An
|
|
516
|
+
* error that ends the subscription ends the iteration with a `SubscriptionError`.
|
|
177
517
|
*
|
|
178
518
|
* @param transport - The transport to the publisher: HTTP, WebSocket, a message port, local
|
|
179
519
|
* @param topics - One topic, or a list of 1 to 64 topics
|
|
180
|
-
* @param options - The signal, the resubscribe, the
|
|
520
|
+
* @param options - The signal, the resubscribe rules, the clock and the confirmation handler
|
|
181
521
|
* @param hooks - The parts that a transport adds (its connection, its backoff, its `onEvent`)
|
|
522
|
+
* @throws TypeError - When `backoff` or `maxAttempts` is not valid, for example a plain number of milliseconds
|
|
182
523
|
*/
|
|
183
524
|
export function remoteEvents(transport, topics, options = {}, hooks = {}) {
|
|
184
525
|
const list = topicList(topics);
|
|
526
|
+
const policy = policyOf(options, hooks);
|
|
185
527
|
return {
|
|
186
|
-
[Symbol.asyncIterator]: () =>
|
|
528
|
+
[Symbol.asyncIterator]: () => {
|
|
529
|
+
const subscriber = new Subscriber(`events-${++subscriptions}`);
|
|
530
|
+
let leave;
|
|
531
|
+
let stopped = false;
|
|
532
|
+
const stop = () => {
|
|
533
|
+
stopped = true;
|
|
534
|
+
if (leave)
|
|
535
|
+
leave();
|
|
536
|
+
else
|
|
537
|
+
subscriber.end(undefined, true);
|
|
538
|
+
};
|
|
539
|
+
const iterator = {
|
|
540
|
+
async next() {
|
|
541
|
+
if (!leave && !stopped)
|
|
542
|
+
leave = attach(subscriber, transport, list, options, hooks, policy);
|
|
543
|
+
for (;;) {
|
|
544
|
+
const entry = await subscriber.take();
|
|
545
|
+
if (!entry) {
|
|
546
|
+
const failure = subscriber.failure;
|
|
547
|
+
subscriber.failure = undefined;
|
|
548
|
+
stop();
|
|
549
|
+
if (failure)
|
|
550
|
+
throw failure;
|
|
551
|
+
return { value: undefined, done: true };
|
|
552
|
+
}
|
|
553
|
+
if (entry.kind === "subscribed") {
|
|
554
|
+
confirmTo(options, entry.topics);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const event = remoteEventOf(entry, subscriber.id);
|
|
558
|
+
hooks.onEvent?.(event);
|
|
559
|
+
return { value: event, done: false };
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
async return() {
|
|
563
|
+
stop();
|
|
564
|
+
return { value: undefined, done: true };
|
|
565
|
+
},
|
|
566
|
+
async throw(error) {
|
|
567
|
+
stop();
|
|
568
|
+
throw error;
|
|
569
|
+
},
|
|
570
|
+
[Symbol.asyncIterator]() {
|
|
571
|
+
return iterator;
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
return iterator;
|
|
575
|
+
},
|
|
187
576
|
};
|
|
188
577
|
}
|
|
189
578
|
/**
|
|
190
579
|
* This function subscribes to topics of the bus of the other side, and calls the handler for each
|
|
191
580
|
* event in order. It gives the unsubscribe function, with the `ready` promise of the first
|
|
192
581
|
* confirmation. An error of the handler goes to `console.error`, and the subscription goes on.
|
|
582
|
+
* The subscribers of the same topics with the same options on one transport share one call.
|
|
193
583
|
*
|
|
194
584
|
* @param transport - The transport to the publisher
|
|
195
585
|
* @param topics - One topic, or a list of 1 to 64 topics
|
|
196
586
|
* @param handler - The handler of each event: it gets the data and the event
|
|
197
587
|
* @param options - The options of `remoteEvents()`, and the `onError` handler of the end
|
|
198
588
|
* @param hooks - The parts that a transport adds
|
|
589
|
+
* @throws TypeError - When `backoff` or `maxAttempts` is not valid, for example a plain number of milliseconds
|
|
199
590
|
*/
|
|
200
591
|
export function subscribeRemote(transport, topics, handler, options = {}, hooks = {}) {
|
|
201
|
-
const
|
|
202
|
-
const
|
|
592
|
+
const list = topicList(topics);
|
|
593
|
+
const policy = policyOf(options, hooks);
|
|
594
|
+
const subscriber = new Subscriber(`events-${++subscriptions}`);
|
|
203
595
|
let confirmed = false;
|
|
204
596
|
let confirm = () => undefined;
|
|
205
597
|
let refuse = () => undefined;
|
|
@@ -209,21 +601,23 @@ export function subscribeRemote(transport, topics, handler, options = {}, hooks
|
|
|
209
601
|
});
|
|
210
602
|
// A caller that does not wait for `ready` gets no unhandled rejection
|
|
211
603
|
ready.catch(() => undefined);
|
|
212
|
-
const
|
|
213
|
-
const settings = {
|
|
214
|
-
...options,
|
|
215
|
-
signal,
|
|
216
|
-
onSubscribed: (list) => {
|
|
217
|
-
if (!confirmed) {
|
|
218
|
-
confirmed = true;
|
|
219
|
-
confirm();
|
|
220
|
-
}
|
|
221
|
-
options.onSubscribed?.(list);
|
|
222
|
-
},
|
|
223
|
-
};
|
|
604
|
+
const leave = attach(subscriber, transport, list, options, hooks, policy);
|
|
224
605
|
void (async () => {
|
|
225
606
|
try {
|
|
226
|
-
for
|
|
607
|
+
for (;;) {
|
|
608
|
+
const entry = await subscriber.take();
|
|
609
|
+
if (!entry)
|
|
610
|
+
break;
|
|
611
|
+
if (entry.kind === "subscribed") {
|
|
612
|
+
if (!confirmed) {
|
|
613
|
+
confirmed = true;
|
|
614
|
+
confirm();
|
|
615
|
+
}
|
|
616
|
+
confirmTo(options, entry.topics);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const event = remoteEventOf(entry, subscriber.id);
|
|
620
|
+
hooks.onEvent?.(event);
|
|
227
621
|
try {
|
|
228
622
|
await handler(event.data, event);
|
|
229
623
|
}
|
|
@@ -231,20 +625,16 @@ export function subscribeRemote(transport, topics, handler, options = {}, hooks
|
|
|
231
625
|
console.error(`[events] The handler of ${event.topic} failed:`, error);
|
|
232
626
|
}
|
|
233
627
|
}
|
|
234
|
-
if (!confirmed)
|
|
235
|
-
refuse(new SubscriptionError("ABORTED", "The subscription ended before its confirmation", false));
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
const failure = errorOfFailure(error);
|
|
239
|
-
if (!confirmed)
|
|
240
|
-
refuse(failure);
|
|
241
|
-
options.onError?.(failure);
|
|
242
628
|
}
|
|
243
629
|
finally {
|
|
244
|
-
|
|
630
|
+
leave();
|
|
245
631
|
}
|
|
632
|
+
const failure = subscriber.failure;
|
|
633
|
+
if (!confirmed)
|
|
634
|
+
refuse(failure ?? new SubscriptionError("ABORTED", "The subscription ended before its confirmation", false));
|
|
635
|
+
if (failure)
|
|
636
|
+
options.onError?.(failure);
|
|
246
637
|
})();
|
|
247
|
-
|
|
248
|
-
return Object.assign(unsubscribe, { ready, id });
|
|
638
|
+
return Object.assign(leave, { ready, id: subscriber.id });
|
|
249
639
|
}
|
|
250
640
|
//# sourceMappingURL=remote.js.map
|