@hydranium/protocol 1.0.0-next.23 → 1.0.0-next.230
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 +40 -3
- package/lib/abstract-logger.d.ts +5 -0
- package/lib/abstract-logger.d.ts.map +1 -1
- package/lib/abstract-logger.js +7 -0
- package/lib/abstract-logger.js.map +1 -1
- package/lib/client/data-connection.d.ts +236 -0
- package/lib/client/data-connection.d.ts.map +1 -0
- package/lib/client/data-connection.js +404 -0
- package/lib/client/data-connection.js.map +1 -0
- package/lib/client/data-events.d.ts +13 -1
- package/lib/client/data-events.d.ts.map +1 -1
- package/lib/client/data-events.js +21 -0
- package/lib/client/data-events.js.map +1 -1
- package/lib/client/data-port.d.ts +27 -22
- package/lib/client/data-port.d.ts.map +1 -1
- package/lib/client/data-session.d.ts +473 -81
- package/lib/client/data-session.d.ts.map +1 -1
- package/lib/client/data-session.js +743 -108
- package/lib/client/data-session.js.map +1 -1
- package/lib/client/index.d.ts +14 -9
- package/lib/client/index.d.ts.map +1 -1
- package/lib/client/index.js +14 -9
- package/lib/client/index.js.map +1 -1
- package/lib/client/message-relay.d.ts +9 -3
- package/lib/client/message-relay.d.ts.map +1 -1
- package/lib/client/message-relay.js +11 -5
- package/lib/client/message-relay.js.map +1 -1
- package/lib/client/post-message-transport.d.ts +64 -3
- package/lib/client/post-message-transport.d.ts.map +1 -1
- package/lib/client/post-message-transport.js +175 -1
- package/lib/client/post-message-transport.js.map +1 -1
- package/lib/client/rpc-connection.d.ts +157 -0
- package/lib/client/rpc-connection.d.ts.map +1 -0
- package/lib/client/rpc-connection.js +214 -0
- package/lib/client/rpc-connection.js.map +1 -0
- package/lib/client-ids.d.ts +45 -0
- package/lib/client-ids.d.ts.map +1 -0
- package/lib/client-ids.js +48 -0
- package/lib/client-ids.js.map +1 -0
- package/lib/clock.d.ts +38 -0
- package/lib/clock.d.ts.map +1 -1
- package/lib/clock.js +36 -1
- package/lib/clock.js.map +1 -1
- package/lib/console-logger.d.ts +23 -0
- package/lib/console-logger.d.ts.map +1 -0
- package/lib/console-logger.js +39 -0
- package/lib/console-logger.js.map +1 -0
- package/lib/data/data-protocol-methods.d.ts +4 -4
- package/lib/data/data-protocol-methods.d.ts.map +1 -1
- package/lib/data/data-protocol-methods.js +12 -1
- package/lib/data/data-protocol-methods.js.map +1 -1
- package/lib/data/data-server-protocol.d.ts +132 -41
- package/lib/data/data-server-protocol.d.ts.map +1 -1
- package/lib/data/events.d.ts +117 -21
- package/lib/data/events.d.ts.map +1 -1
- package/lib/data/requests.d.ts +69 -11
- package/lib/data/requests.d.ts.map +1 -1
- package/lib/debouncer.d.ts.map +1 -1
- package/lib/debouncer.js.map +1 -1
- package/lib/errors.d.ts +187 -29
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +270 -29
- package/lib/errors.js.map +1 -1
- package/lib/glsp-request-model-args.d.ts +16 -0
- package/lib/glsp-request-model-args.d.ts.map +1 -0
- package/lib/glsp-request-model-args.js +19 -0
- package/lib/glsp-request-model-args.js.map +1 -0
- package/lib/glsp-save-model-actions.d.ts +50 -0
- package/lib/glsp-save-model-actions.d.ts.map +1 -0
- package/lib/glsp-save-model-actions.js +28 -0
- package/lib/glsp-save-model-actions.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +10 -0
- package/lib/index.js.map +1 -1
- package/lib/latency-collector.d.ts +8 -4
- package/lib/latency-collector.d.ts.map +1 -1
- package/lib/latency-collector.js.map +1 -1
- package/lib/logger.d.ts +22 -1
- package/lib/logger.d.ts.map +1 -1
- package/lib/logger.js +31 -3
- package/lib/logger.js.map +1 -1
- package/lib/messages/index.d.ts +30 -0
- package/lib/messages/index.d.ts.map +1 -0
- package/lib/messages/index.js +62 -0
- package/lib/messages/index.js.map +1 -0
- package/lib/messages/primitives.d.ts +188 -0
- package/lib/messages/primitives.d.ts.map +1 -0
- package/lib/messages/primitives.js +161 -0
- package/lib/messages/primitives.js.map +1 -0
- package/lib/model-server.d.ts +60 -13
- package/lib/model-server.d.ts.map +1 -1
- package/lib/model-server.js +4 -2
- package/lib/model-server.js.map +1 -1
- package/lib/model-service/base-version.d.ts +64 -0
- package/lib/model-service/base-version.d.ts.map +1 -0
- package/lib/model-service/base-version.js +43 -0
- package/lib/model-service/base-version.js.map +1 -0
- package/lib/model-service/index.d.ts +1 -1
- package/lib/model-service/index.d.ts.map +1 -1
- package/lib/model-service/index.js +4 -5
- package/lib/model-service/index.js.map +1 -1
- package/lib/model-service/reference-candidate.d.ts +5 -3
- package/lib/model-service/reference-candidate.d.ts.map +1 -1
- package/lib/{model-service/args.js → node/index.d.ts} +2 -3
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +29 -0
- package/lib/node/index.js.map +1 -0
- package/lib/node/process-memory.d.ts +66 -0
- package/lib/node/process-memory.d.ts.map +1 -0
- package/lib/node/process-memory.js +291 -0
- package/lib/node/process-memory.js.map +1 -0
- package/lib/noop-logger.d.ts.map +1 -1
- package/lib/noop-logger.js.map +1 -1
- package/lib/observable-value.js.map +1 -1
- package/lib/patch-merge.d.ts +35 -32
- package/lib/patch-merge.d.ts.map +1 -1
- package/lib/patch-merge.js +67 -23
- package/lib/patch-merge.js.map +1 -1
- package/lib/profile-session.d.ts +8 -4
- package/lib/profile-session.d.ts.map +1 -1
- package/lib/profile-session.js.map +1 -1
- package/lib/random-uuid.d.ts +14 -0
- package/lib/random-uuid.d.ts.map +1 -0
- package/lib/random-uuid.js +24 -0
- package/lib/random-uuid.js.map +1 -0
- package/lib/reconcile-write.d.ts +65 -0
- package/lib/reconcile-write.d.ts.map +1 -0
- package/lib/reconcile-write.js +67 -0
- package/lib/reconcile-write.js.map +1 -0
- package/lib/rpc/bind-rpc-methods.d.ts +33 -3
- package/lib/rpc/bind-rpc-methods.d.ts.map +1 -1
- package/lib/rpc/bind-rpc-methods.js +32 -3
- package/lib/rpc/bind-rpc-methods.js.map +1 -1
- package/lib/rpc/create-rpc-proxy.d.ts +10 -0
- package/lib/rpc/create-rpc-proxy.d.ts.map +1 -1
- package/lib/rpc/create-rpc-proxy.js +12 -2
- package/lib/rpc/create-rpc-proxy.js.map +1 -1
- package/lib/rpc/index.d.ts +1 -0
- package/lib/rpc/index.d.ts.map +1 -1
- package/lib/rpc/index.js +1 -0
- package/lib/rpc/index.js.map +1 -1
- package/lib/rpc/send-by-method-name.d.ts +76 -0
- package/lib/rpc/send-by-method-name.d.ts.map +1 -0
- package/lib/rpc/send-by-method-name.js +120 -0
- package/lib/rpc/send-by-method-name.js.map +1 -0
- package/lib/rpc/wire-prefix.js.map +1 -1
- package/lib/testing/catalogue-audit.d.ts +80 -0
- package/lib/testing/catalogue-audit.d.ts.map +1 -0
- package/lib/testing/catalogue-audit.js +94 -0
- package/lib/testing/catalogue-audit.js.map +1 -0
- package/lib/testing/data-doubles.d.ts +42 -15
- package/lib/testing/data-doubles.d.ts.map +1 -1
- package/lib/testing/data-doubles.js +58 -10
- package/lib/testing/data-doubles.js.map +1 -1
- package/lib/testing/fake-clock.d.ts +9 -1
- package/lib/testing/fake-clock.d.ts.map +1 -1
- package/lib/testing/fake-clock.js +54 -45
- package/lib/testing/fake-clock.js.map +1 -1
- package/lib/testing/index.d.ts +1 -0
- package/lib/testing/index.d.ts.map +1 -1
- package/lib/testing/index.js +5 -2
- package/lib/testing/index.js.map +1 -1
- package/lib/testing/node/duplex-connection.d.ts.map +1 -1
- package/lib/testing/node/duplex-connection.js +3 -2
- package/lib/testing/node/duplex-connection.js.map +1 -1
- package/lib/testing/node/duplex-stream.js.map +1 -1
- package/lib/testing/node/index.d.ts +1 -0
- package/lib/testing/node/index.d.ts.map +1 -1
- package/lib/testing/node/index.js +2 -2
- package/lib/testing/node/index.js.map +1 -1
- package/lib/testing/node/message-port-pair.d.ts +25 -0
- package/lib/testing/node/message-port-pair.d.ts.map +1 -0
- package/lib/testing/node/message-port-pair.js +26 -0
- package/lib/testing/node/message-port-pair.js.map +1 -0
- package/lib/testing/wait-for.js.map +1 -1
- package/lib/tracer.d.ts.map +1 -1
- package/lib/tracer.js.map +1 -1
- package/lib/transfer-diagnostic.d.ts +33 -0
- package/lib/transfer-diagnostic.d.ts.map +1 -1
- package/lib/transfer-diagnostic.js +23 -0
- package/lib/transfer-diagnostic.js.map +1 -1
- package/lib/transfer-document.d.ts +70 -32
- package/lib/transfer-document.d.ts.map +1 -1
- package/lib/transfer-document.js +17 -9
- package/lib/transfer-document.js.map +1 -1
- package/lib/uri.d.ts.map +1 -1
- package/lib/uri.js.map +1 -1
- package/lib/util.d.ts +8 -0
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +32 -0
- package/lib/util.js.map +1 -1
- package/package.json +29 -37
- package/src/abstract-logger.ts +8 -0
- package/src/client/data-connection.ts +502 -0
- package/src/client/data-events.ts +33 -1
- package/src/client/data-port.ts +29 -23
- package/src/client/data-session.ts +951 -126
- package/src/client/index.ts +14 -9
- package/src/client/message-relay.ts +29 -7
- package/src/client/post-message-transport.ts +219 -4
- package/src/client/rpc-connection.ts +281 -0
- package/src/client-ids.ts +49 -0
- package/src/clock.ts +56 -0
- package/src/console-logger.ts +39 -0
- package/src/data/data-protocol-methods.ts +13 -4
- package/src/data/data-server-protocol.ts +157 -41
- package/src/data/events.ts +123 -21
- package/src/data/requests.ts +74 -11
- package/src/errors.ts +322 -36
- package/src/glsp-request-model-args.ts +16 -0
- package/src/glsp-save-model-actions.ts +59 -0
- package/src/index.ts +10 -0
- package/src/latency-collector.ts +8 -3
- package/src/logger.ts +28 -2
- package/src/messages/index.ts +37 -0
- package/src/messages/primitives.ts +271 -0
- package/src/model-server.ts +63 -18
- package/src/model-service/base-version.ts +72 -0
- package/src/model-service/index.ts +4 -5
- package/src/model-service/reference-candidate.ts +5 -3
- package/src/node/index.ts +14 -0
- package/src/node/process-memory.ts +299 -0
- package/src/patch-merge.ts +97 -42
- package/src/profile-session.ts +9 -4
- package/src/random-uuid.ts +21 -0
- package/src/reconcile-write.ts +124 -0
- package/src/rpc/README.md +4 -5
- package/src/rpc/bind-rpc-methods.ts +59 -4
- package/src/rpc/create-rpc-proxy.ts +20 -2
- package/src/rpc/index.ts +1 -0
- package/src/rpc/send-by-method-name.ts +140 -0
- package/src/testing/catalogue-audit.ts +111 -0
- package/src/testing/data-doubles.ts +145 -25
- package/src/testing/fake-clock.ts +62 -47
- package/src/testing/index.ts +5 -2
- package/src/testing/node/duplex-connection.ts +3 -2
- package/src/testing/node/index.ts +2 -2
- package/src/testing/node/message-port-pair.ts +40 -0
- package/src/transfer-diagnostic.ts +40 -0
- package/src/transfer-document.ts +87 -34
- package/src/util.ts +33 -0
- package/lib/model-service/args.d.ts +0 -64
- package/lib/model-service/args.d.ts.map +0 -1
- package/lib/model-service/args.js.map +0 -1
- package/src/model-service/args.ts +0 -67
package/src/client/index.ts
CHANGED
|
@@ -13,28 +13,33 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Where `./data` is the wire *contract* and `./rpc` is the machinery that lowers
|
|
15
15
|
* it onto a connection, this is what a client wraps around both: the seam a host
|
|
16
|
-
* fills in (`DataPort`), the
|
|
17
|
-
*
|
|
16
|
+
* fills in (`DataPort`), the connection above it (`DataConnection` — readiness
|
|
17
|
+
* gate and reconnect), the participants on that connection (`DataSession` —
|
|
18
|
+
* identity, open/watch ordering, echo recognition), the inbound
|
|
18
19
|
* fan-out (`DataEvents`), and the two halves of the hop for hosts whose
|
|
19
20
|
* client cannot hold a socket — `createPostMessageTransport` on the client
|
|
20
|
-
* side and `relayToPostMessageChannel` on the side that does hold it
|
|
21
|
+
* side and `relayToPostMessageChannel` on the side that does hold it, and
|
|
22
|
+
* `createMessagePortTransport` for a head's worker `MessagePort`, at either
|
|
23
|
+
* end, which the GLSP worker head uses too.
|
|
21
24
|
*
|
|
22
25
|
* **Neutral, and gate-enforced so.** Nothing here imports a host package or a
|
|
23
26
|
* Node builtin, which is what lets one client tier serve a Theia frontend, a VS
|
|
24
27
|
* Code extension host, a VS Code webview and a plain browser app. `npm run
|
|
25
|
-
* check:neutral` bundles these modules for the browser; `scripts/check-neutral-bundles.
|
|
28
|
+
* check:neutral` bundles these modules for the browser; `scripts/check-neutral-bundles.mts`
|
|
26
29
|
* carries the entries.
|
|
27
30
|
*
|
|
28
31
|
* The Theia-specific mounting of the same contract lives in
|
|
29
|
-
* `@hydranium/data-client-theia
|
|
30
|
-
*
|
|
31
|
-
* is the Theia-bound counterpart of `DataEvents`.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* `@hydranium/data-client-theia`, and is now only the TRANSPORT: its
|
|
33
|
+
* `ChannelDataPort` fills the `DataPort` seam over a Theia channel, and its
|
|
34
|
+
* `EmitterDataClient` is the Theia-bound counterpart of `DataEvents`.
|
|
35
|
+
* Everything above the port is here, so a Theia frontend and a browser page
|
|
36
|
+
* differ by which port they construct and nothing else.
|
|
34
37
|
*/
|
|
35
38
|
|
|
39
|
+
export * from './data-connection';
|
|
36
40
|
export * from './data-events';
|
|
37
41
|
export * from './data-port';
|
|
38
42
|
export * from './data-session';
|
|
39
43
|
export * from './message-relay';
|
|
40
44
|
export * from './post-message-transport';
|
|
45
|
+
export * from './rpc-connection';
|
|
@@ -8,8 +8,29 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import { Emitter, type Disposable, type Event, type Message, type MessageReader, type MessageWriter } from 'vscode-jsonrpc';
|
|
11
|
+
import { defineMessage, describeError, resolve, type ResolvedMessage } from '../messages/primitives';
|
|
11
12
|
import type { PostMessageChannel } from './post-message-transport';
|
|
12
13
|
|
|
14
|
+
export const RELAY_TRANSPORT_OPEN_FAILED = defineMessage(
|
|
15
|
+
'hydranium/protocol/relay-transport-open-failed',
|
|
16
|
+
'Could not open the transport to relay: {detail}'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const RELAY_TRANSPORT_READ_FAILED = defineMessage(
|
|
20
|
+
'hydranium/protocol/relay-transport-read-failed',
|
|
21
|
+
'Could not read from the relayed transport: {detail}'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const RELAY_TRANSPORT_WRITE_FAILED = defineMessage(
|
|
25
|
+
'hydranium/protocol/relay-transport-write-failed',
|
|
26
|
+
'Could not write to the relayed transport: {detail}'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const RELAY_REPLAY_FAILED = defineMessage(
|
|
30
|
+
'hydranium/protocol/relay-replay-failed',
|
|
31
|
+
'Could not replay a buffered message to the relayed transport: {detail}'
|
|
32
|
+
);
|
|
33
|
+
|
|
13
34
|
/**
|
|
14
35
|
* The framed side of a relay: the reader/writer pair over whatever transport the
|
|
15
36
|
* host actually holds — a TCP socket to the data-server, a child process' stdio,
|
|
@@ -30,13 +51,14 @@ export interface RelayTransport {
|
|
|
30
51
|
export interface MessageRelayOptions {
|
|
31
52
|
/**
|
|
32
53
|
* Surface a failure the way the host does. Same contract as
|
|
33
|
-
* `DataPort.reportError`: `
|
|
54
|
+
* `DataPort.reportError`: `reported` is a complete sentence plus the identity
|
|
55
|
+
* needed to render it in another language.
|
|
34
56
|
*
|
|
35
57
|
* A relay has no other way to report — it sits between two transports and
|
|
36
58
|
* owns neither, so a swallowed error here presents as a form that never
|
|
37
59
|
* populates.
|
|
38
60
|
*/
|
|
39
|
-
readonly reportError?: (error: unknown,
|
|
61
|
+
readonly reportError?: (error: unknown, reported: ResolvedMessage) => void;
|
|
40
62
|
}
|
|
41
63
|
|
|
42
64
|
/** A live relay. Dispose to tear both directions down. */
|
|
@@ -88,7 +110,7 @@ export interface MessageRelay extends Disposable {
|
|
|
88
110
|
* imports the entrypoint that frames its own transport (`vscode-jsonrpc/node`
|
|
89
111
|
* for a socket) and the RAL that comes with it; this tier stays neutral and is
|
|
90
112
|
* gated so by `npm run check:neutral`. Contrast
|
|
91
|
-
* `@hydranium/
|
|
113
|
+
* `@hydranium/client-theia`'s `SocketChannelForwarder`, which does build a
|
|
92
114
|
* connection only to borrow its `onClose`, and pays a Theia dependency for the
|
|
93
115
|
* byte coding this shape does not need.
|
|
94
116
|
*
|
|
@@ -143,7 +165,7 @@ export function relayToPostMessageChannel(
|
|
|
143
165
|
bufferSubscription?.dispose();
|
|
144
166
|
bufferSubscription = undefined;
|
|
145
167
|
buffered.length = 0;
|
|
146
|
-
options.reportError?.(error,
|
|
168
|
+
options.reportError?.(error, resolve(RELAY_TRANSPORT_OPEN_FAILED, { detail: describeError(error) }));
|
|
147
169
|
closeFramedSide();
|
|
148
170
|
return false;
|
|
149
171
|
}
|
|
@@ -166,12 +188,12 @@ export function relayToPostMessageChannel(
|
|
|
166
188
|
opened.reader.listen(message => channel.post(message)),
|
|
167
189
|
opened.reader.onClose(() => closeFramedSide()),
|
|
168
190
|
opened.reader.onError(error => {
|
|
169
|
-
options.reportError?.(error,
|
|
191
|
+
options.reportError?.(error, resolve(RELAY_TRANSPORT_READ_FAILED, { detail: describeError(error) }));
|
|
170
192
|
closeFramedSide();
|
|
171
193
|
}),
|
|
172
194
|
channel.onMessage(message => {
|
|
173
195
|
void opened.writer.write(message).catch((error: unknown) => {
|
|
174
|
-
options.reportError?.(error,
|
|
196
|
+
options.reportError?.(error, resolve(RELAY_TRANSPORT_WRITE_FAILED, { detail: describeError(error) }));
|
|
175
197
|
});
|
|
176
198
|
})
|
|
177
199
|
);
|
|
@@ -183,7 +205,7 @@ export function relayToPostMessageChannel(
|
|
|
183
205
|
|
|
184
206
|
for (const message of buffered) {
|
|
185
207
|
void opened.writer.write(message).catch((error: unknown) => {
|
|
186
|
-
options.reportError?.(error,
|
|
208
|
+
options.reportError?.(error, resolve(RELAY_REPLAY_FAILED, { detail: describeError(error) }));
|
|
187
209
|
});
|
|
188
210
|
}
|
|
189
211
|
buffered.length = 0;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
AbstractMessageReader,
|
|
12
12
|
AbstractMessageWriter,
|
|
13
13
|
Emitter,
|
|
14
|
+
RAL,
|
|
14
15
|
type DataCallback,
|
|
15
16
|
type Disposable,
|
|
16
17
|
type Message,
|
|
@@ -36,7 +37,8 @@ export interface PostMessageChannel {
|
|
|
36
37
|
/**
|
|
37
38
|
* Hand one JSON-RPC message to the other side. Fire-and-forget: delivery
|
|
38
39
|
* failures surface through {@link onClose}, never as a rejection, because
|
|
39
|
-
* `postMessage` has no completion to report.
|
|
40
|
+
* `postMessage` has no completion to report. A value the pipe refuses may
|
|
41
|
+
* throw, and the writer turns that into a rejected write and a writer error.
|
|
40
42
|
*/
|
|
41
43
|
post(message: Message): void;
|
|
42
44
|
|
|
@@ -67,12 +69,18 @@ export interface PostMessageChannel {
|
|
|
67
69
|
*
|
|
68
70
|
* Keeping the factory out of this module is also what lets the module stay
|
|
69
71
|
* browser-neutral: it touches only `AbstractMessageReader` /
|
|
70
|
-
* `AbstractMessageWriter` / `Emitter`, none of which need a RAL
|
|
72
|
+
* `AbstractMessageWriter` / `Emitter`, none of which need a RAL, and the RAL's
|
|
73
|
+
* timer, which {@link createMessagePortTransport} reads only once a message
|
|
74
|
+
* arrives, by when the host that built the connection has installed one in
|
|
75
|
+
* the same copy of `vscode-jsonrpc`.
|
|
71
76
|
*/
|
|
72
77
|
export interface PostMessageTransport {
|
|
73
78
|
readonly reader: MessageReader;
|
|
74
79
|
readonly writer: MessageWriter;
|
|
75
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Release the channel subscriptions. A {@link createMessagePortTransport}
|
|
82
|
+
* transport also signals its end to the other side.
|
|
83
|
+
*/
|
|
76
84
|
dispose(): void;
|
|
77
85
|
}
|
|
78
86
|
|
|
@@ -112,6 +120,7 @@ class PostMessageReader extends AbstractMessageReader implements MessageReader {
|
|
|
112
120
|
/** The dual of {@link PostMessageReader} — one `post` per JSON-RPC message. */
|
|
113
121
|
class PostMessageWriter extends AbstractMessageWriter implements MessageWriter {
|
|
114
122
|
protected readonly subscriptions: Disposable[] = [];
|
|
123
|
+
protected errorCount = 0;
|
|
115
124
|
|
|
116
125
|
constructor(protected readonly channel: PostMessageChannel) {
|
|
117
126
|
super();
|
|
@@ -122,7 +131,17 @@ class PostMessageWriter extends AbstractMessageWriter implements MessageWriter {
|
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
write(message: Message): Promise<void> {
|
|
125
|
-
|
|
134
|
+
try {
|
|
135
|
+
this.channel.post(message);
|
|
136
|
+
} catch (error: unknown) {
|
|
137
|
+
// `postMessage` throws synchronously on a value structured clone
|
|
138
|
+
// refuses. Thrown on, it would escape from the caller's
|
|
139
|
+
// `sendNotification` rather than reject it, and the connection would
|
|
140
|
+
// never see a write error.
|
|
141
|
+
this.errorCount++;
|
|
142
|
+
this.fireError(error, message, this.errorCount);
|
|
143
|
+
return Promise.reject(error);
|
|
144
|
+
}
|
|
126
145
|
// `postMessage` reports no completion, so the resolved promise means
|
|
127
146
|
// "handed over", not "delivered". vscode-jsonrpc only needs the former.
|
|
128
147
|
return Promise.resolve();
|
|
@@ -165,3 +184,199 @@ export function createPostMessageTransport(channel: PostMessageChannel): PostMes
|
|
|
165
184
|
}
|
|
166
185
|
};
|
|
167
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A `MessagePort` the host transferred into or out of a worker, described
|
|
190
|
+
* structurally.
|
|
191
|
+
*
|
|
192
|
+
* This package compiles without the DOM lib, so `MessagePort` has no name here
|
|
193
|
+
* and the contract has to be spelled out.
|
|
194
|
+
*
|
|
195
|
+
* **The member set admits a `MessagePort` and REJECTS a `Worker` or the worker
|
|
196
|
+
* global**, both of which would otherwise fit a `postMessage` pipe. Only a port
|
|
197
|
+
* needs starting, so `start` is what tells the three apart, and naming it here
|
|
198
|
+
* turns "never bind a head to the global" into a compile error at the call
|
|
199
|
+
* site. A head on the global receives every other head's traffic.
|
|
200
|
+
*
|
|
201
|
+
* **That compile error happens at the ADOPTER, not here.** This package
|
|
202
|
+
* resolves neither `MessagePort` nor `Worker` as a type, so nothing here can
|
|
203
|
+
* demonstrate the rejection; a host compiling its worker against
|
|
204
|
+
* `lib.webworker` (or a page against `lib.dom`) is where the names resolve and
|
|
205
|
+
* the guard bites. Measured there: passing the worker global fails with
|
|
206
|
+
* `Property 'start' is missing in type 'DedicatedWorkerGlobalScope'`.
|
|
207
|
+
*/
|
|
208
|
+
export interface TransferredMessagePort {
|
|
209
|
+
postMessage(message: unknown): void;
|
|
210
|
+
addEventListener(type: 'message', listener: (event: unknown) => void, options?: unknown): void;
|
|
211
|
+
removeEventListener(type: 'message', listener: (event: unknown) => void, options?: unknown): void;
|
|
212
|
+
start(): void;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The value {@link createMessagePortTransport} posts in place of a JSON-RPC
|
|
217
|
+
* message to say its end is going away. A string, so no JSON-RPC message can
|
|
218
|
+
* be mistaken for it.
|
|
219
|
+
*/
|
|
220
|
+
const MESSAGE_PORT_CLOSE_SIGNAL = 'hydranium/message-port-closed';
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A {@link PostMessageChannel} over a {@link TransferredMessagePort}, whose
|
|
224
|
+
* {@link close} posts {@link MESSAGE_PORT_CLOSE_SIGNAL}.
|
|
225
|
+
*
|
|
226
|
+
* **A port reports nothing when its peer goes away**: Chromium ships no `close`
|
|
227
|
+
* event on `MessagePort`, Firefox and WebKit have not committed to one, and
|
|
228
|
+
* `messageerror` fires only for a message that cannot be deserialized. So the
|
|
229
|
+
* end is signalled in band.
|
|
230
|
+
*
|
|
231
|
+
* The port is never `close()`d. A later transport can still be built on it, at
|
|
232
|
+
* both ends, and whether messages posted just before a `close()` are delivered
|
|
233
|
+
* is not something the platform settles.
|
|
234
|
+
*/
|
|
235
|
+
class MessagePortChannel implements PostMessageChannel {
|
|
236
|
+
protected readonly messageEmitter = new Emitter<Message>();
|
|
237
|
+
protected readonly closeEmitter = new Emitter<void>();
|
|
238
|
+
/** Set once either end has signalled; this end then delivers nothing more and never signals again. */
|
|
239
|
+
protected closed = false;
|
|
240
|
+
/** Set when the peer's signal has arrived and its close waits on {@link backlog}. */
|
|
241
|
+
protected closePending = false;
|
|
242
|
+
/** Messages handed to the connection that its queue may not have dispatched yet. */
|
|
243
|
+
protected backlog = 0;
|
|
244
|
+
// A `MessageEvent`, which the listener's type cannot name without the DOM lib.
|
|
245
|
+
protected readonly listener = (event: unknown): void => this.receive((event as { readonly data: unknown }).data);
|
|
246
|
+
|
|
247
|
+
constructor(protected readonly port: TransferredMessagePort) {
|
|
248
|
+
port.addEventListener('message', this.listener);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
post(message: Message): void {
|
|
252
|
+
if (!this.closed) {
|
|
253
|
+
this.port.postMessage(message);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
onMessage(listener: (message: Message) => void): Disposable {
|
|
258
|
+
return this.messageEmitter.event(listener);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
onClose(listener: () => void): Disposable {
|
|
262
|
+
return this.closeEmitter.event(listener);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Signal this end's close to the peer, once, after everything posted before it. */
|
|
266
|
+
close(): void {
|
|
267
|
+
if (this.closed) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.end();
|
|
271
|
+
this.port.postMessage(MESSAGE_PORT_CLOSE_SIGNAL);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
protected end(): void {
|
|
275
|
+
this.closed = true;
|
|
276
|
+
this.port.removeEventListener('message', this.listener);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Deliver one inbound value, or take the peer's signal.
|
|
281
|
+
*
|
|
282
|
+
* **The close waits until the connection has dispatched every message that
|
|
283
|
+
* came before it.** Firing it at once is safe under `vscode-jsonrpc`'s
|
|
284
|
+
* browser runtime, which drains the connection's queue on microtasks before
|
|
285
|
+
* the signal's own task runs. Under its Node runtime the queue gives out one
|
|
286
|
+
* message per `setImmediate` turn, so a close fired at once would overtake a
|
|
287
|
+
* `closeSession` still queued, and end that session as lost. So each
|
|
288
|
+
* delivered message is counted down on the same RAL timer, one per turn,
|
|
289
|
+
* scheduled after the connection's own, and the close fires when the count
|
|
290
|
+
* reaches zero: under either runtime, after the last dispatch.
|
|
291
|
+
*/
|
|
292
|
+
protected receive(data: unknown): void {
|
|
293
|
+
if (this.closed) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (data === MESSAGE_PORT_CLOSE_SIGNAL) {
|
|
297
|
+
this.end();
|
|
298
|
+
this.closePending = true;
|
|
299
|
+
if (this.backlog === 0) {
|
|
300
|
+
this.closeEmitter.fire();
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.messageEmitter.fire(data as Message);
|
|
305
|
+
this.backlog++;
|
|
306
|
+
if (this.backlog === 1) {
|
|
307
|
+
this.countDownBacklog();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Mirrors the connection's queue at its default of one message per turn. A
|
|
312
|
+
// connection built with a `maxParallelism`, or with a `messageStrategy` that
|
|
313
|
+
// defers `next`, can hold messages longer than this counts, and its close
|
|
314
|
+
// can then overtake them.
|
|
315
|
+
protected countDownBacklog(): void {
|
|
316
|
+
RAL().timer.setImmediate(() => {
|
|
317
|
+
this.backlog--;
|
|
318
|
+
if (this.backlog > 0) {
|
|
319
|
+
this.countDownBacklog();
|
|
320
|
+
} else if (this.closePending) {
|
|
321
|
+
this.closeEmitter.fire();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* A {@link PostMessageWriter} whose dispose, which a `MessageConnection`'s
|
|
329
|
+
* `dispose` and `end` both reach, signals this end's close to the peer.
|
|
330
|
+
*/
|
|
331
|
+
class MessagePortWriter extends PostMessageWriter {
|
|
332
|
+
constructor(protected override readonly channel: MessagePortChannel) {
|
|
333
|
+
super(channel);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
override dispose(): void {
|
|
337
|
+
this.channel.close();
|
|
338
|
+
super.dispose();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Build the reader/writer pair for a head's `MessagePort`, at either end.
|
|
344
|
+
*
|
|
345
|
+
* Disposing the writer (which the connection's own `dispose` does) posts a
|
|
346
|
+
* close signal after everything it wrote, and the other end's reader and writer
|
|
347
|
+
* then fire close, so a head tears down on its client's dispose as it does on a
|
|
348
|
+
* socket's close. For a connection at its default parallelism and message
|
|
349
|
+
* strategy, the close fires only once that end's connection has dispatched
|
|
350
|
+
* every message sent before the signal, under either `vscode-jsonrpc` runtime,
|
|
351
|
+
* so a `closeSession` sent just before the dispose still ends its session as
|
|
352
|
+
* closed. Nothing reports a page or a worker that dies, because the port
|
|
353
|
+
* cannot.
|
|
354
|
+
*
|
|
355
|
+
* **The transport and the connection must come from the same copy of
|
|
356
|
+
* `vscode-jsonrpc`.** The close is scheduled through the runtime abstraction
|
|
357
|
+
* layer of the copy this package imports. If the host builds its connection
|
|
358
|
+
* from another copy, that layer was never installed: the first message it
|
|
359
|
+
* delivers throws, and the peer's close never fires.
|
|
360
|
+
*
|
|
361
|
+
* **Both ends must use this.** A plain `BrowserMessageReader` peer hands the
|
|
362
|
+
* signal to its connection as a message it does not understand, and its own
|
|
363
|
+
* dispose posts nothing, so this end never learns that it went.
|
|
364
|
+
*
|
|
365
|
+
* Starts the port, because a listener added with `addEventListener` does not.
|
|
366
|
+
* From then on a message that arrives before the connection listens is dropped,
|
|
367
|
+
* so listen in the same task.
|
|
368
|
+
*/
|
|
369
|
+
export function createMessagePortTransport(port: TransferredMessagePort): PostMessageTransport {
|
|
370
|
+
const channel = new MessagePortChannel(port);
|
|
371
|
+
const reader = new PostMessageReader(channel);
|
|
372
|
+
const writer = new MessagePortWriter(channel);
|
|
373
|
+
port.start();
|
|
374
|
+
return {
|
|
375
|
+
reader,
|
|
376
|
+
writer,
|
|
377
|
+
dispose(): void {
|
|
378
|
+
reader.dispose();
|
|
379
|
+
writer.dispose();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import type { MessageConnection } from 'vscode-jsonrpc';
|
|
11
|
+
import { type ResolvedMessage, defineMessage, describeError, resolve } from '../messages/primitives';
|
|
12
|
+
import { type RpcProxy, createRpcProxy } from '../rpc';
|
|
13
|
+
import type { DataPort } from './data-port';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The transport never opened. A complete sentence rather than a fragment: a
|
|
17
|
+
* fragment is nested inside a sentence the framework does not own, so no
|
|
18
|
+
* translator controls the whole and the composition cannot be made to read
|
|
19
|
+
* correctly in every language.
|
|
20
|
+
*/
|
|
21
|
+
export const DATA_SERVER_CONNECT_FAILED = defineMessage(
|
|
22
|
+
'hydranium/protocol/data-server-connect-failed',
|
|
23
|
+
'Could not connect to the data server: {detail}'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const DATA_SERVER_NOT_READY = defineMessage(
|
|
27
|
+
'hydranium/protocol/data-server-not-ready',
|
|
28
|
+
'The data server did not become ready: {detail}'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/** The one method a connection needs of any server: its startup gate. */
|
|
32
|
+
export interface ReadyServer {
|
|
33
|
+
waitForReady(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Lifecycle reporting, for a host that raises warm-up UI around the two waits.
|
|
38
|
+
*
|
|
39
|
+
* Nothing fires until something asks for the connection — the generation is
|
|
40
|
+
* built on the first {@link RpcConnection.connected}. A host that wants the
|
|
41
|
+
* sequence at startup drives that call itself, or a workspace where nobody
|
|
42
|
+
* opens a document reports neither the connect nor the readiness its UI waits
|
|
43
|
+
* on.
|
|
44
|
+
*/
|
|
45
|
+
export interface RpcConnectionLifecycle {
|
|
46
|
+
/** A generation is opening its transport, including on each reconnect. */
|
|
47
|
+
readonly onConnecting?: () => void;
|
|
48
|
+
/** The server's readiness gate has settled for a generation. */
|
|
49
|
+
readonly onReady?: () => void;
|
|
50
|
+
/**
|
|
51
|
+
* A generation failed to connect or to become ready. The failure is still
|
|
52
|
+
* reported through {@link DataPort.reportError} and still rejects the
|
|
53
|
+
* awaiting caller; this is for a host that also drives its own UI.
|
|
54
|
+
*/
|
|
55
|
+
readonly onFailed?: (error: unknown) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Calls each distinct lifecycle's hooks in order; one passed twice is called once. */
|
|
59
|
+
function composeLifecycles(...lifecycles: (RpcConnectionLifecycle | undefined)[]): RpcConnectionLifecycle {
|
|
60
|
+
const present = [...new Set(lifecycles)].filter((lifecycle): lifecycle is RpcConnectionLifecycle => lifecycle !== undefined);
|
|
61
|
+
return {
|
|
62
|
+
onConnecting: () => present.forEach(lifecycle => lifecycle.onConnecting?.()),
|
|
63
|
+
onReady: () => present.forEach(lifecycle => lifecycle.onReady?.()),
|
|
64
|
+
onFailed: error => present.forEach(lifecycle => lifecycle.onFailed?.(error))
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Everything {@link RpcConnection} needs once a subclass has resolved its defaults. */
|
|
69
|
+
export interface ResolvedRpcConnectionOptions<TClient extends object> {
|
|
70
|
+
readonly methodNamespace: string;
|
|
71
|
+
readonly clientMethods: readonly (keyof TClient & string)[];
|
|
72
|
+
readonly lifecycle: RpcConnectionLifecycle;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One connection generation: its connection, its proxy, and its readiness.
|
|
77
|
+
*
|
|
78
|
+
* Named by the `protected` reconnect seams {@link RpcConnection.currentGeneration}
|
|
79
|
+
* and {@link RpcConnection.awaitReady}, so an override has to name it too.
|
|
80
|
+
*/
|
|
81
|
+
export interface RpcConnectionGeneration<TServer extends object> {
|
|
82
|
+
readonly connection: Promise<MessageConnection>;
|
|
83
|
+
readonly server: RpcProxy<TServer>;
|
|
84
|
+
/** Set on first use; the shared readiness gate for this generation. */
|
|
85
|
+
ready?: Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* One JSON-RPC connection to a head, with the three jobs every host adapter
|
|
90
|
+
* would otherwise re-derive above {@link DataPort}:
|
|
91
|
+
*
|
|
92
|
+
* 1. **Build the typed proxy** over the port's connection, with the caller's
|
|
93
|
+
* wire prefix and client-method allowlist.
|
|
94
|
+
* 2. **Own the readiness gate** — `waitForReady` once per connection, shared
|
|
95
|
+
* across concurrent callers. A client can connect before the workspace walk
|
|
96
|
+
* finishes, and an early request is then answered correctly from an empty
|
|
97
|
+
* registry, which reads as a broken project tier rather than as a race.
|
|
98
|
+
* 3. **Own the reconnect policy**, by dropping its generation when the port
|
|
99
|
+
* disposes and building a fresh one on the next request.
|
|
100
|
+
*
|
|
101
|
+
* Bounded only by {@link ReadyServer}, so a head serving a slice of the data
|
|
102
|
+
* protocol — diagnostics alone, or one with methods excluded — is still a
|
|
103
|
+
* legal server here. `DataConnection` narrows the bound because its sessions
|
|
104
|
+
* call the document methods; nothing at this layer does.
|
|
105
|
+
*/
|
|
106
|
+
export class RpcConnection<TServer extends ReadyServer, TClient extends object> {
|
|
107
|
+
protected readonly methodNamespace: string;
|
|
108
|
+
protected readonly clientMethods: readonly (keyof TClient & string)[];
|
|
109
|
+
protected readonly lifecycle: RpcConnectionLifecycle;
|
|
110
|
+
/** The current generation, or `undefined` before the first request / after a teardown. */
|
|
111
|
+
protected generation?: RpcConnectionGeneration<TServer>;
|
|
112
|
+
protected disposed = false;
|
|
113
|
+
protected readonly portDisposeListener: { dispose(): void };
|
|
114
|
+
|
|
115
|
+
constructor(
|
|
116
|
+
protected readonly port: DataPort,
|
|
117
|
+
protected readonly client: TClient,
|
|
118
|
+
options: ResolvedRpcConnectionOptions<TClient>
|
|
119
|
+
) {
|
|
120
|
+
this.methodNamespace = options.methodNamespace;
|
|
121
|
+
this.clientMethods = options.clientMethods;
|
|
122
|
+
this.lifecycle = composeLifecycles(port.connectionLifecycle, options.lifecycle);
|
|
123
|
+
this.portDisposeListener = this.port.onDispose(() => this.dropGeneration());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The connected, READY server proxy.
|
|
128
|
+
*
|
|
129
|
+
* Returns the proxy rather than `void` on purpose. A reconnect replaces the
|
|
130
|
+
* proxy, so a caller that cached one from an earlier call would go on
|
|
131
|
+
* addressing a dead connection with no error — handing it back per call
|
|
132
|
+
* makes the stale reference unrepresentable.
|
|
133
|
+
*
|
|
134
|
+
* Concurrent callers share one readiness promise, so `waitForReady` is
|
|
135
|
+
* awaited once per generation and not once per caller.
|
|
136
|
+
*/
|
|
137
|
+
async connected(): Promise<RpcProxy<TServer>> {
|
|
138
|
+
this.assertLive();
|
|
139
|
+
const generation = this.currentGeneration();
|
|
140
|
+
if (!generation.ready) {
|
|
141
|
+
generation.ready = this.awaitReady(generation);
|
|
142
|
+
}
|
|
143
|
+
await generation.ready;
|
|
144
|
+
// Dropped while it opened: its transport is gone, so the call goes to the
|
|
145
|
+
// next generation, or rejects once this connection is disposed.
|
|
146
|
+
if (this.generation !== generation) {
|
|
147
|
+
return this.connected();
|
|
148
|
+
}
|
|
149
|
+
return generation.server;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The current generation's proxy WITHOUT awaiting readiness — calls queue
|
|
154
|
+
* against the connection promise.
|
|
155
|
+
*
|
|
156
|
+
* **Protected, and that is the point.** An early request is answered
|
|
157
|
+
* correctly from a registry the workspace walk has not filled yet, which
|
|
158
|
+
* reads as a broken project tier rather than as a race — so every caller has
|
|
159
|
+
* to interpose the gate, and every caller forgetting to is a silent bug.
|
|
160
|
+
* {@link connected} is the public route and returns this same proxy once the
|
|
161
|
+
* gate has settled, which leaves nothing to forget. The one caller that
|
|
162
|
+
* cannot use it is {@link awaitReady}, whose whole job is running the gate.
|
|
163
|
+
*
|
|
164
|
+
* Read per access, never cached: a reconnect replaces the generation, and a
|
|
165
|
+
* held reference would address the dead one.
|
|
166
|
+
*/
|
|
167
|
+
protected get server(): RpcProxy<TServer> {
|
|
168
|
+
this.assertLive();
|
|
169
|
+
return this.currentGeneration().server;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Surface a failure the way this host does, through the port's sink.
|
|
174
|
+
*
|
|
175
|
+
* Here rather than only on the port so every participant sharing the
|
|
176
|
+
* connection reports through one route without being handed the transport.
|
|
177
|
+
*/
|
|
178
|
+
reportError(error: unknown, reported: ResolvedMessage): void {
|
|
179
|
+
this.port.reportError(error, reported);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Tear down the current connection and stop tracking the port. Idempotent. */
|
|
183
|
+
dispose(): void {
|
|
184
|
+
if (this.disposed) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.disposed = true;
|
|
188
|
+
this.portDisposeListener.dispose();
|
|
189
|
+
this.dropGeneration();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The live generation, building one if there is none. */
|
|
193
|
+
protected currentGeneration(): RpcConnectionGeneration<TServer> {
|
|
194
|
+
if (this.generation) {
|
|
195
|
+
return this.generation;
|
|
196
|
+
}
|
|
197
|
+
this.lifecycle.onConnecting?.();
|
|
198
|
+
const connection = this.port.connect();
|
|
199
|
+
// Rejection is reported here rather than left to float: an unhandled
|
|
200
|
+
// rejection on a connection promise is the failure mode that reads as
|
|
201
|
+
// "the model is empty" instead of "the transport never opened".
|
|
202
|
+
connection.catch((error: unknown) =>
|
|
203
|
+
this.port.reportError(error, resolve(DATA_SERVER_CONNECT_FAILED, { detail: describeError(error) }))
|
|
204
|
+
);
|
|
205
|
+
const server = createRpcProxy<TServer, TClient>(connection, {
|
|
206
|
+
methodNamespace: this.methodNamespace,
|
|
207
|
+
localTarget: this.localTarget(),
|
|
208
|
+
localMethods: this.clientMethods
|
|
209
|
+
});
|
|
210
|
+
this.generation = { connection, server };
|
|
211
|
+
return this.generation;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The object whose {@link clientMethods} answer the server on each new
|
|
216
|
+
* generation: {@link client} itself. A subclass that must see a call before
|
|
217
|
+
* the client does returns an object that forwards each of them to it.
|
|
218
|
+
*/
|
|
219
|
+
protected localTarget(): TClient {
|
|
220
|
+
return this.client;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Await the connection and the server's startup gate for one generation. */
|
|
224
|
+
protected async awaitReady(generation: RpcConnectionGeneration<TServer>): Promise<void> {
|
|
225
|
+
try {
|
|
226
|
+
await generation.connection;
|
|
227
|
+
// Dropped meanwhile: writing the request would hit a closed transport.
|
|
228
|
+
if (this.generation !== generation) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await generation.server.waitForReady();
|
|
232
|
+
// Before `onReady`, so a failing restore reports a failure alone.
|
|
233
|
+
this.generationReady(generation);
|
|
234
|
+
this.lifecycle.onReady?.();
|
|
235
|
+
} catch (error: unknown) {
|
|
236
|
+
// A dropped generation's request fails with its transport, which says
|
|
237
|
+
// nothing about whether the server is ready.
|
|
238
|
+
if (this.generation !== generation) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.lifecycle.onFailed?.(error);
|
|
242
|
+
// Drop the generation so the next request retries rather than
|
|
243
|
+
// re-awaiting a settled rejection forever.
|
|
244
|
+
if (this.generation === generation) {
|
|
245
|
+
this.generation = undefined;
|
|
246
|
+
}
|
|
247
|
+
this.port.reportError(error, resolve(DATA_SERVER_NOT_READY, { detail: describeError(error) }));
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Called once `generation` has passed its readiness gate, whichever request
|
|
254
|
+
* brought it up. A generation can replace one that failed at readiness
|
|
255
|
+
* without {@link dropGeneration}, so this, not a drop, is where state kept
|
|
256
|
+
* on the server is put back. It runs inside the readiness gate, so a throw
|
|
257
|
+
* from an override fails the generation as a refused gate would.
|
|
258
|
+
*/
|
|
259
|
+
protected generationReady(_generation: RpcConnectionGeneration<TServer>): void {
|
|
260
|
+
// Nothing to put back at this layer.
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Discard the current generation, disposing its connection if it opened.
|
|
265
|
+
* The next {@link connected} builds a fresh one.
|
|
266
|
+
*/
|
|
267
|
+
protected dropGeneration(): void {
|
|
268
|
+
const generation = this.generation;
|
|
269
|
+
this.generation = undefined;
|
|
270
|
+
if (!generation) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
generation.connection.then(connection => connection.dispose()).catch(() => undefined);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
protected assertLive(): void {
|
|
277
|
+
if (this.disposed) {
|
|
278
|
+
throw new Error(`${this.constructor.name} is disposed`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|