@hydranium/data-client-vscode 0.0.0-stage → 1.0.0-next
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 +110 -2
- package/lib/browser/index.d.ts +10 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/browser/index.js +26 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/browser/webview-data-port.d.ts +64 -0
- package/lib/browser/webview-data-port.d.ts.map +1 -0
- package/lib/browser/webview-data-port.js +97 -0
- package/lib/browser/webview-data-port.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +29 -0
- package/lib/index.js.map +1 -0
- package/lib/messenger-channel.d.ts +155 -0
- package/lib/messenger-channel.d.ts.map +1 -0
- package/lib/messenger-channel.js +106 -0
- package/lib/messenger-channel.js.map +1 -0
- package/lib/node/data-server-connection.d.ts +57 -0
- package/lib/node/data-server-connection.d.ts.map +1 -0
- package/lib/node/data-server-connection.js +92 -0
- package/lib/node/data-server-connection.js.map +1 -0
- package/lib/node/index.d.ts +10 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +26 -0
- package/lib/node/index.js.map +1 -0
- package/lib/testing/index.d.ts +10 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +26 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/messenger-harness.d.ts +48 -0
- package/lib/testing/messenger-harness.d.ts.map +1 -0
- package/lib/testing/messenger-harness.js +91 -0
- package/lib/testing/messenger-harness.js.map +1 -0
- package/package.json +99 -4
- package/src/browser/index.ts +10 -0
- package/src/browser/webview-data-port.ts +115 -0
- package/src/index.ts +13 -0
- package/src/messenger-channel.ts +224 -0
- package/src/node/data-server-connection.ts +85 -0
- package/src/node/index.ts +10 -0
- package/src/testing/index.ts +10 -0
- package/src/testing/messenger-harness.ts +121 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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
|
+
createPostMessageTransport,
|
|
12
|
+
renderFrameworkMessage,
|
|
13
|
+
type DataPort,
|
|
14
|
+
type Logger,
|
|
15
|
+
type PostMessageChannel,
|
|
16
|
+
type ResolvedMessage
|
|
17
|
+
} from '@hydranium/protocol';
|
|
18
|
+
import { Emitter, type Event, type MessageConnection } from 'vscode-jsonrpc';
|
|
19
|
+
import { createMessageConnection } from 'vscode-jsonrpc/browser';
|
|
20
|
+
|
|
21
|
+
/** Options for {@link WebviewDataPort}. */
|
|
22
|
+
export interface WebviewDataPortOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Surface a data-head failure. Without one it goes to the webview's console;
|
|
25
|
+
* a webview whose extension shows notifications forwards it there instead.
|
|
26
|
+
*/
|
|
27
|
+
readonly reportError?: (error: unknown, reported: ResolvedMessage) => void;
|
|
28
|
+
/** Where the connection logs its protocol faults, and what it does on its own. */
|
|
29
|
+
readonly logger?: Logger;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The webview's {@link DataPort}: the sandbox side of the data head, over the
|
|
34
|
+
* extension host's messenger hop.
|
|
35
|
+
*
|
|
36
|
+
* `vscode-jsonrpc/browser`, not the package root, which installs no runtime
|
|
37
|
+
* abstraction layer and throws on the first message rather than at wire-up.
|
|
38
|
+
*/
|
|
39
|
+
export class WebviewDataPort implements DataPort {
|
|
40
|
+
protected readonly disposeEmitter = new Emitter<void>();
|
|
41
|
+
readonly onDispose: Event<void> = this.disposeEmitter.event;
|
|
42
|
+
readonly logger?: Logger;
|
|
43
|
+
protected disposed = false;
|
|
44
|
+
/** The connection every generation gets until it is disposed. */
|
|
45
|
+
protected connection?: MessageConnection;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
protected readonly channel: PostMessageChannel,
|
|
49
|
+
protected readonly options: WebviewDataPortOptions = {}
|
|
50
|
+
) {
|
|
51
|
+
this.logger = options.logger;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hand back a listening connection over the hop: the same one until it is
|
|
56
|
+
* disposed, and then a fresh transport, so a disposed connection leaves no
|
|
57
|
+
* second reader on the pipe.
|
|
58
|
+
*
|
|
59
|
+
* Every connection reaches the one relay behind the pipe, and each numbers
|
|
60
|
+
* its requests from zero, so a second live one would take answers meant for
|
|
61
|
+
* the first. A retry after a failed readiness check therefore gets the
|
|
62
|
+
* connection the check used.
|
|
63
|
+
*/
|
|
64
|
+
async connect(): Promise<MessageConnection> {
|
|
65
|
+
if (this.disposed) {
|
|
66
|
+
throw new Error('WebviewDataPort: the port is disposed');
|
|
67
|
+
}
|
|
68
|
+
if (!this.connection) {
|
|
69
|
+
const transport = createPostMessageTransport(this.channel);
|
|
70
|
+
const connection = createMessageConnection(transport.reader, transport.writer, this.logger);
|
|
71
|
+
connection.onDispose(() => {
|
|
72
|
+
transport.dispose();
|
|
73
|
+
if (this.connection === connection) {
|
|
74
|
+
this.connection = undefined;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
connection.listen();
|
|
78
|
+
this.connection = connection;
|
|
79
|
+
}
|
|
80
|
+
return this.connection;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
reportError(error: unknown, reported: ResolvedMessage): void {
|
|
84
|
+
if (this.options.reportError) {
|
|
85
|
+
this.options.reportError(error, reported);
|
|
86
|
+
} else {
|
|
87
|
+
console.error(`[data head] ${renderFrameworkMessage(reported)}`, error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The extension host reports the relay's framed side died.
|
|
93
|
+
*
|
|
94
|
+
* A `PostMessageChannel` has no `close()`, so the pipe still looks open from
|
|
95
|
+
* here: firing `onDispose` makes `DataConnection` drop its generation, and
|
|
96
|
+
* disposing the connection rejects everything in flight, also when no
|
|
97
|
+
* generation holds it. The connection is forgotten before the event, so a
|
|
98
|
+
* listener that connects gets a fresh one rather than the one about to be
|
|
99
|
+
* disposed.
|
|
100
|
+
*/
|
|
101
|
+
connectionLost(): void {
|
|
102
|
+
const lost = this.connection;
|
|
103
|
+
this.connection = undefined;
|
|
104
|
+
this.disposeEmitter.fire();
|
|
105
|
+
lost?.dispose();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Refuses to connect afterwards: the consumer's reconnect would otherwise open a reader nothing releases. */
|
|
109
|
+
dispose(): void {
|
|
110
|
+
this.disposed = true;
|
|
111
|
+
this.disposeEmitter.fire();
|
|
112
|
+
this.disposeEmitter.dispose();
|
|
113
|
+
this.connection?.dispose();
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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
|
+
// Neutral: the messenger channel both ends of the hop build on, free of
|
|
11
|
+
// `vscode`, Node and the DOM. The extension host's end is `./node`, the
|
|
12
|
+
// webview's `./browser`.
|
|
13
|
+
export * from './messenger-channel';
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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 { PostMessageChannel } from '@hydranium/protocol';
|
|
11
|
+
import type { Disposable, Message } from 'vscode-jsonrpc';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A notification type as `vscode-messenger` defines it: a method name, with the
|
|
15
|
+
* payload type carried only in the generic parameter.
|
|
16
|
+
*
|
|
17
|
+
* Restated rather than imported: `vscode-messenger-common`'s
|
|
18
|
+
* `NotificationType<P>` is `{ method: string }` with `P` phantom, and the import
|
|
19
|
+
* would pin the adopter's messenger copy, which GLSP's VS Code integration
|
|
20
|
+
* chooses.
|
|
21
|
+
*/
|
|
22
|
+
export interface NotificationTypeLike {
|
|
23
|
+
readonly method: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The one method both ends of the hop share: hand a message to a participant.
|
|
28
|
+
*
|
|
29
|
+
* `TParticipant` stays generic because the two ends address different things,
|
|
30
|
+
* the extension side a registered webview and the webview side
|
|
31
|
+
* `HOST_EXTENSION`.
|
|
32
|
+
*/
|
|
33
|
+
export interface MessengerSendLike<TParticipant> {
|
|
34
|
+
sendNotification(type: NotificationTypeLike, receiver: TParticipant, params?: Message): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The two methods the channel needs from the **extension-side** messenger.
|
|
39
|
+
*
|
|
40
|
+
* Two methods rather than the class, so any messenger with them fits, GLSP's
|
|
41
|
+
* included, without pinning a `vscode-messenger` copy.
|
|
42
|
+
*/
|
|
43
|
+
export interface MessengerLike<TParticipant> extends MessengerSendLike<TParticipant> {
|
|
44
|
+
onNotification(
|
|
45
|
+
type: NotificationTypeLike,
|
|
46
|
+
handler: (params: Message, sender: TParticipant) => void,
|
|
47
|
+
options?: { sender?: TParticipant }
|
|
48
|
+
): Disposable;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The **webview-side** messenger, which differs from the extension side in
|
|
53
|
+
* every point the channel depends on:
|
|
54
|
+
*
|
|
55
|
+
* - **`onNotification` returns the messenger itself**, not a `Disposable`, and
|
|
56
|
+
* there is no unregister API, so a registration lasts as long as the webview.
|
|
57
|
+
* {@link createMessengerChannel} therefore stops delivering through a flag.
|
|
58
|
+
* - **There is no `sender` option**: a webview has one peer.
|
|
59
|
+
* - **`start()` must be called** before anything arrives, since it attaches the
|
|
60
|
+
* `window` listener. It is absent here because the channel does not call it;
|
|
61
|
+
* the webview's bootstrap does.
|
|
62
|
+
*
|
|
63
|
+
* A separate interface rather than a wider {@link MessengerLike}: one shape for
|
|
64
|
+
* both ends would promise a webview an unregisterable handler and a sender
|
|
65
|
+
* filter it cannot honour.
|
|
66
|
+
*/
|
|
67
|
+
export interface WebviewMessengerLike<TParticipant> extends MessengerSendLike<TParticipant> {
|
|
68
|
+
onNotification(type: NotificationTypeLike, handler: (params: Message, sender: TParticipant) => void): unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The data head's two notification types, one per direction.
|
|
73
|
+
*
|
|
74
|
+
* `vscode-messenger` addresses a notification to its receiver, so one method
|
|
75
|
+
* for both directions would also deliver correctly. Two keep the direction
|
|
76
|
+
* readable on the wire and in a messenger trace, and keep a delivery that
|
|
77
|
+
* routes by method alone, such as `BROADCAST`, from reaching the sender's own
|
|
78
|
+
* handler.
|
|
79
|
+
*/
|
|
80
|
+
export const DATA_TO_WEBVIEW: NotificationTypeLike = { method: 'hydranium/data/toWebview' };
|
|
81
|
+
export const DATA_TO_EXTENSION: NotificationTypeLike = { method: 'hydranium/data/toExtension' };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Options for {@link createMessengerChannel}.
|
|
85
|
+
*
|
|
86
|
+
* Generic over the registration type as well as the participant, because the two
|
|
87
|
+
* sides disagree on what `onNotification` hands back: a `Disposable` on the
|
|
88
|
+
* extension side, the messenger itself on the webview side. The type parameter
|
|
89
|
+
* keeps {@link releaseRegistration} typed; probing an `unknown` for a `dispose`
|
|
90
|
+
* method would dispose the whole webview messenger if it ever gained one.
|
|
91
|
+
*/
|
|
92
|
+
export interface MessengerChannelOptions<TParticipant, TRegistration> {
|
|
93
|
+
readonly messenger: {
|
|
94
|
+
sendNotification(type: NotificationTypeLike, receiver: TParticipant, params?: Message): void;
|
|
95
|
+
onNotification(
|
|
96
|
+
type: NotificationTypeLike,
|
|
97
|
+
handler: (params: Message, sender: TParticipant) => void,
|
|
98
|
+
options?: { sender?: TParticipant }
|
|
99
|
+
): TRegistration;
|
|
100
|
+
};
|
|
101
|
+
/** Where this side's outbound messages are addressed. */
|
|
102
|
+
readonly receiver: TParticipant;
|
|
103
|
+
/** The notification type this side SENDS on. */
|
|
104
|
+
readonly outbound: NotificationTypeLike;
|
|
105
|
+
/** The notification type this side LISTENS on. */
|
|
106
|
+
readonly inbound: NotificationTypeLike;
|
|
107
|
+
/**
|
|
108
|
+
* Accept inbound notifications only from this participant. Supply it
|
|
109
|
+
* whenever the messenger is shared, as GLSP's is with every diagram
|
|
110
|
+
* webview, so a second webview cannot inject traffic into this connection.
|
|
111
|
+
*
|
|
112
|
+
* Extension side only: the webview-side messenger ignores it.
|
|
113
|
+
*/
|
|
114
|
+
readonly sender?: TParticipant;
|
|
115
|
+
/**
|
|
116
|
+
* Observe the hop going away, the webview being disposed. Passed through to
|
|
117
|
+
* {@link PostMessageChannel.onClose}, whose reader and writer use it to fire
|
|
118
|
+
* their own close.
|
|
119
|
+
*/
|
|
120
|
+
readonly onClose?: (listener: () => void) => Disposable;
|
|
121
|
+
/**
|
|
122
|
+
* Release what `onNotification` returned, where the messenger hands back
|
|
123
|
+
* something releasable: on the extension side, whose registrations would
|
|
124
|
+
* otherwise accumulate on a hop that outlives every webview on it.
|
|
125
|
+
*/
|
|
126
|
+
readonly releaseRegistration?: (registration: TRegistration) => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Present a messenger as a {@link PostMessageChannel}, so the data head's
|
|
131
|
+
* transport rides a host's existing notification hop.
|
|
132
|
+
*
|
|
133
|
+
* `createPostMessageTransport` turns the channel into a reader and writer,
|
|
134
|
+
* `relayToPostMessageChannel` pumps the extension host's socket onto it from
|
|
135
|
+
* the other side, and everything above, `createRpcProxy` and `DataSession`
|
|
136
|
+
* included, is host-invariant. Sharing GLSP's `Messenger` rather than opening a
|
|
137
|
+
* second hop gives a diagram and a data webview one lifecycle and one dispose.
|
|
138
|
+
*
|
|
139
|
+
* **`onMessage`'s `Disposable` means "stop delivering", not necessarily
|
|
140
|
+
* "unregister".** The webview-side messenger cannot unregister, so a flag keeps
|
|
141
|
+
* the promise on both sides. Where unregistering is possible,
|
|
142
|
+
* {@link MessengerChannelOptions.releaseRegistration} does it too, or every
|
|
143
|
+
* disposed webview would leave a handler on a shared hop.
|
|
144
|
+
*/
|
|
145
|
+
export function createMessengerChannel<TParticipant, TRegistration>(
|
|
146
|
+
options: MessengerChannelOptions<TParticipant, TRegistration>
|
|
147
|
+
): PostMessageChannel {
|
|
148
|
+
const { messenger, receiver, outbound, inbound, sender, onClose, releaseRegistration } = options;
|
|
149
|
+
return {
|
|
150
|
+
post(message: Message): void {
|
|
151
|
+
messenger.sendNotification(outbound, receiver, message);
|
|
152
|
+
},
|
|
153
|
+
onMessage(listener: (message: Message) => void): Disposable {
|
|
154
|
+
let delivering = true;
|
|
155
|
+
const registration = messenger.onNotification(
|
|
156
|
+
inbound,
|
|
157
|
+
message => {
|
|
158
|
+
if (delivering) {
|
|
159
|
+
listener(message);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
sender ? { sender } : undefined
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
dispose: () => {
|
|
166
|
+
delivering = false;
|
|
167
|
+
releaseRegistration?.(registration);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
...(onClose ? { onClose } : {})
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The extension host's end: posts toward the webview, listens for what the
|
|
177
|
+
* webview sends.
|
|
178
|
+
*/
|
|
179
|
+
export function createExtensionSideChannel<TParticipant>(
|
|
180
|
+
messenger: MessengerLike<TParticipant>,
|
|
181
|
+
webview: TParticipant,
|
|
182
|
+
onClose?: (listener: () => void) => Disposable
|
|
183
|
+
): PostMessageChannel {
|
|
184
|
+
return createMessengerChannel<TParticipant, Disposable>({
|
|
185
|
+
messenger,
|
|
186
|
+
receiver: webview,
|
|
187
|
+
outbound: DATA_TO_WEBVIEW,
|
|
188
|
+
inbound: DATA_TO_EXTENSION,
|
|
189
|
+
// Only this webview's traffic, since the hop is shared.
|
|
190
|
+
sender: webview,
|
|
191
|
+
onClose,
|
|
192
|
+
// This side can unregister, and must: the hop outlives every webview on it.
|
|
193
|
+
releaseRegistration: registration => registration.dispose()
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The webview's end: the mirror image, addressed at the extension host.
|
|
199
|
+
*
|
|
200
|
+
* Two preconditions the webview's bootstrap owns, both of which present as "no
|
|
201
|
+
* data ever arrives" when missed:
|
|
202
|
+
*
|
|
203
|
+
* - **Read the channel through `WebviewDataPort`**, or build its connection
|
|
204
|
+
* with `createMessageConnection` from `vscode-jsonrpc/browser`: the package
|
|
205
|
+
* root installs no runtime abstraction layer and throws on the first
|
|
206
|
+
* message.
|
|
207
|
+
* - **Call the messenger's own `start()`**, which attaches the `window`
|
|
208
|
+
* listener.
|
|
209
|
+
*
|
|
210
|
+
* No `sender` filter: a webview has one peer.
|
|
211
|
+
*/
|
|
212
|
+
export function createWebviewSideChannel<TParticipant>(
|
|
213
|
+
messenger: WebviewMessengerLike<TParticipant>,
|
|
214
|
+
hostExtension: TParticipant,
|
|
215
|
+
onClose?: (listener: () => void) => Disposable
|
|
216
|
+
): PostMessageChannel {
|
|
217
|
+
return createMessengerChannel<TParticipant, unknown>({
|
|
218
|
+
messenger,
|
|
219
|
+
receiver: hostExtension,
|
|
220
|
+
outbound: DATA_TO_EXTENSION,
|
|
221
|
+
inbound: DATA_TO_WEBVIEW,
|
|
222
|
+
onClose
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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
|
+
* The extension host's end of the data head.
|
|
12
|
+
*
|
|
13
|
+
* A webview has no `net`, so the extension host holds the socket to the
|
|
14
|
+
* data-server and moves whole JSON-RPC messages between it and the webview over
|
|
15
|
+
* a `vscode-messenger` hop, typically GLSP's own, so a diagram and a data
|
|
16
|
+
* webview share one lifecycle and one dispose.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { relayToPostMessageChannel, type MessageRelay, type RelayTransport, type ResolvedMessage } from '@hydranium/protocol';
|
|
20
|
+
import * as net from 'node:net';
|
|
21
|
+
import type { Disposable } from 'vscode-jsonrpc';
|
|
22
|
+
// `/node` because the extension host is Node and holds the socket. The webview
|
|
23
|
+
// imports `/browser`; the package root installs no runtime abstraction layer.
|
|
24
|
+
import { SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc/node';
|
|
25
|
+
import { createExtensionSideChannel, type MessengerLike } from '../messenger-channel';
|
|
26
|
+
|
|
27
|
+
/** Connect to a head's socket and present it as a relay's framed side. */
|
|
28
|
+
export function openSocketTransport(port: number, host = '127.0.0.1'): Promise<RelayTransport> {
|
|
29
|
+
return new Promise<RelayTransport>((resolve, reject) => {
|
|
30
|
+
const socket = net.createConnection({ port, host });
|
|
31
|
+
socket.once('error', reject);
|
|
32
|
+
socket.once('connect', () => {
|
|
33
|
+
// Drop the construction-time handler so a later socket error goes to
|
|
34
|
+
// the relay's reader rather than rejecting an already-settled promise.
|
|
35
|
+
socket.removeAllListeners('error');
|
|
36
|
+
resolve({
|
|
37
|
+
reader: new SocketMessageReader(socket),
|
|
38
|
+
writer: new SocketMessageWriter(socket),
|
|
39
|
+
dispose: () => socket.destroy()
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What {@link connectDataServer} needs from the host. */
|
|
46
|
+
export interface DataServerConnectionOptions<TParticipant> {
|
|
47
|
+
/**
|
|
48
|
+
* The extension-side messenger, typically `GlspVscodeConnector.messenger`.
|
|
49
|
+
* It must be constructed with `ignoreHiddenViews: false`, or a hidden
|
|
50
|
+
* webview misses every message the data-server sends it.
|
|
51
|
+
*/
|
|
52
|
+
readonly messenger: MessengerLike<TParticipant>;
|
|
53
|
+
/** The registered webview this connection serves. */
|
|
54
|
+
readonly webview: TParticipant;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the data-server's listening port. Called once per connection
|
|
57
|
+
* generation, so a restarted server is re-discovered rather than cached —
|
|
58
|
+
* the language server binds a fresh ephemeral port each start.
|
|
59
|
+
*/
|
|
60
|
+
readonly findPort: () => Promise<number>;
|
|
61
|
+
/** Observe the webview going away, so the relay releases with it. */
|
|
62
|
+
readonly onWebviewDisposed?: (listener: () => void) => Disposable;
|
|
63
|
+
/**
|
|
64
|
+
* Surface a relay failure. `reported` is a complete sentence plus the
|
|
65
|
+
* identity needed to render it in another language — the extension host holds
|
|
66
|
+
* the locale, so it renders and the relay does not.
|
|
67
|
+
*/
|
|
68
|
+
readonly reportError?: (error: unknown, reported: ResolvedMessage) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Wire one webview to the data head.
|
|
73
|
+
*
|
|
74
|
+
* The returned relay's `onClose` is the host's ONLY notice that the framed side
|
|
75
|
+
* died — a `PostMessageChannel` has no `close()`, so a dead data-server is
|
|
76
|
+
* invisible to the webview, whose pending requests would otherwise hang with no
|
|
77
|
+
* rejection. Tell the webview, whose `WebviewDataPort.connectionLost` rejects
|
|
78
|
+
* them, or dispose the webview; doing nothing is the one wrong choice.
|
|
79
|
+
*/
|
|
80
|
+
export function connectDataServer<TParticipant>(options: DataServerConnectionOptions<TParticipant>): MessageRelay {
|
|
81
|
+
const channel = createExtensionSideChannel(options.messenger, options.webview, options.onWebviewDisposed);
|
|
82
|
+
return relayToPostMessageChannel(channel, async () => openSocketTransport(await options.findPort()), {
|
|
83
|
+
reportError: options.reportError
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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
|
+
export * from './data-server-connection';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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
|
+
export * from './messenger-harness';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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 { Messenger } from 'vscode-messenger';
|
|
11
|
+
import type { WebviewIdMessageParticipant } from 'vscode-messenger-common';
|
|
12
|
+
import { Messenger as WebviewMessenger, type VsCodeApi } from 'vscode-messenger-webview';
|
|
13
|
+
|
|
14
|
+
/** One webview on a {@link MessengerHarness}. */
|
|
15
|
+
export interface MessengerHarnessWebview {
|
|
16
|
+
/** How the extension-side messenger addresses this webview. */
|
|
17
|
+
readonly participant: WebviewIdMessageParticipant;
|
|
18
|
+
/** The webview's own messenger, already started. */
|
|
19
|
+
readonly messenger: WebviewMessenger;
|
|
20
|
+
/** Close the webview, as VS Code disposing its panel does: the extension-side messenger forgets it. */
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The real `vscode-messenger` classes, joined without VS Code. */
|
|
25
|
+
export interface MessengerHarness {
|
|
26
|
+
/** The extension host's messenger, constructed as `GlspVscodeConnector` constructs its own. */
|
|
27
|
+
readonly extension: Messenger;
|
|
28
|
+
/** Open a webview: a panel registered with {@link extension}, and the webview's messenger behind it. */
|
|
29
|
+
openWebview(): MessengerHarnessWebview;
|
|
30
|
+
/**
|
|
31
|
+
* Restore the global `window` this harness replaced. A webview messenger
|
|
32
|
+
* reads `window.crypto` for every request it sends, so its requests throw
|
|
33
|
+
* once the harness is disposed; notifications still go.
|
|
34
|
+
*/
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What the webview messenger reads from the global `window`. */
|
|
39
|
+
interface HarnessWindow {
|
|
40
|
+
addEventListener(type: string, listener: (event: { data: unknown }) => void): void;
|
|
41
|
+
readonly crypto: typeof globalThis.crypto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Join the real extension-side and webview-side messengers in a Node test,
|
|
46
|
+
* through a fake webview panel on one end and a fake `window` on the other.
|
|
47
|
+
*
|
|
48
|
+
* Both classes run unchanged, so their routing is the real one: delivery is
|
|
49
|
+
* asynchronous, the webview side keeps one handler per method, and the
|
|
50
|
+
* extension side filters by sender. The webview messenger listens on the
|
|
51
|
+
* global `window`, so the harness installs one for its lifetime and routes each
|
|
52
|
+
* listener to the webview that registered it.
|
|
53
|
+
*
|
|
54
|
+
* One harness at a time: a second replaces the `window` the first routes its
|
|
55
|
+
* webviews through, and disposing them out of order restores the wrong one.
|
|
56
|
+
*/
|
|
57
|
+
export function makeMessengerHarness(): MessengerHarness {
|
|
58
|
+
const host = globalThis as { window?: unknown };
|
|
59
|
+
const previousWindow = host.window;
|
|
60
|
+
let starting: Array<(event: { data: unknown }) => void> | undefined;
|
|
61
|
+
const harnessWindow: HarnessWindow = {
|
|
62
|
+
addEventListener: (type, listener) => {
|
|
63
|
+
if (type === 'message') {
|
|
64
|
+
starting?.push(listener);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
crypto: globalThis.crypto
|
|
68
|
+
};
|
|
69
|
+
host.window = harnessWindow;
|
|
70
|
+
const extension = new Messenger({ ignoreHiddenViews: false });
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
extension,
|
|
74
|
+
openWebview: () => {
|
|
75
|
+
const toWebview: Array<(event: { data: unknown }) => void> = [];
|
|
76
|
+
const toExtension: Array<(message: unknown) => void> = [];
|
|
77
|
+
const disposeListeners: Array<() => void> = [];
|
|
78
|
+
const panel = {
|
|
79
|
+
viewType: 'hydranium.messengerHarness',
|
|
80
|
+
visible: true,
|
|
81
|
+
webview: {
|
|
82
|
+
postMessage: async (message: unknown) => {
|
|
83
|
+
[...toWebview].forEach(listener => listener({ data: message }));
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
onDidReceiveMessage: (listener: (message: unknown) => void) => {
|
|
87
|
+
toExtension.push(listener);
|
|
88
|
+
return {
|
|
89
|
+
dispose: () => {
|
|
90
|
+
const index = toExtension.indexOf(listener);
|
|
91
|
+
if (index >= 0) {
|
|
92
|
+
toExtension.splice(index, 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
onDidDispose: (listener: () => void) => {
|
|
99
|
+
disposeListeners.push(listener);
|
|
100
|
+
return { dispose: () => undefined };
|
|
101
|
+
},
|
|
102
|
+
onDidChangeViewState: () => ({ dispose: () => undefined })
|
|
103
|
+
};
|
|
104
|
+
// The members the extension-side messenger reads; the rest of a panel is VS Code's.
|
|
105
|
+
const participant = extension.registerWebviewPanel(panel as unknown as Parameters<Messenger['registerWebviewPanel']>[0]);
|
|
106
|
+
const vscodeApi: VsCodeApi = {
|
|
107
|
+
postMessage: message => [...toExtension].forEach(listener => listener(message)),
|
|
108
|
+
getState: () => undefined,
|
|
109
|
+
setState: () => undefined
|
|
110
|
+
};
|
|
111
|
+
const messenger = new WebviewMessenger(vscodeApi);
|
|
112
|
+
starting = toWebview;
|
|
113
|
+
messenger.start();
|
|
114
|
+
starting = undefined;
|
|
115
|
+
return { participant, messenger, dispose: () => disposeListeners.forEach(listener => listener()) };
|
|
116
|
+
},
|
|
117
|
+
dispose: () => {
|
|
118
|
+
host.window = previousWindow;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|