@procedures/client 0.7.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 +49 -0
- package/README.md +20 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/transport.d.ts +6 -0
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +10 -1
- package/dist/adapters/http/server/transport.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 +4 -4
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- 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/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 +37 -3
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +86 -4
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/timers.d.ts +5 -0
- package/dist/timers.d.ts.map +1 -1
- package/dist/timers.js +3 -1
- package/dist/timers.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/http/server/transport.ts +11 -1
- package/src/adapters/websocket/client/transport.ts +9 -3
- package/src/adapters/websocket/server/transport.ts +23 -2
- package/src/browser.ts +6 -0
- 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/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 +103 -6
- package/src/timers.ts +8 -1
package/src/events/remote.ts
CHANGED
|
@@ -10,15 +10,23 @@
|
|
|
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
|
|
|
20
26
|
import type { Message, Method, ResponseItem, Transport } from "../client/types.js";
|
|
21
27
|
import { backoffDelay } from "@mark1russell7/service/backoff";
|
|
28
|
+
import { realClock, type Clock } from "../clock.js";
|
|
29
|
+
import { toMs, type Duration } from "../duration.js";
|
|
22
30
|
|
|
23
31
|
/** The path of the procedure of a subscription. */
|
|
24
32
|
export const EVENTS_SUBSCRIBE_PATH: readonly ["events", "subscribe"] = Object.freeze(["events", "subscribe"] as const);
|
|
@@ -54,9 +62,12 @@ export interface RemoteEvent<T = unknown> {
|
|
|
54
62
|
readonly topic: string;
|
|
55
63
|
/** The data of the event, after a trip as JSON. */
|
|
56
64
|
readonly data: T;
|
|
57
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The number of the event for this subscriber, from 1. A resubscribe starts again at 1. A
|
|
67
|
+
* subscriber that joins a shared subscription also starts at 1.
|
|
68
|
+
*/
|
|
58
69
|
readonly seq: number;
|
|
59
|
-
/** The number of events that
|
|
70
|
+
/** The number of events that this subscriber lost immediately before this one: 0 without a loss. */
|
|
60
71
|
readonly dropped: number;
|
|
61
72
|
/** The id of the subscription. It stays the same after a resubscribe. */
|
|
62
73
|
readonly subscription: string;
|
|
@@ -64,10 +75,39 @@ export interface RemoteEvent<T = unknown> {
|
|
|
64
75
|
|
|
65
76
|
/**
|
|
66
77
|
* The handler of the events of a subscription. When it gives a promise, the subscriber waits for
|
|
67
|
-
* it before the next event
|
|
78
|
+
* it before the next event. A slow handler holds the credit of its subscription only when no
|
|
79
|
+
* other subscriber of its shared subscription waits.
|
|
68
80
|
*/
|
|
69
81
|
export type RemoteEventHandler<T = unknown> = (data: T, event: RemoteEvent<T>) => void | Promise<void>;
|
|
70
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The backoff before each resubscribe try: an exponential backoff with jitter. The names are the
|
|
85
|
+
* names of the `reconnect` options of `WebSocketTransport`. Each time is a `Duration`.
|
|
86
|
+
*/
|
|
87
|
+
export interface ResubscribeBackoff {
|
|
88
|
+
/**
|
|
89
|
+
* The delay before the first resubscribe try, for example `ms(250)`.
|
|
90
|
+
* @default ms(250)
|
|
91
|
+
*/
|
|
92
|
+
initialDelay?: Duration | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* The longest delay before a resubscribe try.
|
|
95
|
+
* @default seconds(30)
|
|
96
|
+
*/
|
|
97
|
+
maxDelay?: Duration | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* The factor of the delay from one try to the next.
|
|
100
|
+
* @default 2
|
|
101
|
+
*/
|
|
102
|
+
backoffMultiplier?: number | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* The random part of each delay, from 0 to 1. With 1 (full jitter), the delay is a random time
|
|
105
|
+
* from 0 to the backoff. 0 gives the backoff exactly.
|
|
106
|
+
* @default 1
|
|
107
|
+
*/
|
|
108
|
+
jitter?: number | undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
71
111
|
/** The options of a subscription. */
|
|
72
112
|
export interface RemoteEventsOptions {
|
|
73
113
|
/** When this signal aborts, the subscription ends: the transport sends a cancel. */
|
|
@@ -79,11 +119,24 @@ export interface RemoteEventsOptions {
|
|
|
79
119
|
*/
|
|
80
120
|
resubscribe?: boolean | undefined;
|
|
81
121
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
122
|
+
* The number of resubscribe tries after a failure, one after the other without a confirmation.
|
|
123
|
+
* Each confirmation starts the count again. When the tries run out, the subscription ends with a
|
|
124
|
+
* `SubscriptionError` that has the code of the last failure. 0 is the same as `resubscribe: false`.
|
|
125
|
+
* The WebSocket transport uses the `reconnect.maxAttempts` of its options (default `Infinity`).
|
|
126
|
+
* @default 10 (with the default backoff, at most approximately two minutes of waits)
|
|
127
|
+
*/
|
|
128
|
+
maxAttempts?: number | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* The backoff before each resubscribe try. A plain number or a function throws a `TypeError`.
|
|
131
|
+
* The WebSocket transport uses its reconnect backoff when the options give none.
|
|
132
|
+
* @default { initialDelay: ms(250), maxDelay: seconds(30), backoffMultiplier: 2, jitter: 1 }
|
|
133
|
+
*/
|
|
134
|
+
backoff?: ResubscribeBackoff | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* The clock of the backoff. A test gives the `fakeClock` of `@mark1russell7/service/testing`.
|
|
137
|
+
* The default is the clock of the platform. The WebSocket transport uses its own clock.
|
|
85
138
|
*/
|
|
86
|
-
|
|
139
|
+
clock?: Clock | undefined;
|
|
87
140
|
/** This function gets the topics of each confirmation: the first one, and one after each resubscribe. */
|
|
88
141
|
onSubscribed?: ((topics: readonly string[]) => void) | undefined;
|
|
89
142
|
/** The metadata of the call, for example a trace. */
|
|
@@ -94,8 +147,9 @@ export interface RemoteEventsOptions {
|
|
|
94
147
|
export interface SubscribeOptions extends RemoteEventsOptions {
|
|
95
148
|
/**
|
|
96
149
|
* This function gets the error that ends the subscription: a refusal (`TOPIC_NOT_FOUND`,
|
|
97
|
-
* `FORBIDDEN`), an older publisher (`NOT_FOUND`),
|
|
98
|
-
*
|
|
150
|
+
* `FORBIDDEN`), an older publisher (`NOT_FOUND`), a topic that the host cleared
|
|
151
|
+
* (`TOPIC_CLEARED`), a transport that closed, or the last failure after `maxAttempts` tries. An
|
|
152
|
+
* unsubscribe is not an error.
|
|
99
153
|
*/
|
|
100
154
|
onError?: ((error: SubscriptionError) => void) | undefined;
|
|
101
155
|
}
|
|
@@ -131,8 +185,9 @@ export class SubscriptionError extends Error {
|
|
|
131
185
|
|
|
132
186
|
/**
|
|
133
187
|
* The parts that a transport adds to its subscriptions. The WebSocket transport waits for its
|
|
134
|
-
* connection, uses its reconnect backoff,
|
|
135
|
-
* event to its `onEvent` option.
|
|
188
|
+
* connection, uses its reconnect backoff, its `maxAttempts` and its clock, ends its subscriptions
|
|
189
|
+
* at `close()`, and gives each event to its `onEvent` option. A shared subscription uses the
|
|
190
|
+
* hooks of its first subscriber.
|
|
136
191
|
*/
|
|
137
192
|
export interface SubscriptionHooks {
|
|
138
193
|
/** When this signal aborts, each subscription ends without an error. */
|
|
@@ -142,8 +197,12 @@ export interface SubscriptionHooks {
|
|
|
142
197
|
* closed. After a lost connection, the subscriber waits for it, not for a backoff.
|
|
143
198
|
*/
|
|
144
199
|
readonly ready?: ((signal: AbortSignal) => Promise<void>) | undefined;
|
|
145
|
-
/** The backoff, when the options give none. */
|
|
200
|
+
/** The backoff in milliseconds before resubscribe try `attempt` (from 1), when the options give none. */
|
|
146
201
|
readonly backoff?: ((attempt: number) => number) | undefined;
|
|
202
|
+
/** The number of resubscribe tries, when the options give none. */
|
|
203
|
+
readonly maxAttempts?: number | undefined;
|
|
204
|
+
/** The clock of the backoff, when the options give none. */
|
|
205
|
+
readonly clock?: Clock | undefined;
|
|
147
206
|
/** This function gets each event of each subscription. */
|
|
148
207
|
readonly onEvent?: ((event: RemoteEvent) => void) | undefined;
|
|
149
208
|
/** False: no resubscribe, whatever the options say (a server cannot reach a closed connection). */
|
|
@@ -156,9 +215,26 @@ export type EventTransport = Pick<Transport, "send">;
|
|
|
156
215
|
/** The codes after which a resubscribe can succeed, also when the item does not say retryable. */
|
|
157
216
|
const RESUBSCRIBE_CODES: ReadonlySet<string> = new Set(["UNAVAILABLE", "TIMEOUT", "CONNECTION_CLOSED", "ABORTED"]);
|
|
158
217
|
|
|
159
|
-
/** The default
|
|
218
|
+
/** The default number of resubscribe tries (EVENTS.md). */
|
|
219
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
220
|
+
|
|
221
|
+
/** The default backoff, in milliseconds: from 250 ms, twice as long at each try, at most 30 s, with full jitter. */
|
|
222
|
+
const DEFAULT_BACKOFF = { initialDelayMs: 250, maxDelayMs: 30_000, backoffMultiplier: 2, jitter: 1 } as const;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The number of events that the queue of one subscriber holds when another subscriber of its
|
|
226
|
+
* shared subscription reads faster. When it is full, the oldest event goes, as at the publisher.
|
|
227
|
+
*/
|
|
228
|
+
const SUBSCRIBER_BUFFER = 1024;
|
|
229
|
+
|
|
230
|
+
/** The default backoff of `DEFAULT_BACKOFF`. */
|
|
160
231
|
function defaultBackoff(attempt: number): number {
|
|
161
|
-
return
|
|
232
|
+
return exponentialBackoff([DEFAULT_BACKOFF.initialDelayMs, DEFAULT_BACKOFF.maxDelayMs, DEFAULT_BACKOFF.backoffMultiplier, DEFAULT_BACKOFF.jitter], attempt);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The delay before try `attempt`: an exponential backoff of `[base, cap, factor, jitter]`. */
|
|
236
|
+
function exponentialBackoff([base, cap, factor, jitter]: readonly [number, number, number, number], attempt: number): number {
|
|
237
|
+
return backoffDelay({ base, factor, cap, jitter: { kind: "subtract", fraction: jitter } }, attempt);
|
|
162
238
|
}
|
|
163
239
|
|
|
164
240
|
let subscriptions = 0;
|
|
@@ -183,16 +259,17 @@ function linkSignals(signals: ReadonlyArray<AbortSignal | undefined>): { signal:
|
|
|
183
259
|
};
|
|
184
260
|
}
|
|
185
261
|
|
|
186
|
-
/** This function waits for a time, or until the signal aborts. */
|
|
187
|
-
function pause(ms: number, signal: AbortSignal): Promise<void> {
|
|
262
|
+
/** This function waits for a time on the clock, or until the signal aborts. */
|
|
263
|
+
function pause(ms: number, signal: AbortSignal, clock: Clock): Promise<void> {
|
|
188
264
|
if (signal.aborted || !(ms > 0)) return Promise.resolve();
|
|
189
265
|
return new Promise((resolve) => {
|
|
266
|
+
let cancel: (() => void) | undefined;
|
|
190
267
|
const done = (): void => {
|
|
191
|
-
|
|
268
|
+
cancel?.();
|
|
192
269
|
signal.removeEventListener("abort", done);
|
|
193
270
|
resolve();
|
|
194
271
|
};
|
|
195
|
-
|
|
272
|
+
cancel = clock.after(ms, done);
|
|
196
273
|
signal.addEventListener("abort", done, { once: true });
|
|
197
274
|
});
|
|
198
275
|
}
|
|
@@ -213,17 +290,112 @@ function errorOfFailure(error: unknown): SubscriptionError {
|
|
|
213
290
|
return new SubscriptionError("CONNECTION_CLOSED", message, true);
|
|
214
291
|
}
|
|
215
292
|
|
|
216
|
-
/** The
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
|
|
293
|
+
/** The error that ends a subscription when its resubscribe tries ran out. It keeps the code of the last failure. */
|
|
294
|
+
function exhaustedError(last: SubscriptionError, tries: number): SubscriptionError {
|
|
295
|
+
return new SubscriptionError(
|
|
296
|
+
last.code,
|
|
297
|
+
`The subscription stopped after ${tries} resubscribe tries without a confirmation: ${last.message}`,
|
|
298
|
+
last.retryable,
|
|
299
|
+
last.reason,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// The resubscribe rules
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
/** The resubscribe rules of a subscription, from its options and the hooks of its transport. */
|
|
308
|
+
interface ResubscribePolicy {
|
|
309
|
+
readonly resubscribe: boolean;
|
|
310
|
+
readonly maxAttempts: number;
|
|
311
|
+
/** The delay in milliseconds before try `attempt` (from 1). */
|
|
312
|
+
readonly backoff: (attempt: number) => number;
|
|
313
|
+
readonly clock: Clock;
|
|
314
|
+
/** The part of the key of a shared subscription that these rules give. */
|
|
315
|
+
readonly key: unknown;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The ids of the clocks of the options, for the keys of the shared subscriptions. */
|
|
319
|
+
const clockIds = new WeakMap<Clock, number>();
|
|
320
|
+
let clocks = 0;
|
|
321
|
+
|
|
322
|
+
/** The id of the clock of the options: 0 without one. */
|
|
323
|
+
function clockIdOf(clock: Clock | undefined): number {
|
|
324
|
+
if (!clock) return 0;
|
|
325
|
+
let id = clockIds.get(clock);
|
|
326
|
+
if (id === undefined) {
|
|
327
|
+
id = ++clocks;
|
|
328
|
+
clockIds.set(clock, id);
|
|
329
|
+
}
|
|
330
|
+
return id;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The milliseconds of a backoff of the options. A plain number or a function throws a `TypeError`. */
|
|
334
|
+
function backoffSettings(backoff: ResubscribeBackoff): [number, number, number, number] {
|
|
335
|
+
if (backoff === null || typeof backoff !== "object") {
|
|
336
|
+
const given = typeof backoff === "function" ? "a function" : JSON.stringify(backoff);
|
|
337
|
+
throw new TypeError(
|
|
338
|
+
`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).`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const base = backoff.initialDelay === undefined ? DEFAULT_BACKOFF.initialDelayMs : toMs(backoff.initialDelay, "backoff.initialDelay");
|
|
342
|
+
const cap = backoff.maxDelay === undefined ? DEFAULT_BACKOFF.maxDelayMs : toMs(backoff.maxDelay, "backoff.maxDelay");
|
|
343
|
+
const factor = backoff.backoffMultiplier ?? DEFAULT_BACKOFF.backoffMultiplier;
|
|
344
|
+
const jitter = Math.min(1, Math.max(0, backoff.jitter ?? DEFAULT_BACKOFF.jitter));
|
|
345
|
+
return [base, cap, factor, jitter];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The resubscribe rules of the options and the hooks. A bad option throws a `TypeError` at once. */
|
|
349
|
+
function policyOf(options: RemoteEventsOptions, hooks: SubscriptionHooks): ResubscribePolicy {
|
|
350
|
+
const maxAttempts = options.maxAttempts ?? hooks.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
351
|
+
if (typeof maxAttempts !== "number" || Number.isNaN(maxAttempts) || maxAttempts < 0) {
|
|
352
|
+
throw new TypeError(`maxAttempts must be a number from 0 (Infinity for no limit), got ${JSON.stringify(maxAttempts)}`);
|
|
353
|
+
}
|
|
354
|
+
let backoff: (attempt: number) => number;
|
|
355
|
+
let backoffKey: unknown;
|
|
356
|
+
if (options.backoff !== undefined) {
|
|
357
|
+
const settings = backoffSettings(options.backoff);
|
|
358
|
+
backoff = (attempt) => exponentialBackoff(settings, attempt);
|
|
359
|
+
backoffKey = settings;
|
|
360
|
+
} else {
|
|
361
|
+
backoff = hooks.backoff ?? defaultBackoff;
|
|
362
|
+
backoffKey = "transport";
|
|
363
|
+
}
|
|
225
364
|
const resubscribe = hooks.resubscribe !== false && options.resubscribe !== false;
|
|
226
|
-
|
|
365
|
+
return {
|
|
366
|
+
resubscribe,
|
|
367
|
+
maxAttempts,
|
|
368
|
+
backoff,
|
|
369
|
+
clock: options.clock ?? hooks.clock ?? realClock,
|
|
370
|
+
key: [resubscribe, maxAttempts === Number.POSITIVE_INFINITY ? "Infinity" : maxAttempts, backoffKey, clockIdOf(options.clock)],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// =============================================================================
|
|
375
|
+
// The wire subscription
|
|
376
|
+
// =============================================================================
|
|
377
|
+
|
|
378
|
+
/** An event item of the stream of `events.subscribe`. */
|
|
379
|
+
type WireEvent = Extract<EventItem, { type: "event" }>;
|
|
380
|
+
|
|
381
|
+
/** What the call of a wire subscription needs besides the transport. */
|
|
382
|
+
interface WireCall {
|
|
383
|
+
readonly topics: readonly string[];
|
|
384
|
+
readonly metadata: Record<string, unknown>;
|
|
385
|
+
readonly policy: ResubscribePolicy;
|
|
386
|
+
readonly hooks: SubscriptionHooks;
|
|
387
|
+
/** The id of the subscription: the prefix of the id of each call. */
|
|
388
|
+
readonly id: string;
|
|
389
|
+
/** This function gets the topics of each `subscribed` item. */
|
|
390
|
+
readonly onSubscribed: (topics: readonly string[]) => void;
|
|
391
|
+
/** This function runs before each resubscribe try. */
|
|
392
|
+
readonly onRetry: () => void;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The events of a wire subscription, with each resubscribe. The generator ends when `stop` aborts. */
|
|
396
|
+
async function* wireEvents(transport: EventTransport, call: WireCall, stop: AbortSignal): AsyncGenerator<WireEvent, void, undefined> {
|
|
397
|
+
const { policy, hooks } = call;
|
|
398
|
+
const { signal, unlink } = linkSignals([stop, hooks.lifetime]);
|
|
227
399
|
let attempt = 0;
|
|
228
400
|
let tries = 0;
|
|
229
401
|
try {
|
|
@@ -234,10 +406,10 @@ async function* eventsOf<T>(
|
|
|
234
406
|
if (signal.aborted) return;
|
|
235
407
|
}
|
|
236
408
|
const message: Message<SubscribeInput> = {
|
|
237
|
-
id: `${id}.${++tries}`,
|
|
409
|
+
id: `${call.id}.${++tries}`,
|
|
238
410
|
method: SUBSCRIBE_METHOD,
|
|
239
|
-
payload: { topics: [...topics] },
|
|
240
|
-
metadata: { ...
|
|
411
|
+
payload: { topics: [...call.topics] },
|
|
412
|
+
metadata: { ...call.metadata },
|
|
241
413
|
signal,
|
|
242
414
|
};
|
|
243
415
|
let failure: { error: SubscriptionError; lost: boolean } | undefined;
|
|
@@ -251,17 +423,9 @@ async function* eventsOf<T>(
|
|
|
251
423
|
if (payload === null || typeof payload !== "object") continue;
|
|
252
424
|
if (payload.type === "subscribed") {
|
|
253
425
|
attempt = 0;
|
|
254
|
-
|
|
426
|
+
call.onSubscribed(payload.topics);
|
|
255
427
|
} else if (payload.type === "event") {
|
|
256
|
-
|
|
257
|
-
topic: payload.topic,
|
|
258
|
-
data: payload.data as T,
|
|
259
|
-
seq: payload.seq,
|
|
260
|
-
dropped: payload.dropped ?? 0,
|
|
261
|
-
subscription: id,
|
|
262
|
-
};
|
|
263
|
-
hooks.onEvent?.(event as RemoteEvent);
|
|
264
|
-
yield event;
|
|
428
|
+
yield payload;
|
|
265
429
|
}
|
|
266
430
|
}
|
|
267
431
|
} catch (error) {
|
|
@@ -271,25 +435,318 @@ async function* eventsOf<T>(
|
|
|
271
435
|
if (signal.aborted) return;
|
|
272
436
|
// The publisher ended the subscription
|
|
273
437
|
if (!failure) return;
|
|
274
|
-
if (!resubscribe || !failure.error.retryable) throw failure.error;
|
|
438
|
+
if (!policy.resubscribe || !failure.error.retryable || policy.maxAttempts === 0) throw failure.error;
|
|
439
|
+
// The tries ran out: before, HTTP resubscribed forever
|
|
440
|
+
if (attempt >= policy.maxAttempts) throw exhaustedError(failure.error, attempt);
|
|
275
441
|
attempt++;
|
|
442
|
+
call.onRetry();
|
|
276
443
|
// After a lost connection, the transport waits for its reconnect: no second backoff
|
|
277
|
-
if (!(failure.lost && hooks.ready)) await pause(backoff(attempt), signal);
|
|
444
|
+
if (!(failure.lost && hooks.ready)) await pause(policy.backoff(attempt), signal, policy.clock);
|
|
278
445
|
}
|
|
279
446
|
} finally {
|
|
280
447
|
unlink();
|
|
281
448
|
}
|
|
282
449
|
}
|
|
283
450
|
|
|
451
|
+
// =============================================================================
|
|
452
|
+
// The shared subscriptions
|
|
453
|
+
// =============================================================================
|
|
454
|
+
|
|
455
|
+
/** An entry of the queue of one subscriber: a confirmation, or an event with its own numbers. */
|
|
456
|
+
type Entry =
|
|
457
|
+
| { readonly kind: "subscribed"; readonly topics: readonly string[] }
|
|
458
|
+
| { readonly kind: "event"; readonly topic: string; readonly data: unknown; readonly seq: number; dropped: number };
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* One subscriber of a shared subscription: its queue, and its numbers. Its `seq` counts from the
|
|
462
|
+
* last event that the shared subscription got before the subscriber joined.
|
|
463
|
+
*/
|
|
464
|
+
class Subscriber {
|
|
465
|
+
/** The `seq` of the wire subscription before the first event of this subscriber. */
|
|
466
|
+
base = 0;
|
|
467
|
+
/** The error that ends this subscriber, after the entries of its queue. */
|
|
468
|
+
failure: SubscriptionError | undefined;
|
|
469
|
+
/** This function tells the shared subscription that the subscriber waits. */
|
|
470
|
+
onWait: () => void = () => undefined;
|
|
471
|
+
private readonly queue: Entry[] = [];
|
|
472
|
+
private events = 0;
|
|
473
|
+
private waiter: ((entry: Entry | undefined) => void) | undefined;
|
|
474
|
+
private ended = false;
|
|
475
|
+
|
|
476
|
+
constructor(readonly id: string) {}
|
|
477
|
+
|
|
478
|
+
/** True when the subscriber waits for an entry. */
|
|
479
|
+
get waiting(): boolean {
|
|
480
|
+
return this.waiter !== undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** This function gives the subscriber one event of the wire, with its own `seq`. */
|
|
484
|
+
pushEvent(event: WireEvent): void {
|
|
485
|
+
this.push({ kind: "event", topic: event.topic, data: event.data, seq: event.seq - this.base, dropped: event.dropped ?? 0 });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** This function gives the subscriber one entry. A full queue drops its oldest event. */
|
|
489
|
+
push(entry: Entry): void {
|
|
490
|
+
if (this.ended) return;
|
|
491
|
+
const waiter = this.waiter;
|
|
492
|
+
if (waiter) {
|
|
493
|
+
this.waiter = undefined;
|
|
494
|
+
waiter(entry);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (entry.kind === "event") {
|
|
498
|
+
if (this.events >= SUBSCRIBER_BUFFER) {
|
|
499
|
+
// The next event of the same wire stream tells the loss. After a confirmation, a new stream starts.
|
|
500
|
+
const index = this.queue.findIndex((queued) => queued.kind === "event");
|
|
501
|
+
const lost = this.queue.splice(index, 1)[0] as Extract<Entry, { kind: "event" }>;
|
|
502
|
+
this.events--;
|
|
503
|
+
const next = index < this.queue.length ? this.queue[index]! : entry;
|
|
504
|
+
if (next.kind === "event") next.dropped += 1 + lost.dropped;
|
|
505
|
+
}
|
|
506
|
+
this.events++;
|
|
507
|
+
}
|
|
508
|
+
this.queue.push(entry);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** The next entry, or undefined at the end. */
|
|
512
|
+
take(): Promise<Entry | undefined> {
|
|
513
|
+
const entry = this.queue.shift();
|
|
514
|
+
if (entry) {
|
|
515
|
+
if (entry.kind === "event") this.events--;
|
|
516
|
+
return Promise.resolve(entry);
|
|
517
|
+
}
|
|
518
|
+
if (this.ended) return Promise.resolve(undefined);
|
|
519
|
+
return new Promise((resolve) => {
|
|
520
|
+
this.waiter = resolve;
|
|
521
|
+
this.onWait();
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* This function ends the subscriber. Without `discard`, it first gives the entries of its queue.
|
|
527
|
+
* The shared subscription ends it with its error, and an unsubscribe ends it with `discard`.
|
|
528
|
+
*/
|
|
529
|
+
end(failure?: SubscriptionError, discard = false): void {
|
|
530
|
+
if (this.ended) return;
|
|
531
|
+
this.ended = true;
|
|
532
|
+
this.failure = failure;
|
|
533
|
+
if (discard) {
|
|
534
|
+
this.queue.length = 0;
|
|
535
|
+
this.events = 0;
|
|
536
|
+
}
|
|
537
|
+
const waiter = this.waiter;
|
|
538
|
+
this.waiter = undefined;
|
|
539
|
+
waiter?.(undefined);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* One wire subscription and its subscribers. It reads the next item of the wire only when a
|
|
545
|
+
* subscriber waits, so a single slow subscriber holds the credit, as before the share. It ends
|
|
546
|
+
* when its last subscriber leaves, or with the end of the wire subscription for each subscriber.
|
|
547
|
+
*/
|
|
548
|
+
class SharedSubscription {
|
|
549
|
+
private readonly members = new Set<Subscriber>();
|
|
550
|
+
private readonly stop = new AbortController();
|
|
551
|
+
/** The topics of the current confirmation, or undefined before it and during a resubscribe. */
|
|
552
|
+
private confirmed: readonly string[] | undefined;
|
|
553
|
+
/** The `seq` of the last event of the wire. */
|
|
554
|
+
private lastSeq = 0;
|
|
555
|
+
private demand: (() => void) | undefined;
|
|
556
|
+
private started = false;
|
|
557
|
+
private finished = false;
|
|
558
|
+
|
|
559
|
+
constructor(
|
|
560
|
+
private readonly shares: Map<string, SharedSubscription> | undefined,
|
|
561
|
+
private readonly key: string,
|
|
562
|
+
private readonly transport: EventTransport,
|
|
563
|
+
private readonly call: Omit<WireCall, "onSubscribed" | "onRetry">,
|
|
564
|
+
) {}
|
|
565
|
+
|
|
566
|
+
/** True while a new subscriber can join. */
|
|
567
|
+
get open(): boolean {
|
|
568
|
+
return !this.finished && !this.stop.signal.aborted;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** This function adds a subscriber. On a confirmed subscription, it gets the confirmation at once. */
|
|
572
|
+
join(subscriber: Subscriber): void {
|
|
573
|
+
this.members.add(subscriber);
|
|
574
|
+
subscriber.base = this.lastSeq;
|
|
575
|
+
subscriber.onWait = () => this.wake();
|
|
576
|
+
if (this.confirmed) subscriber.push({ kind: "subscribed", topics: this.confirmed });
|
|
577
|
+
if (!this.started) {
|
|
578
|
+
this.started = true;
|
|
579
|
+
void this.run();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** This function removes a subscriber. The last one ends the wire subscription: the transport sends the cancel. */
|
|
584
|
+
leave(subscriber: Subscriber): void {
|
|
585
|
+
if (!this.members.delete(subscriber) || this.members.size > 0) return;
|
|
586
|
+
this.forget();
|
|
587
|
+
this.stop.abort();
|
|
588
|
+
this.wake();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private wake(): void {
|
|
592
|
+
const demand = this.demand;
|
|
593
|
+
this.demand = undefined;
|
|
594
|
+
demand?.();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** This function removes the subscription from the shares of its transport. */
|
|
598
|
+
private forget(): void {
|
|
599
|
+
if (this.shares?.get(this.key) === this) this.shares.delete(this.key);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private stopped(): boolean {
|
|
603
|
+
return this.stop.signal.aborted || this.call.hooks.lifetime?.aborted === true;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private wanted(): boolean {
|
|
607
|
+
for (const member of this.members) if (member.waiting) return true;
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private async run(): Promise<void> {
|
|
612
|
+
const events = wireEvents(
|
|
613
|
+
this.transport,
|
|
614
|
+
{
|
|
615
|
+
...this.call,
|
|
616
|
+
onSubscribed: (topics) => {
|
|
617
|
+
this.confirmed = topics;
|
|
618
|
+
this.lastSeq = 0;
|
|
619
|
+
for (const member of this.members) {
|
|
620
|
+
member.base = 0;
|
|
621
|
+
member.push({ kind: "subscribed", topics });
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
onRetry: () => {
|
|
625
|
+
this.confirmed = undefined;
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
this.stop.signal,
|
|
629
|
+
);
|
|
630
|
+
// The end of the transport ends the wait for a subscriber too
|
|
631
|
+
const lifetime = this.call.hooks.lifetime;
|
|
632
|
+
const onLifetime = (): void => this.wake();
|
|
633
|
+
lifetime?.addEventListener("abort", onLifetime, { once: true });
|
|
634
|
+
let failure: SubscriptionError | undefined;
|
|
635
|
+
try {
|
|
636
|
+
for (;;) {
|
|
637
|
+
while (!this.stopped() && !this.wanted()) {
|
|
638
|
+
await new Promise<void>((resolve) => {
|
|
639
|
+
this.demand = resolve;
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (this.stopped()) break;
|
|
643
|
+
const result = await events.next();
|
|
644
|
+
if (result.done) break;
|
|
645
|
+
this.lastSeq = result.value.seq;
|
|
646
|
+
for (const member of this.members) member.pushEvent(result.value);
|
|
647
|
+
}
|
|
648
|
+
} catch (error) {
|
|
649
|
+
failure = errorOfFailure(error);
|
|
650
|
+
} finally {
|
|
651
|
+
this.finished = true;
|
|
652
|
+
this.forget();
|
|
653
|
+
lifetime?.removeEventListener("abort", onLifetime);
|
|
654
|
+
await events.return(undefined).catch(() => undefined);
|
|
655
|
+
}
|
|
656
|
+
// The end of the wire subscription goes to each subscriber
|
|
657
|
+
for (const member of [...this.members]) member.end(failure);
|
|
658
|
+
this.members.clear();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** The shared subscriptions of each transport, by key. */
|
|
663
|
+
const sharesOf = new WeakMap<EventTransport, Map<string, SharedSubscription>>();
|
|
664
|
+
|
|
665
|
+
/** The key of a shared subscription: the set of topics, the metadata and the resubscribe rules. */
|
|
666
|
+
function keyOf(topics: readonly string[], metadata: Record<string, unknown>, policy: ResubscribePolicy): string | undefined {
|
|
667
|
+
try {
|
|
668
|
+
return JSON.stringify([[...new Set(topics)].sort(), metadata, policy.key]);
|
|
669
|
+
} catch {
|
|
670
|
+
// Metadata that is not JSON: the subscription is not shared
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** The shared subscription of a new subscriber: an open one with the same key, or a new one with the id of the subscriber. */
|
|
676
|
+
function sharedFor(
|
|
677
|
+
transport: EventTransport,
|
|
678
|
+
topics: readonly string[],
|
|
679
|
+
options: RemoteEventsOptions,
|
|
680
|
+
hooks: SubscriptionHooks,
|
|
681
|
+
policy: ResubscribePolicy,
|
|
682
|
+
id: string,
|
|
683
|
+
): SharedSubscription {
|
|
684
|
+
const metadata = { ...(options.metadata ?? {}) };
|
|
685
|
+
const key = keyOf(topics, metadata, policy);
|
|
686
|
+
let shares = sharesOf.get(transport);
|
|
687
|
+
if (!shares) {
|
|
688
|
+
shares = new Map();
|
|
689
|
+
sharesOf.set(transport, shares);
|
|
690
|
+
}
|
|
691
|
+
const existing = key === undefined ? undefined : shares.get(key);
|
|
692
|
+
if (existing?.open) return existing;
|
|
693
|
+
const shared = new SharedSubscription(key === undefined ? undefined : shares, key ?? "", transport, { topics, metadata, policy, hooks, id });
|
|
694
|
+
if (key !== undefined) shares.set(key, shared);
|
|
695
|
+
return shared;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** The event that a subscriber gets from an entry of its queue. */
|
|
699
|
+
function remoteEventOf<T>(entry: Extract<Entry, { kind: "event" }>, id: string): RemoteEvent<T> {
|
|
700
|
+
return { topic: entry.topic, data: entry.data as T, seq: entry.seq, dropped: entry.dropped, subscription: id };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** This function calls `onSubscribed`. An error of it goes to `console.error`, and the subscription goes on. */
|
|
704
|
+
function confirmTo(options: RemoteEventsOptions, topics: readonly string[]): void {
|
|
705
|
+
try {
|
|
706
|
+
options.onSubscribed?.(topics);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
console.error("[events] The onSubscribed handler failed:", error);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** This function joins a new subscriber to its shared subscription, and gives the function that leaves it. */
|
|
713
|
+
function attach(
|
|
714
|
+
subscriber: Subscriber,
|
|
715
|
+
transport: EventTransport,
|
|
716
|
+
topics: readonly string[],
|
|
717
|
+
options: RemoteEventsOptions,
|
|
718
|
+
hooks: SubscriptionHooks,
|
|
719
|
+
policy: ResubscribePolicy,
|
|
720
|
+
): () => void {
|
|
721
|
+
if (options.signal?.aborted) {
|
|
722
|
+
subscriber.end(undefined, true);
|
|
723
|
+
return () => undefined;
|
|
724
|
+
}
|
|
725
|
+
const shared = sharedFor(transport, topics, options, hooks, policy, subscriber.id);
|
|
726
|
+
let left = false;
|
|
727
|
+
const leave = (): void => {
|
|
728
|
+
if (left) return;
|
|
729
|
+
left = true;
|
|
730
|
+
options.signal?.removeEventListener("abort", leave);
|
|
731
|
+
shared.leave(subscriber);
|
|
732
|
+
subscriber.end(undefined, true);
|
|
733
|
+
};
|
|
734
|
+
options.signal?.addEventListener("abort", leave, { once: true });
|
|
735
|
+
shared.join(subscriber);
|
|
736
|
+
return leave;
|
|
737
|
+
}
|
|
738
|
+
|
|
284
739
|
/**
|
|
285
740
|
* This function gives the events of a subscription to topics of the bus of the other side. The
|
|
286
|
-
* iteration
|
|
287
|
-
*
|
|
741
|
+
* iteration joins the subscription, so it sends the call when no subscriber shares one. A `break`
|
|
742
|
+
* of the reader, or the abort of the signal, leaves it: the last subscriber sends the cancel. An
|
|
743
|
+
* error that ends the subscription ends the iteration with a `SubscriptionError`.
|
|
288
744
|
*
|
|
289
745
|
* @param transport - The transport to the publisher: HTTP, WebSocket, a message port, local
|
|
290
746
|
* @param topics - One topic, or a list of 1 to 64 topics
|
|
291
|
-
* @param options - The signal, the resubscribe, the
|
|
747
|
+
* @param options - The signal, the resubscribe rules, the clock and the confirmation handler
|
|
292
748
|
* @param hooks - The parts that a transport adds (its connection, its backoff, its `onEvent`)
|
|
749
|
+
* @throws TypeError - When `backoff` or `maxAttempts` is not valid, for example a plain number of milliseconds
|
|
293
750
|
*/
|
|
294
751
|
export function remoteEvents<T = unknown>(
|
|
295
752
|
transport: EventTransport,
|
|
@@ -298,8 +755,52 @@ export function remoteEvents<T = unknown>(
|
|
|
298
755
|
hooks: SubscriptionHooks = {},
|
|
299
756
|
): AsyncIterable<RemoteEvent<T>> {
|
|
300
757
|
const list = topicList(topics);
|
|
758
|
+
const policy = policyOf(options, hooks);
|
|
301
759
|
return {
|
|
302
|
-
[Symbol.asyncIterator]: ()
|
|
760
|
+
[Symbol.asyncIterator]: (): AsyncIterableIterator<RemoteEvent<T>> => {
|
|
761
|
+
const subscriber = new Subscriber(`events-${++subscriptions}`);
|
|
762
|
+
let leave: (() => void) | undefined;
|
|
763
|
+
let stopped = false;
|
|
764
|
+
const stop = (): void => {
|
|
765
|
+
stopped = true;
|
|
766
|
+
if (leave) leave();
|
|
767
|
+
else subscriber.end(undefined, true);
|
|
768
|
+
};
|
|
769
|
+
const iterator: AsyncIterableIterator<RemoteEvent<T>> = {
|
|
770
|
+
async next(): Promise<IteratorResult<RemoteEvent<T>>> {
|
|
771
|
+
if (!leave && !stopped) leave = attach(subscriber, transport, list, options, hooks, policy);
|
|
772
|
+
for (;;) {
|
|
773
|
+
const entry = await subscriber.take();
|
|
774
|
+
if (!entry) {
|
|
775
|
+
const failure = subscriber.failure;
|
|
776
|
+
subscriber.failure = undefined;
|
|
777
|
+
stop();
|
|
778
|
+
if (failure) throw failure;
|
|
779
|
+
return { value: undefined, done: true };
|
|
780
|
+
}
|
|
781
|
+
if (entry.kind === "subscribed") {
|
|
782
|
+
confirmTo(options, entry.topics);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
const event = remoteEventOf<T>(entry, subscriber.id);
|
|
786
|
+
hooks.onEvent?.(event as RemoteEvent);
|
|
787
|
+
return { value: event, done: false };
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
async return(): Promise<IteratorResult<RemoteEvent<T>>> {
|
|
791
|
+
stop();
|
|
792
|
+
return { value: undefined, done: true };
|
|
793
|
+
},
|
|
794
|
+
async throw(error?: unknown): Promise<IteratorResult<RemoteEvent<T>>> {
|
|
795
|
+
stop();
|
|
796
|
+
throw error;
|
|
797
|
+
},
|
|
798
|
+
[Symbol.asyncIterator]() {
|
|
799
|
+
return iterator;
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
return iterator;
|
|
803
|
+
},
|
|
303
804
|
};
|
|
304
805
|
}
|
|
305
806
|
|
|
@@ -307,12 +808,14 @@ export function remoteEvents<T = unknown>(
|
|
|
307
808
|
* This function subscribes to topics of the bus of the other side, and calls the handler for each
|
|
308
809
|
* event in order. It gives the unsubscribe function, with the `ready` promise of the first
|
|
309
810
|
* confirmation. An error of the handler goes to `console.error`, and the subscription goes on.
|
|
811
|
+
* The subscribers of the same topics with the same options on one transport share one call.
|
|
310
812
|
*
|
|
311
813
|
* @param transport - The transport to the publisher
|
|
312
814
|
* @param topics - One topic, or a list of 1 to 64 topics
|
|
313
815
|
* @param handler - The handler of each event: it gets the data and the event
|
|
314
816
|
* @param options - The options of `remoteEvents()`, and the `onError` handler of the end
|
|
315
817
|
* @param hooks - The parts that a transport adds
|
|
818
|
+
* @throws TypeError - When `backoff` or `maxAttempts` is not valid, for example a plain number of milliseconds
|
|
316
819
|
*/
|
|
317
820
|
export function subscribeRemote<T = unknown>(
|
|
318
821
|
transport: EventTransport,
|
|
@@ -321,8 +824,9 @@ export function subscribeRemote<T = unknown>(
|
|
|
321
824
|
options: SubscribeOptions = {},
|
|
322
825
|
hooks: SubscriptionHooks = {},
|
|
323
826
|
): Subscription {
|
|
324
|
-
const
|
|
325
|
-
const
|
|
827
|
+
const list = topicList(topics);
|
|
828
|
+
const policy = policyOf(options, hooks);
|
|
829
|
+
const subscriber = new Subscriber(`events-${++subscriptions}`);
|
|
326
830
|
let confirmed = false;
|
|
327
831
|
let confirm: () => void = () => undefined;
|
|
328
832
|
let refuse: (error: SubscriptionError) => void = () => undefined;
|
|
@@ -332,38 +836,36 @@ export function subscribeRemote<T = unknown>(
|
|
|
332
836
|
});
|
|
333
837
|
// A caller that does not wait for `ready` gets no unhandled rejection
|
|
334
838
|
ready.catch(() => undefined);
|
|
335
|
-
const
|
|
336
|
-
const settings: RemoteEventsOptions = {
|
|
337
|
-
...options,
|
|
338
|
-
signal,
|
|
339
|
-
onSubscribed: (list) => {
|
|
340
|
-
if (!confirmed) {
|
|
341
|
-
confirmed = true;
|
|
342
|
-
confirm();
|
|
343
|
-
}
|
|
344
|
-
options.onSubscribed?.(list);
|
|
345
|
-
},
|
|
346
|
-
};
|
|
839
|
+
const leave = attach(subscriber, transport, list, options, hooks, policy);
|
|
347
840
|
|
|
348
841
|
void (async () => {
|
|
349
842
|
try {
|
|
350
|
-
for
|
|
843
|
+
for (;;) {
|
|
844
|
+
const entry = await subscriber.take();
|
|
845
|
+
if (!entry) break;
|
|
846
|
+
if (entry.kind === "subscribed") {
|
|
847
|
+
if (!confirmed) {
|
|
848
|
+
confirmed = true;
|
|
849
|
+
confirm();
|
|
850
|
+
}
|
|
851
|
+
confirmTo(options, entry.topics);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const event = remoteEventOf<T>(entry, subscriber.id);
|
|
855
|
+
hooks.onEvent?.(event as RemoteEvent);
|
|
351
856
|
try {
|
|
352
857
|
await handler(event.data, event);
|
|
353
858
|
} catch (error) {
|
|
354
859
|
console.error(`[events] The handler of ${event.topic} failed:`, error);
|
|
355
860
|
}
|
|
356
861
|
}
|
|
357
|
-
if (!confirmed) refuse(new SubscriptionError("ABORTED", "The subscription ended before its confirmation", false));
|
|
358
|
-
} catch (error) {
|
|
359
|
-
const failure = errorOfFailure(error);
|
|
360
|
-
if (!confirmed) refuse(failure);
|
|
361
|
-
options.onError?.(failure);
|
|
362
862
|
} finally {
|
|
363
|
-
|
|
863
|
+
leave();
|
|
364
864
|
}
|
|
865
|
+
const failure = subscriber.failure;
|
|
866
|
+
if (!confirmed) refuse(failure ?? new SubscriptionError("ABORTED", "The subscription ended before its confirmation", false));
|
|
867
|
+
if (failure) options.onError?.(failure);
|
|
365
868
|
})();
|
|
366
869
|
|
|
367
|
-
|
|
368
|
-
return Object.assign(unsubscribe, { ready, id });
|
|
870
|
+
return Object.assign(leave, { ready, id: subscriber.id });
|
|
369
871
|
}
|