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