@hydranium/protocol 1.0.0-next.10
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/LICENSE +21 -0
- package/README.md +81 -0
- package/lib/abstract-logger.d.ts +66 -0
- package/lib/abstract-logger.d.ts.map +1 -0
- package/lib/abstract-logger.js +110 -0
- package/lib/abstract-logger.js.map +1 -0
- package/lib/browser-runtime.d.ts +124 -0
- package/lib/browser-runtime.d.ts.map +1 -0
- package/lib/browser-runtime.js +11 -0
- package/lib/browser-runtime.js.map +1 -0
- package/lib/client/data-events.d.ts +49 -0
- package/lib/client/data-events.d.ts.map +1 -0
- package/lib/client/data-events.js +60 -0
- package/lib/client/data-events.js.map +1 -0
- package/lib/client/data-port.d.ts +95 -0
- package/lib/client/data-port.d.ts.map +1 -0
- package/lib/client/data-port.js +11 -0
- package/lib/client/data-port.js.map +1 -0
- package/lib/client/data-session.d.ts +129 -0
- package/lib/client/data-session.d.ts.map +1 -0
- package/lib/client/data-session.js +179 -0
- package/lib/client/data-session.js.map +1 -0
- package/lib/client/index.d.ts +39 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +55 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/message-relay.d.ts +111 -0
- package/lib/client/message-relay.d.ts.map +1 -0
- package/lib/client/message-relay.js +139 -0
- package/lib/client/message-relay.js.map +1 -0
- package/lib/client/post-message-transport.d.ts +77 -0
- package/lib/client/post-message-transport.d.ts.map +1 -0
- package/lib/client/post-message-transport.js +95 -0
- package/lib/client/post-message-transport.js.map +1 -0
- package/lib/clock.d.ts +111 -0
- package/lib/clock.d.ts.map +1 -0
- package/lib/clock.js +74 -0
- package/lib/clock.js.map +1 -0
- package/lib/data/data-protocol-methods.d.ts +39 -0
- package/lib/data/data-protocol-methods.d.ts.map +1 -0
- package/lib/data/data-protocol-methods.js +61 -0
- package/lib/data/data-protocol-methods.js.map +1 -0
- package/lib/data/data-server-protocol.d.ts +264 -0
- package/lib/data/data-server-protocol.d.ts.map +1 -0
- package/lib/data/data-server-protocol.js +11 -0
- package/lib/data/data-server-protocol.js.map +1 -0
- package/lib/data/diagnostics.d.ts +97 -0
- package/lib/data/diagnostics.d.ts.map +1 -0
- package/lib/data/diagnostics.js +25 -0
- package/lib/data/diagnostics.js.map +1 -0
- package/lib/data/events.d.ts +107 -0
- package/lib/data/events.d.ts.map +1 -0
- package/lib/data/events.js +11 -0
- package/lib/data/events.js.map +1 -0
- package/lib/data/index.d.ts +15 -0
- package/lib/data/index.d.ts.map +1 -0
- package/lib/data/index.js +38 -0
- package/lib/data/index.js.map +1 -0
- package/lib/data/methods.d.ts +57 -0
- package/lib/data/methods.d.ts.map +1 -0
- package/lib/data/methods.js +60 -0
- package/lib/data/methods.js.map +1 -0
- package/lib/data/requests.d.ts +65 -0
- package/lib/data/requests.d.ts.map +1 -0
- package/lib/data/requests.js +11 -0
- package/lib/data/requests.js.map +1 -0
- package/lib/debouncer.d.ts +75 -0
- package/lib/debouncer.d.ts.map +1 -0
- package/lib/debouncer.js +91 -0
- package/lib/debouncer.js.map +1 -0
- package/lib/errors.d.ts +84 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +113 -0
- package/lib/errors.js.map +1 -0
- package/lib/host-diagnostics.d.ts +58 -0
- package/lib/host-diagnostics.d.ts.map +1 -0
- package/lib/host-diagnostics.js +18 -0
- package/lib/host-diagnostics.js.map +1 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/latency-collector.d.ts +126 -0
- package/lib/latency-collector.d.ts.map +1 -0
- package/lib/latency-collector.js +137 -0
- package/lib/latency-collector.js.map +1 -0
- package/lib/logger.d.ts +119 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +126 -0
- package/lib/logger.js.map +1 -0
- package/lib/model-server.d.ts +352 -0
- package/lib/model-server.d.ts.map +1 -0
- package/lib/model-server.js +160 -0
- package/lib/model-server.js.map +1 -0
- package/lib/model-service/args.d.ts +64 -0
- package/lib/model-service/args.d.ts.map +1 -0
- package/lib/model-service/args.js +11 -0
- package/lib/model-service/args.js.map +1 -0
- package/lib/model-service/index.d.ts +11 -0
- package/lib/model-service/index.d.ts.map +1 -0
- package/lib/model-service/index.js +31 -0
- package/lib/model-service/index.js.map +1 -0
- package/lib/model-service/reference-candidate.d.ts +56 -0
- package/lib/model-service/reference-candidate.d.ts.map +1 -0
- package/lib/model-service/reference-candidate.js +11 -0
- package/lib/model-service/reference-candidate.js.map +1 -0
- package/lib/noop-logger.d.ts +25 -0
- package/lib/noop-logger.d.ts.map +1 -0
- package/lib/noop-logger.js +36 -0
- package/lib/noop-logger.js.map +1 -0
- package/lib/observable-value.d.ts +72 -0
- package/lib/observable-value.d.ts.map +1 -0
- package/lib/observable-value.js +52 -0
- package/lib/observable-value.js.map +1 -0
- package/lib/patch-merge.d.ts +123 -0
- package/lib/patch-merge.d.ts.map +1 -0
- package/lib/patch-merge.js +104 -0
- package/lib/patch-merge.js.map +1 -0
- package/lib/profile-session.d.ts +87 -0
- package/lib/profile-session.d.ts.map +1 -0
- package/lib/profile-session.js +98 -0
- package/lib/profile-session.js.map +1 -0
- package/lib/profiling.d.ts +83 -0
- package/lib/profiling.d.ts.map +1 -0
- package/lib/profiling.js +20 -0
- package/lib/profiling.js.map +1 -0
- package/lib/project.d.ts +121 -0
- package/lib/project.d.ts.map +1 -0
- package/lib/project.js +34 -0
- package/lib/project.js.map +1 -0
- package/lib/rpc/bind-rpc-methods.d.ts +100 -0
- package/lib/rpc/bind-rpc-methods.d.ts.map +1 -0
- package/lib/rpc/bind-rpc-methods.js +110 -0
- package/lib/rpc/bind-rpc-methods.js.map +1 -0
- package/lib/rpc/create-rpc-proxy.d.ts +188 -0
- package/lib/rpc/create-rpc-proxy.d.ts.map +1 -0
- package/lib/rpc/create-rpc-proxy.js +158 -0
- package/lib/rpc/create-rpc-proxy.js.map +1 -0
- package/lib/rpc/index.d.ts +12 -0
- package/lib/rpc/index.d.ts.map +1 -0
- package/lib/rpc/index.js +31 -0
- package/lib/rpc/index.js.map +1 -0
- package/lib/rpc/wire-prefix.d.ts +37 -0
- package/lib/rpc/wire-prefix.d.ts.map +1 -0
- package/lib/rpc/wire-prefix.js +49 -0
- package/lib/rpc/wire-prefix.js.map +1 -0
- package/lib/testing/data-doubles.d.ts +121 -0
- package/lib/testing/data-doubles.d.ts.map +1 -0
- package/lib/testing/data-doubles.js +102 -0
- package/lib/testing/data-doubles.js.map +1 -0
- package/lib/testing/fake-clock.d.ts +35 -0
- package/lib/testing/fake-clock.d.ts.map +1 -0
- package/lib/testing/fake-clock.js +86 -0
- package/lib/testing/fake-clock.js.map +1 -0
- package/lib/testing/harness.d.ts +24 -0
- package/lib/testing/harness.d.ts.map +1 -0
- package/lib/testing/harness.js +11 -0
- package/lib/testing/harness.js.map +1 -0
- package/lib/testing/index.d.ts +13 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +41 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/node/duplex-connection.d.ts +25 -0
- package/lib/testing/node/duplex-connection.d.ts.map +1 -0
- package/lib/testing/node/duplex-connection.js +34 -0
- package/lib/testing/node/duplex-connection.js.map +1 -0
- package/lib/testing/node/duplex-stream.d.ts +32 -0
- package/lib/testing/node/duplex-stream.d.ts.map +1 -0
- package/lib/testing/node/duplex-stream.js +35 -0
- package/lib/testing/node/duplex-stream.js.map +1 -0
- package/lib/testing/node/index.d.ts +11 -0
- package/lib/testing/node/index.d.ts.map +1 -0
- package/lib/testing/node/index.js +37 -0
- package/lib/testing/node/index.js.map +1 -0
- package/lib/testing/wait-for.d.ts +69 -0
- package/lib/testing/wait-for.d.ts.map +1 -0
- package/lib/testing/wait-for.js +67 -0
- package/lib/testing/wait-for.js.map +1 -0
- package/lib/tracer.d.ts +115 -0
- package/lib/tracer.d.ts.map +1 -0
- package/lib/tracer.js +205 -0
- package/lib/tracer.js.map +1 -0
- package/lib/transfer-diagnostic.d.ts +73 -0
- package/lib/transfer-diagnostic.d.ts.map +1 -0
- package/lib/transfer-diagnostic.js +45 -0
- package/lib/transfer-diagnostic.js.map +1 -0
- package/lib/transfer-document.d.ts +73 -0
- package/lib/transfer-document.d.ts.map +1 -0
- package/lib/transfer-document.js +46 -0
- package/lib/transfer-document.js.map +1 -0
- package/lib/transfer-element.d.ts +39 -0
- package/lib/transfer-element.d.ts.map +1 -0
- package/lib/transfer-element.js +11 -0
- package/lib/transfer-element.js.map +1 -0
- package/lib/uri.d.ts +62 -0
- package/lib/uri.d.ts.map +1 -0
- package/lib/uri.js +28 -0
- package/lib/uri.js.map +1 -0
- package/lib/util.d.ts +147 -0
- package/lib/util.d.ts.map +1 -0
- package/lib/util.js +211 -0
- package/lib/util.js.map +1 -0
- package/package.json +100 -0
- package/src/abstract-logger.ts +120 -0
- package/src/browser-runtime.ts +135 -0
- package/src/client/data-events.ts +71 -0
- package/src/client/data-port.ts +99 -0
- package/src/client/data-session.ts +209 -0
- package/src/client/index.ts +40 -0
- package/src/client/message-relay.ts +216 -0
- package/src/client/post-message-transport.ts +167 -0
- package/src/clock.ts +173 -0
- package/src/data/data-protocol-methods.ts +79 -0
- package/src/data/data-server-protocol.ts +296 -0
- package/src/data/diagnostics.ts +116 -0
- package/src/data/events.ts +127 -0
- package/src/data/index.ts +23 -0
- package/src/data/methods.ts +59 -0
- package/src/data/requests.ts +70 -0
- package/src/debouncer.ts +118 -0
- package/src/errors.ts +127 -0
- package/src/host-diagnostics.ts +63 -0
- package/src/index.ts +41 -0
- package/src/latency-collector.ts +231 -0
- package/src/logger.ts +172 -0
- package/src/model-server.ts +479 -0
- package/src/model-service/args.ts +67 -0
- package/src/model-service/index.ts +16 -0
- package/src/model-service/reference-candidate.ts +58 -0
- package/src/noop-logger.ts +36 -0
- package/src/observable-value.ts +92 -0
- package/src/patch-merge.ts +165 -0
- package/src/profile-session.ts +164 -0
- package/src/profiling.ts +113 -0
- package/src/project.ts +125 -0
- package/src/rpc/README.md +163 -0
- package/src/rpc/bind-rpc-methods.ts +180 -0
- package/src/rpc/create-rpc-proxy.ts +292 -0
- package/src/rpc/index.ts +16 -0
- package/src/rpc/wire-prefix.ts +48 -0
- package/src/testing/data-doubles.ts +178 -0
- package/src/testing/fake-clock.ts +106 -0
- package/src/testing/harness.ts +24 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/node/duplex-connection.ts +50 -0
- package/src/testing/node/duplex-stream.ts +51 -0
- package/src/testing/node/index.ts +22 -0
- package/src/testing/wait-for.ts +94 -0
- package/src/tracer.ts +277 -0
- package/src/transfer-diagnostic.ts +92 -0
- package/src/transfer-document.ts +99 -0
- package/src/transfer-element.ts +43 -0
- package/src/uri.ts +61 -0
- package/src/util.ts +250 -0
|
@@ -0,0 +1,209 @@
|
|
|
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 { DATA_CLIENT_PROTOCOL_METHODS, DATA_SERVER_WIRE_PREFIX, type DataClientProtocol, type DataServerProtocol } from '../data';
|
|
12
|
+
import { type RpcProxy, createRpcProxy } from '../rpc';
|
|
13
|
+
import type { TransferDocument } from '../transfer-document';
|
|
14
|
+
import type { TransferElement } from '../transfer-element';
|
|
15
|
+
import type { DataPort } from './data-port';
|
|
16
|
+
|
|
17
|
+
/** Options for {@link DataSession}. */
|
|
18
|
+
export interface DataSessionOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Wire namespace the server is addressed under. Defaults to the
|
|
21
|
+
* framework's {@link DATA_SERVER_WIRE_PREFIX}, which is what an unmodified
|
|
22
|
+
* `DataServer` binds. Override only alongside the server's own
|
|
23
|
+
* `methodNamespace` option — a mismatch turns every request into
|
|
24
|
+
* "Unhandled method" rather than failing at wire-up.
|
|
25
|
+
*/
|
|
26
|
+
readonly methodNamespace?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One connection generation: its connection, its proxy, and its readiness. */
|
|
30
|
+
interface Generation<TTransfer extends TransferElement> {
|
|
31
|
+
readonly connection: Promise<MessageConnection>;
|
|
32
|
+
readonly server: RpcProxy<DataServerProtocol<TTransfer>>;
|
|
33
|
+
/** Set on first use; the shared readiness gate for this generation. */
|
|
34
|
+
ready?: Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The host-invariant half of talking to the data head: everything above
|
|
39
|
+
* {@link DataPort} that would otherwise be re-derived by every host
|
|
40
|
+
* adapter.
|
|
41
|
+
*
|
|
42
|
+
* Three jobs, and deliberately no fourth:
|
|
43
|
+
*
|
|
44
|
+
* 1. **Build the typed proxy** over the port's connection, with the framework's
|
|
45
|
+
* wire prefix and its drift-proof client-method allowlist.
|
|
46
|
+
* 2. **Own the readiness gate** — `waitForReady` once per connection, shared
|
|
47
|
+
* across concurrent callers. A socket client can connect before the
|
|
48
|
+
* workspace walk finishes, and an early request is then answered correctly
|
|
49
|
+
* from an empty registry, which reads as a broken project tier rather than
|
|
50
|
+
* as a race.
|
|
51
|
+
* 3. **Own the reconnect policy**, by dropping its connection generation when
|
|
52
|
+
* the port disposes and building a fresh one on the next request.
|
|
53
|
+
*
|
|
54
|
+
* It does **not** wrap the protocol methods; callers reach them through
|
|
55
|
+
* {@link connected}. The one exception is {@link openDocument}, which exists
|
|
56
|
+
* because the open/watch *order* is silently wrong the other way round — see
|
|
57
|
+
* its own doc.
|
|
58
|
+
*
|
|
59
|
+
* Generic over the transfer root so this file names no grammar. An adopter
|
|
60
|
+
* binds the concrete root (or the union of them, for a multi-grammar head) at
|
|
61
|
+
* its own edge.
|
|
62
|
+
*/
|
|
63
|
+
export class DataSession<TTransfer extends TransferElement> {
|
|
64
|
+
protected readonly methodNamespace: string;
|
|
65
|
+
/** The current generation, or `undefined` before the first request / after a teardown. */
|
|
66
|
+
protected generation?: Generation<TTransfer>;
|
|
67
|
+
protected disposed = false;
|
|
68
|
+
protected readonly portDisposeListener: { dispose(): void };
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
protected readonly port: DataPort,
|
|
72
|
+
protected readonly client: DataClientProtocol<TTransfer>,
|
|
73
|
+
options: DataSessionOptions = {}
|
|
74
|
+
) {
|
|
75
|
+
this.methodNamespace = options.methodNamespace ?? DATA_SERVER_WIRE_PREFIX;
|
|
76
|
+
this.portDisposeListener = this.port.onDispose(() => this.dropGeneration());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The identity every request is made under — the port's, not a second one. */
|
|
80
|
+
get clientId(): string {
|
|
81
|
+
return this.port.clientId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The connected, READY server proxy.
|
|
86
|
+
*
|
|
87
|
+
* Returns the proxy rather than `void` on purpose. A reconnect replaces the
|
|
88
|
+
* proxy, so a caller that cached one from an earlier call would go on
|
|
89
|
+
* addressing a dead connection with no error — handing it back per call
|
|
90
|
+
* makes the stale reference unrepresentable.
|
|
91
|
+
*
|
|
92
|
+
* Concurrent callers share one readiness promise, so `waitForReady` is
|
|
93
|
+
* awaited once per generation and not once per caller.
|
|
94
|
+
*/
|
|
95
|
+
async connected(): Promise<RpcProxy<DataServerProtocol<TTransfer>>> {
|
|
96
|
+
if (this.disposed) {
|
|
97
|
+
throw new Error('DataSession is disposed');
|
|
98
|
+
}
|
|
99
|
+
const generation = this.currentGeneration();
|
|
100
|
+
if (!generation.ready) {
|
|
101
|
+
generation.ready = this.awaitReady(generation);
|
|
102
|
+
}
|
|
103
|
+
await generation.ready;
|
|
104
|
+
return generation.server;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Open `uri` for editing and start watching it, in that order, returning
|
|
109
|
+
* the opened snapshot.
|
|
110
|
+
*
|
|
111
|
+
* **The order is the whole reason this method exists.**
|
|
112
|
+
* `watchModelDocument` baselines its dedup fingerprint from the *current*
|
|
113
|
+
* document, but only if one exists. Watching first therefore leaves no
|
|
114
|
+
* baseline, and the first phase event after the open arrives as a spurious
|
|
115
|
+
* `'changed'` — which a widget that resets its in-memory root to the server
|
|
116
|
+
* view misreads as a concurrent third-party write, losing whatever the user
|
|
117
|
+
* had typed. Nothing about the wrong order fails loudly, so it is encoded
|
|
118
|
+
* here rather than documented and re-derived.
|
|
119
|
+
*
|
|
120
|
+
* Note that the returned snapshot's empty `diagnostics` does not mean
|
|
121
|
+
* valid: `open` settles at the integrity landmark, not at validation.
|
|
122
|
+
* Validity arrives asynchronously on `onDocumentUpdated`, or synchronously
|
|
123
|
+
* from `getModelDocument({ includeDiagnostics: true })`.
|
|
124
|
+
*/
|
|
125
|
+
async openDocument(uri: string): Promise<TransferDocument<TTransfer>> {
|
|
126
|
+
const server = await this.connected();
|
|
127
|
+
const document = await server.openModelDocument({ uri, clientId: this.clientId });
|
|
128
|
+
await server.watchModelDocument({ uri, clientId: this.clientId });
|
|
129
|
+
return document;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Close `uri`. The server unwatches implicitly, so this is the dual of
|
|
134
|
+
* {@link openDocument} and needs no separate unwatch.
|
|
135
|
+
*/
|
|
136
|
+
async closeDocument(uri: string): Promise<void> {
|
|
137
|
+
const server = await this.connected();
|
|
138
|
+
await server.closeModelDocument({ uri, clientId: this.clientId });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Whether `event.sourceClientId` identifies this session's own write.
|
|
143
|
+
*
|
|
144
|
+
* Every watcher needs this and the check is one comparison, so getting it
|
|
145
|
+
* wrong is cheap to do and expensive to find: an unfiltered echo looks
|
|
146
|
+
* exactly like a concurrent third-party edit.
|
|
147
|
+
*/
|
|
148
|
+
isOwnEcho(sourceClientId: string): boolean {
|
|
149
|
+
return sourceClientId === this.clientId;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Tear down the current connection and stop tracking the port. Idempotent. */
|
|
153
|
+
dispose(): void {
|
|
154
|
+
if (this.disposed) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.disposed = true;
|
|
158
|
+
this.portDisposeListener.dispose();
|
|
159
|
+
this.dropGeneration();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The live generation, building one if there is none. */
|
|
163
|
+
protected currentGeneration(): Generation<TTransfer> {
|
|
164
|
+
if (this.generation) {
|
|
165
|
+
return this.generation;
|
|
166
|
+
}
|
|
167
|
+
const connection = this.port.connect();
|
|
168
|
+
// Rejection is reported here rather than left to float: an unhandled
|
|
169
|
+
// rejection on a connection promise is the failure mode that reads as
|
|
170
|
+
// "the model is empty" instead of "the transport never opened".
|
|
171
|
+
connection.catch((error: unknown) => this.port.reportError(error, 'connecting to the data server'));
|
|
172
|
+
const server = createRpcProxy<DataServerProtocol<TTransfer>, DataClientProtocol<TTransfer>>(connection, {
|
|
173
|
+
methodNamespace: this.methodNamespace,
|
|
174
|
+
localTarget: this.client,
|
|
175
|
+
localMethods: DATA_CLIENT_PROTOCOL_METHODS
|
|
176
|
+
});
|
|
177
|
+
this.generation = { connection, server };
|
|
178
|
+
return this.generation;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Await the connection and the server's startup gate for one generation. */
|
|
182
|
+
protected async awaitReady(generation: Generation<TTransfer>): Promise<void> {
|
|
183
|
+
try {
|
|
184
|
+
await generation.connection;
|
|
185
|
+
await generation.server.waitForReady();
|
|
186
|
+
} catch (error: unknown) {
|
|
187
|
+
// Drop the generation so the next request retries rather than
|
|
188
|
+
// re-awaiting a settled rejection forever.
|
|
189
|
+
if (this.generation === generation) {
|
|
190
|
+
this.generation = undefined;
|
|
191
|
+
}
|
|
192
|
+
this.port.reportError(error, 'waiting for the data server to become ready');
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Discard the current generation, disposing its connection if it opened.
|
|
199
|
+
* The next {@link connected} builds a fresh one.
|
|
200
|
+
*/
|
|
201
|
+
protected dropGeneration(): void {
|
|
202
|
+
const generation = this.generation;
|
|
203
|
+
this.generation = undefined;
|
|
204
|
+
if (!generation) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
generation.connection.then(connection => connection.dispose()).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* Subpath barrel for `@hydranium/protocol/client` — the **host-neutral client
|
|
12
|
+
* tier** of the data head.
|
|
13
|
+
*
|
|
14
|
+
* Where `./data` is the wire *contract* and `./rpc` is the machinery that lowers
|
|
15
|
+
* it onto a connection, this is what a client wraps around both: the seam a host
|
|
16
|
+
* fills in (`DataPort`), the lifecycle above it (`DataSession` —
|
|
17
|
+
* readiness gate, open/watch ordering, echo recognition, reconnect), the inbound
|
|
18
|
+
* fan-out (`DataEvents`), and the two halves of the hop for hosts whose
|
|
19
|
+
* client cannot hold a socket — `createPostMessageTransport` on the client
|
|
20
|
+
* side and `relayToPostMessageChannel` on the side that does hold it.
|
|
21
|
+
*
|
|
22
|
+
* **Neutral, and gate-enforced so.** Nothing here imports a host package or a
|
|
23
|
+
* Node builtin, which is what lets one client tier serve a Theia frontend, a VS
|
|
24
|
+
* 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.mjs`
|
|
26
|
+
* carries the entries.
|
|
27
|
+
*
|
|
28
|
+
* The Theia-specific mounting of the same contract lives in
|
|
29
|
+
* `@hydranium/data-client-theia`: its `AbstractDataServiceFrontend` solves the
|
|
30
|
+
* same problem against Theia's channel transport, and its `EmitterDataClient`
|
|
31
|
+
* is the Theia-bound counterpart of `DataEvents`. Prefer this tier for
|
|
32
|
+
* anything new, and reach for the Theia package only for what genuinely needs
|
|
33
|
+
* Theia DI.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export * from './data-events';
|
|
37
|
+
export * from './data-port';
|
|
38
|
+
export * from './data-session';
|
|
39
|
+
export * from './message-relay';
|
|
40
|
+
export * from './post-message-transport';
|
|
@@ -0,0 +1,216 @@
|
|
|
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 { Emitter, type Disposable, type Event, type Message, type MessageReader, type MessageWriter } from 'vscode-jsonrpc';
|
|
11
|
+
import type { PostMessageChannel } from './post-message-transport';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The framed side of a relay: the reader/writer pair over whatever transport the
|
|
15
|
+
* host actually holds — a TCP socket to the data-server, a child process' stdio,
|
|
16
|
+
* a websocket.
|
|
17
|
+
*
|
|
18
|
+
* Structurally the same pair `PostMessageTransport` exposes, and
|
|
19
|
+
* deliberately so: either can stand on either side of a relay. What differs is
|
|
20
|
+
* only who owns the framing, which is why this tier never needs to know.
|
|
21
|
+
*/
|
|
22
|
+
export interface RelayTransport {
|
|
23
|
+
readonly reader: MessageReader;
|
|
24
|
+
readonly writer: MessageWriter;
|
|
25
|
+
/** Release the transport. Called when the relay is disposed. */
|
|
26
|
+
dispose?(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Options for {@link relayToPostMessageChannel}. */
|
|
30
|
+
export interface MessageRelayOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Surface a failure the way the host does. Same contract as
|
|
33
|
+
* `DataPort.reportError`: `context` names what was being attempted.
|
|
34
|
+
*
|
|
35
|
+
* A relay has no other way to report — it sits between two transports and
|
|
36
|
+
* owns neither, so a swallowed error here presents as a form that never
|
|
37
|
+
* populates.
|
|
38
|
+
*/
|
|
39
|
+
readonly reportError?: (error: unknown, context: string) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A live relay. Dispose to tear both directions down. */
|
|
43
|
+
export interface MessageRelay extends Disposable {
|
|
44
|
+
/**
|
|
45
|
+
* Resolves `true` once messages are flowing in both directions, `false` if
|
|
46
|
+
* opening the framed transport failed or the relay was disposed first.
|
|
47
|
+
*
|
|
48
|
+
* It resolves rather than rejects on failure so that a caller which never
|
|
49
|
+
* awaits it cannot produce an unhandled rejection; the error itself goes to
|
|
50
|
+
* {@link MessageRelayOptions.reportError}. Tests await it to get a
|
|
51
|
+
* deterministic "wiring is done" edge instead of polling.
|
|
52
|
+
*/
|
|
53
|
+
readonly wired: Promise<boolean>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fires when the framed side goes away — the data-server exiting, the socket
|
|
57
|
+
* erroring, a language-server restart taking its ports with it.
|
|
58
|
+
*
|
|
59
|
+
* **The relay cannot propagate this to the clone hop itself**, and the host
|
|
60
|
+
* has to. {@link PostMessageChannel} is deliberately an input-only contract:
|
|
61
|
+
* it offers `onClose` for observing the pipe but no `close()` for ending it,
|
|
62
|
+
* because a webview's pipe belongs to the webview's lifecycle, not to
|
|
63
|
+
* whoever relays over it. So a framed-side death is invisible to the far
|
|
64
|
+
* end, whose pending requests would otherwise hang forever with no rejection
|
|
65
|
+
* — the far side's `MessageConnection` sees an open pipe and no answer. On
|
|
66
|
+
* this event a host disposes the webview, reloads it, or sends its own
|
|
67
|
+
* "connection lost" notification; doing nothing is the one wrong choice.
|
|
68
|
+
*/
|
|
69
|
+
readonly onClose: Event<void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Pump JSON-RPC messages between a framed transport and a structured-clone
|
|
74
|
+
* {@link PostMessageChannel}, decoding neither.
|
|
75
|
+
*
|
|
76
|
+
* This is the extension-host half of the hop whose webview half is
|
|
77
|
+
* `createPostMessageTransport`. A webview has no `net`, so the data-server
|
|
78
|
+
* socket can only be held by the extension host; the host therefore has to move
|
|
79
|
+
* whole messages between a Content-Length-framed socket and a pipe that carries
|
|
80
|
+
* objects. Both sides speak the same JSON-RPC, so this moves messages and adds
|
|
81
|
+
* no semantics — there is no re-proxy in between, and `createRpcProxy` on the far
|
|
82
|
+
* side is unaware the relay exists.
|
|
83
|
+
*
|
|
84
|
+
* **No `MessageConnection` is built here, and that is what keeps the module
|
|
85
|
+
* browser-neutral.** A relay is not a JSON-RPC endpoint: it has no requests of
|
|
86
|
+
* its own, so it needs no message queue, hence no vscode-jsonrpc runtime
|
|
87
|
+
* abstraction layer. `reader.listen` / `writer.write` are enough. The caller
|
|
88
|
+
* imports the entrypoint that frames its own transport (`vscode-jsonrpc/node`
|
|
89
|
+
* for a socket) and the RAL that comes with it; this tier stays neutral and is
|
|
90
|
+
* gated so by `npm run check:neutral`. Contrast
|
|
91
|
+
* `@hydranium/data-client-theia`'s `SocketChannelForwarder`, which does build a
|
|
92
|
+
* connection only to borrow its `onClose`, and pays a Theia dependency for the
|
|
93
|
+
* byte coding this shape does not need.
|
|
94
|
+
*
|
|
95
|
+
* **The race this exists to close.** `openTransport` is asynchronous — a real
|
|
96
|
+
* host discovers a port first, then connects — while the clone pipe is usable
|
|
97
|
+
* immediately. `PostMessageChannel.onMessage` is a plain emitter with no replay,
|
|
98
|
+
* so a message sent during the connect window is dropped outright.
|
|
99
|
+
*
|
|
100
|
+
* It is not enough that a disciplined client awaits its own connection before
|
|
101
|
+
* sending: a host builds the far-side connection and hands it back *without*
|
|
102
|
+
* waiting for the relay, so `DataSession`'s very first call — the readiness
|
|
103
|
+
* handshake — is already in flight while the socket is still opening. Dropping
|
|
104
|
+
* that first request presents as a client hanging forever on connect against a
|
|
105
|
+
* perfectly healthy server.
|
|
106
|
+
*
|
|
107
|
+
* So the subscription is taken **synchronously, before the first await**, and
|
|
108
|
+
* buffers; the hand-off then swaps listeners and replays within one turn of the
|
|
109
|
+
* event loop, which is what makes it impossible for a message to be both
|
|
110
|
+
* buffered and forwarded, or to arrive between the two. Replay is FIFO because
|
|
111
|
+
* order is load-bearing — a `getModelDocument` that overtook its own
|
|
112
|
+
* `openModelDocument` would answer against an unopened document.
|
|
113
|
+
* `@hydranium/client-theia`'s `AbstractSocketForwardingConnectionHandler` carries the
|
|
114
|
+
* same fix for the Theia channel transport.
|
|
115
|
+
*/
|
|
116
|
+
export function relayToPostMessageChannel(
|
|
117
|
+
channel: PostMessageChannel,
|
|
118
|
+
openTransport: () => Promise<RelayTransport>,
|
|
119
|
+
options: MessageRelayOptions = {}
|
|
120
|
+
): MessageRelay {
|
|
121
|
+
const closeEmitter = new Emitter<void>();
|
|
122
|
+
const subscriptions: Disposable[] = [];
|
|
123
|
+
let transport: RelayTransport | undefined;
|
|
124
|
+
let disposed = false;
|
|
125
|
+
|
|
126
|
+
// Taken before the first await, so nothing the far side sends during the
|
|
127
|
+
// connect window is lost. See the race note above.
|
|
128
|
+
const buffered: Message[] = [];
|
|
129
|
+
let bufferSubscription: Disposable | undefined = channel.onMessage(message => buffered.push(message));
|
|
130
|
+
|
|
131
|
+
const closeFramedSide = (): void => {
|
|
132
|
+
if (disposed) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
closeEmitter.fire();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const wired = (async (): Promise<boolean> => {
|
|
139
|
+
let opened: RelayTransport;
|
|
140
|
+
try {
|
|
141
|
+
opened = await openTransport();
|
|
142
|
+
} catch (error: unknown) {
|
|
143
|
+
bufferSubscription?.dispose();
|
|
144
|
+
bufferSubscription = undefined;
|
|
145
|
+
buffered.length = 0;
|
|
146
|
+
options.reportError?.(error, 'opening the transport to relay');
|
|
147
|
+
closeFramedSide();
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (disposed) {
|
|
152
|
+
// Disposed while connecting: the transport is ours now, so it is ours
|
|
153
|
+
// to release, or the socket outlives the relay that owns it.
|
|
154
|
+
opened.dispose?.();
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
transport = opened;
|
|
158
|
+
|
|
159
|
+
// Synchronous hand-off: drop the buffering listener, wire both
|
|
160
|
+
// directions, then replay. No await in between, so the event loop cannot
|
|
161
|
+
// interleave a message into the gap.
|
|
162
|
+
bufferSubscription?.dispose();
|
|
163
|
+
bufferSubscription = undefined;
|
|
164
|
+
|
|
165
|
+
subscriptions.push(
|
|
166
|
+
opened.reader.listen(message => channel.post(message)),
|
|
167
|
+
opened.reader.onClose(() => closeFramedSide()),
|
|
168
|
+
opened.reader.onError(error => {
|
|
169
|
+
options.reportError?.(error, 'reading from the relayed transport');
|
|
170
|
+
closeFramedSide();
|
|
171
|
+
}),
|
|
172
|
+
channel.onMessage(message => {
|
|
173
|
+
void opened.writer.write(message).catch((error: unknown) => {
|
|
174
|
+
options.reportError?.(error, 'writing to the relayed transport');
|
|
175
|
+
});
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const onChannelClose = channel.onClose?.(() => dispose());
|
|
180
|
+
if (onChannelClose) {
|
|
181
|
+
subscriptions.push(onChannelClose);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const message of buffered) {
|
|
185
|
+
void opened.writer.write(message).catch((error: unknown) => {
|
|
186
|
+
options.reportError?.(error, 'replaying a buffered message to the relayed transport');
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
buffered.length = 0;
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
})();
|
|
193
|
+
|
|
194
|
+
function dispose(): void {
|
|
195
|
+
if (disposed) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
disposed = true;
|
|
199
|
+
bufferSubscription?.dispose();
|
|
200
|
+
bufferSubscription = undefined;
|
|
201
|
+
buffered.length = 0;
|
|
202
|
+
for (const subscription of subscriptions) {
|
|
203
|
+
subscription.dispose();
|
|
204
|
+
}
|
|
205
|
+
subscriptions.length = 0;
|
|
206
|
+
transport?.dispose?.();
|
|
207
|
+
transport = undefined;
|
|
208
|
+
closeEmitter.dispose();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
wired,
|
|
213
|
+
onClose: closeEmitter.event,
|
|
214
|
+
dispose
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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 {
|
|
11
|
+
AbstractMessageReader,
|
|
12
|
+
AbstractMessageWriter,
|
|
13
|
+
Emitter,
|
|
14
|
+
type DataCallback,
|
|
15
|
+
type Disposable,
|
|
16
|
+
type Message,
|
|
17
|
+
type MessageReader,
|
|
18
|
+
type MessageWriter
|
|
19
|
+
} from 'vscode-jsonrpc';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A `postMessage`-shaped pipe: fire-and-forget, structured-clone-only, no
|
|
23
|
+
* framing and no bytes.
|
|
24
|
+
*
|
|
25
|
+
* This is the shape every extension↔webview hop reduces to, and the reason the
|
|
26
|
+
* data head needs a port at all. A webview has no `net`, no reach into the
|
|
27
|
+
* extension host's connection, and nothing but structured clone — so a
|
|
28
|
+
* transport built on it can carry plain data and nothing else.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately minimal, and deliberately not typed against any host's messaging
|
|
31
|
+
* library — a host adapter satisfies it in a few lines over whatever messenger
|
|
32
|
+
* it already has, so a diagram and a form can share one hop, one lifecycle and
|
|
33
|
+
* one dispose rather than opening a second channel for the form.
|
|
34
|
+
*/
|
|
35
|
+
export interface PostMessageChannel {
|
|
36
|
+
/**
|
|
37
|
+
* Hand one JSON-RPC message to the other side. Fire-and-forget: delivery
|
|
38
|
+
* failures surface through {@link onClose}, never as a rejection, because
|
|
39
|
+
* `postMessage` has no completion to report.
|
|
40
|
+
*/
|
|
41
|
+
post(message: Message): void;
|
|
42
|
+
|
|
43
|
+
/** Register for messages arriving from the other side. */
|
|
44
|
+
onMessage(listener: (message: Message) => void): Disposable;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register for the pipe going away — the webview being disposed, the
|
|
48
|
+
* extension deactivating. Optional: a channel with no observable end simply
|
|
49
|
+
* never fires close, and the reader/writer then rely on their owner's
|
|
50
|
+
* dispose instead.
|
|
51
|
+
*/
|
|
52
|
+
onClose?(listener: () => void): Disposable;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A reader/writer pair over a {@link PostMessageChannel}, ready to be handed to
|
|
57
|
+
* `createMessageConnection`.
|
|
58
|
+
*
|
|
59
|
+
* The connection is NOT built here, and that omission is the point:
|
|
60
|
+
* `createMessageConnection`'s message queue needs a vscode-jsonrpc runtime
|
|
61
|
+
* abstraction layer, and only the `/node` and `/browser` entrypoints install
|
|
62
|
+
* one. vscode-jsonrpc 9's package ROOT resolves to the RAL-less common API and
|
|
63
|
+
* throws `No runtime abstraction layer installed` on the first message. Only
|
|
64
|
+
* the host knows which side it is on, so the host imports the matching entry
|
|
65
|
+
* (`/browser` in a webview, `/node` in an extension host) and builds the
|
|
66
|
+
* connection there.
|
|
67
|
+
*
|
|
68
|
+
* Keeping the factory out of this module is also what lets the module stay
|
|
69
|
+
* browser-neutral: it touches only `AbstractMessageReader` /
|
|
70
|
+
* `AbstractMessageWriter` / `Emitter`, none of which need a RAL.
|
|
71
|
+
*/
|
|
72
|
+
export interface PostMessageTransport {
|
|
73
|
+
readonly reader: MessageReader;
|
|
74
|
+
readonly writer: MessageWriter;
|
|
75
|
+
/** Release the channel subscriptions. */
|
|
76
|
+
dispose(): void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A `MessageReader` over a {@link PostMessageChannel}.
|
|
81
|
+
*
|
|
82
|
+
* Structured clone moves objects, so there is nothing to frame and nothing to
|
|
83
|
+
* decode — the message arrives as the message.
|
|
84
|
+
*/
|
|
85
|
+
class PostMessageReader extends AbstractMessageReader implements MessageReader {
|
|
86
|
+
protected readonly messageEmitter = new Emitter<Message>();
|
|
87
|
+
protected readonly subscriptions: Disposable[] = [];
|
|
88
|
+
|
|
89
|
+
constructor(channel: PostMessageChannel) {
|
|
90
|
+
super();
|
|
91
|
+
this.subscriptions.push(channel.onMessage(message => this.messageEmitter.fire(message)));
|
|
92
|
+
const onClose = channel.onClose?.(() => this.fireClose());
|
|
93
|
+
if (onClose) {
|
|
94
|
+
this.subscriptions.push(onClose);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
listen(callback: DataCallback): Disposable {
|
|
99
|
+
return this.messageEmitter.event(callback);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override dispose(): void {
|
|
103
|
+
super.dispose();
|
|
104
|
+
for (const subscription of this.subscriptions) {
|
|
105
|
+
subscription.dispose();
|
|
106
|
+
}
|
|
107
|
+
this.subscriptions.length = 0;
|
|
108
|
+
this.messageEmitter.dispose();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The dual of {@link PostMessageReader} — one `post` per JSON-RPC message. */
|
|
113
|
+
class PostMessageWriter extends AbstractMessageWriter implements MessageWriter {
|
|
114
|
+
protected readonly subscriptions: Disposable[] = [];
|
|
115
|
+
|
|
116
|
+
constructor(protected readonly channel: PostMessageChannel) {
|
|
117
|
+
super();
|
|
118
|
+
const onClose = channel.onClose?.(() => this.fireClose());
|
|
119
|
+
if (onClose) {
|
|
120
|
+
this.subscriptions.push(onClose);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
write(message: Message): Promise<void> {
|
|
125
|
+
this.channel.post(message);
|
|
126
|
+
// `postMessage` reports no completion, so the resolved promise means
|
|
127
|
+
// "handed over", not "delivered". vscode-jsonrpc only needs the former.
|
|
128
|
+
return Promise.resolve();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
end(): void {
|
|
132
|
+
this.dispose();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
override dispose(): void {
|
|
136
|
+
super.dispose();
|
|
137
|
+
for (const subscription of this.subscriptions) {
|
|
138
|
+
subscription.dispose();
|
|
139
|
+
}
|
|
140
|
+
this.subscriptions.length = 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build the reader/writer pair for `channel`.
|
|
146
|
+
*
|
|
147
|
+
* What this buys, and why `DataPort` is shaped the way it is:
|
|
148
|
+
* `createRpcProxy` runs **unchanged** over the result. The typed surface, the
|
|
149
|
+
* `DATA_*_PROTOCOL_METHODS` allowlists and the whole open → watch → update →
|
|
150
|
+
* close sequence are identical in a webview and in a Node extension host,
|
|
151
|
+
* because only the `MessageConnection` the port hands back differs. Terminating
|
|
152
|
+
* JSON-RPC in the extension host and declaring one request type per data-head
|
|
153
|
+
* method is the wrong trade: it hand-maintains a per-method mapping and
|
|
154
|
+
* discards the `as const satisfies keyof` allowlists, which cannot drift.
|
|
155
|
+
*/
|
|
156
|
+
export function createPostMessageTransport(channel: PostMessageChannel): PostMessageTransport {
|
|
157
|
+
const reader = new PostMessageReader(channel);
|
|
158
|
+
const writer = new PostMessageWriter(channel);
|
|
159
|
+
return {
|
|
160
|
+
reader,
|
|
161
|
+
writer,
|
|
162
|
+
dispose(): void {
|
|
163
|
+
reader.dispose();
|
|
164
|
+
writer.dispose();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|