@hydranium/client-browser 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CrossBreeze, EclipseSource and others.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,102 @@
1
- # Temporary Holding Version
1
+ # `@hydranium/client-browser`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The page side of a [Hydranium](https://github.com/eclipse-emfcloud/hydranium)
4
+ host that runs its heads in a web worker, with no backend. Install it when
5
+ your language runs in a plain web page: it hands each head its own
6
+ `MessagePort`, runs the page's LSP handshake, and gives the data head its port.
7
+
8
+ ## What it gives you
9
+
10
+ - One `MessagePort` per head, transferred to the worker in the framework's
11
+ bootstrap message, so no head is ever bound to the worker's global scope.
12
+ - The LSP head's connection with the server's own requests answered: the
13
+ capability registrations and the configuration section it reads while the
14
+ workspace comes up.
15
+ - A `DataPort` over the data head's port, for the connection and sessions from
16
+ `@hydranium/protocol`.
17
+ - The GLSP head's connection, for GLSP's own client.
18
+ - Pending requests rejected when the worker fails to start or fails its
19
+ bootstrap, instead of waiting forever, since a port cannot report a dead
20
+ peer.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @hydranium/client-browser
26
+ ```
27
+
28
+ | Peer | Range |
29
+ | -------------------------------- | ------------- |
30
+ | `@hydranium/protocol` | `^1.0.0-next` |
31
+ | `vscode-jsonrpc` | `^9.0.0` |
32
+ | `vscode-languageserver-protocol` | `~3.18.1` |
33
+
34
+ ## Wiring
35
+
36
+ Build the `Worker` yourself, from a URL your build gives you, and hand it to
37
+ `connectWorkerHeads`. Run the LSP handshake on the LSP head's connection before
38
+ anything else uses it, and give the data head's port to a data connection:
39
+
40
+ <!-- snippet-preamble
41
+ import { connectWorkerHeads } from '@hydranium/client-browser';
42
+ // The page's `Worker`, which has no name under this gate's DOM-free lib.
43
+ declare const worker: Parameters<typeof connectWorkerHeads>[0];
44
+ -->
45
+
46
+ ```ts
47
+ import { initializeLanguageClient } from '@hydranium/client-browser';
48
+ import { DataConnectionWithEvents } from '@hydranium/protocol';
49
+
50
+ const heads = connectWorkerHeads(worker);
51
+ heads.onWorkerError(report => console.error(`worker, ${report.phase}: ${report.message}`));
52
+ const { capabilities } = await initializeLanguageClient(heads.lsp, {
53
+ rootUri: 'file:///workspace',
54
+ configuration: { section: 'my-language', settings: () => ({ log: { level: 'info' } }) }
55
+ });
56
+ const data = new DataConnectionWithEvents(heads.data);
57
+ ```
58
+
59
+ In the worker, read the bootstrap with `isWorkerBootstrapMessage` from
60
+ `@hydranium/protocol` and start each head on its port, never on the worker's
61
+ global scope. Post a `WorkerErrorMessage` for a failure you catch there; one
62
+ under `WORKER_BOOTSTRAP_PHASE` ends the page's heads, any other phase only
63
+ reports.
64
+
65
+ Register the handler for the server's diagnostics in `beforeInitialized`,
66
+ which runs once `initialize` has answered and before `initialized` goes out.
67
+ A second handler for the same notification replaces the first, so fan out
68
+ from one.
69
+
70
+ The data port reports a data-head failure to the console. A host with a
71
+ surface of its own subclasses `WorkerDataPort` and passes it as
72
+ `createDataPort`.
73
+
74
+ The default capabilities declare `workspace.applyEdit`, which the handshake
75
+ does not answer: register a `workspace/applyEdit` handler on the connection
76
+ before you open a document, as any LSP client does, or the server's writes
77
+ to that document are lost.
78
+
79
+ For the whole setup, see *Host in a browser* in
80
+ [Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
81
+
82
+ ## Entry points
83
+
84
+ | Subpath | Use it for | Runs in |
85
+ | ------- | --------------------------------------------------------- | ---------- |
86
+ | `.` | The worker bootstrap, the LSP handshake and the data port | a web page |
87
+
88
+ The package needs a TypeScript `moduleResolution` that reads `exports`
89
+ (`NodeNext` or `Bundler`); see *Requirements* in
90
+ [Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
91
+
92
+ ## Status
93
+
94
+ Alpha: every release is a prerelease that may break the API, so pin an exact
95
+ version. Guides and known limitations:
96
+ [Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
97
+
98
+ ## License
99
+
100
+ `MIT` — see this package's [`LICENSE`](./LICENSE), and the repository
101
+ [`NOTICE.md`](https://github.com/eclipse-emfcloud/hydranium/blob/main/NOTICE.md)
102
+ for third-party notices.
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
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
+ export * from './worker-heads';
10
+ export * from './language-client';
11
+ export * from './worker-data-port';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ __exportStar(require("./worker-heads"), exports);
26
+ __exportStar(require("./language-client"), exports);
27
+ __exportStar(require("./worker-data-port"), exports);
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,iDAA+B;AAC/B,oDAAkC;AAClC,qDAAmC"}
@@ -0,0 +1,64 @@
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
+ import type { Locale } from '@hydranium/protocol';
10
+ import type { MessageConnection } from 'vscode-jsonrpc/browser';
11
+ import { type ClientCapabilities, type InitializeResult, type LogMessageParams } from 'vscode-languageserver-protocol';
12
+ /** The one configuration section the server reads, and what it reads there. */
13
+ export interface LanguageClientConfiguration {
14
+ /** The server's `lsp.configurationRoot`. A mismatch leaves every answer `null`, so the setting reads as unset. */
15
+ readonly section: string;
16
+ /** The section's whole subtree. The server replaces its cached section with what it is given, so a partial answer deletes the siblings it omits. */
17
+ settings(): unknown;
18
+ }
19
+ export interface LanguageClientOptions {
20
+ /** The workspace root, a URI, which is also the one workspace folder. */
21
+ readonly rootUri: string;
22
+ /** The folder's name, the root's last path segment by default. */
23
+ readonly workspaceName?: string;
24
+ /** The reading user's language, which only the page knows. Absent means the framework's English. */
25
+ readonly locale?: Locale;
26
+ /**
27
+ * The client's capabilities, `workspace.applyEdit` and
28
+ * `workspace.configuration` by default. `configuration` gates its request:
29
+ * absent, the server asks for no section ever. `applyEdit` does not: the
30
+ * server sends `workspace/applyEdit` to any client with the document open,
31
+ * and a client with no handler answers it as an unknown method, which loses
32
+ * the whole inbound direction. Declaring less than the handlers the host
33
+ * registers is the one safe direction; declaring more produces answers it
34
+ * cannot render.
35
+ */
36
+ readonly capabilities?: ClientCapabilities;
37
+ /** Answered from the start, since the server reads it while the workspace comes up. */
38
+ readonly configuration?: LanguageClientConfiguration;
39
+ /** The server's log, every head of it; a page with no handler loses it. Registered before `listen`, so the first build's lines arrive too. */
40
+ readonly onLogMessage?: (params: LogMessageParams) => void;
41
+ /**
42
+ * Called with the server's answer before `initialized` is sent: the window
43
+ * in which a handler for the server's first notifications is registered
44
+ * once the answer it needs is known. The server publishes nothing before
45
+ * `initialized`, so nothing is missed.
46
+ */
47
+ readonly beforeInitialized?: (result: InitializeResult) => void;
48
+ }
49
+ /**
50
+ * Run the page's end of the LSP handshake on `connection`, which must not be
51
+ * listening yet, and resolve with the server's answer.
52
+ *
53
+ * Registers the answers to the server's own requests before `listen`, because
54
+ * each is asked while the workspace comes up and an unanswered one is a
55
+ * failure the page sees as an error and the server as a client with no
56
+ * settings: capability registrations are acknowledged and kept nowhere, and a
57
+ * configuration request is answered item by item, since the response is
58
+ * positional and a dropped item mis-aligns every later answer. These
59
+ * registrations replace any the host made before the call, since a
60
+ * connection keeps one handler per method; a host serving more than one
61
+ * section registers its own configuration handler in `beforeInitialized`.
62
+ */
63
+ export declare function initializeLanguageClient(connection: MessageConnection, options: LanguageClientOptions): Promise<InitializeResult>;
64
+ //# sourceMappingURL=language-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-client.d.ts","sourceRoot":"","sources":["../src/language-client.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAOJ,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACvB,MAAM,gCAAgC,CAAC;AAExC,+EAA+E;AAC/E,MAAM,WAAW,2BAA2B;IACzC,kHAAkH;IAClH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oJAAoJ;IACpJ,QAAQ,IAAI,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACnC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oGAAoG;IACpG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;OASG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAC3C,uFAAuF;IACvF,QAAQ,CAAC,aAAa,CAAC,EAAE,2BAA2B,CAAC;IACrD,8IAA8I;IAC9I,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC3D;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAClE;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,wBAAwB,CAAC,UAAU,EAAE,iBAAiB,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAyBvI"}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.initializeLanguageClient = initializeLanguageClient;
12
+ const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
13
+ /** The capabilities the server's inbound requests rely on, declared unless the host declares its own. */
14
+ const DEFAULT_CAPABILITIES = { workspace: { applyEdit: true, configuration: true } };
15
+ /**
16
+ * Run the page's end of the LSP handshake on `connection`, which must not be
17
+ * listening yet, and resolve with the server's answer.
18
+ *
19
+ * Registers the answers to the server's own requests before `listen`, because
20
+ * each is asked while the workspace comes up and an unanswered one is a
21
+ * failure the page sees as an error and the server as a client with no
22
+ * settings: capability registrations are acknowledged and kept nowhere, and a
23
+ * configuration request is answered item by item, since the response is
24
+ * positional and a dropped item mis-aligns every later answer. These
25
+ * registrations replace any the host made before the call, since a
26
+ * connection keeps one handler per method; a host serving more than one
27
+ * section registers its own configuration handler in `beforeInitialized`.
28
+ */
29
+ async function initializeLanguageClient(connection, options) {
30
+ // Before `listen`: a root that is no URI throws here, and a connection left
31
+ // listening with no `initialize` sent would be one the host cannot reuse.
32
+ const workspaceName = options.workspaceName ?? workspaceNameOf(options.rootUri);
33
+ connection.onRequest(vscode_languageserver_protocol_1.RegistrationRequest.type, () => undefined);
34
+ connection.onRequest(vscode_languageserver_protocol_1.UnregistrationRequest.type, () => undefined);
35
+ const configuration = options.configuration;
36
+ connection.onRequest(vscode_languageserver_protocol_1.ConfigurationRequest.type, params => params.items.map(item => (configuration !== undefined && item.section === configuration.section ? configuration.settings() : null)));
37
+ if (options.onLogMessage) {
38
+ connection.onNotification(vscode_languageserver_protocol_1.LogMessageNotification.type, options.onLogMessage);
39
+ }
40
+ connection.listen();
41
+ const result = await connection.sendRequest(vscode_languageserver_protocol_1.InitializeRequest.type, {
42
+ processId: null,
43
+ rootUri: options.rootUri,
44
+ locale: options.locale,
45
+ capabilities: options.capabilities ?? DEFAULT_CAPABILITIES,
46
+ workspaceFolders: [{ uri: options.rootUri, name: workspaceName }]
47
+ });
48
+ options.beforeInitialized?.(result);
49
+ await connection.sendNotification(vscode_languageserver_protocol_1.InitializedNotification.type, {});
50
+ return result;
51
+ }
52
+ /** The root's last path segment, decoded, or the root itself for one with no path. */
53
+ function workspaceNameOf(rootUri) {
54
+ return decodeURIComponent(new URL(rootUri).pathname.replace(/\/+$/, '').split('/').pop() ?? '') || rootUri;
55
+ }
56
+ //# sourceMappingURL=language-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-client.js","sourceRoot":"","sources":["../src/language-client.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAIlF,mFAUwC;AAyCxC,yGAAyG;AACzG,MAAM,oBAAoB,GAAuB,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,CAAC;AAEzG;;;;;;;;;;;;;GAaG;AACI,KAAK,mCAAmC,UAA6B,EAAE,OAA8B;IACzG,4EAA4E;IAC5E,0EAA0E;IAC1E,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAChF,UAAU,CAAC,SAAS,CAAC,oDAAmB,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAChE,UAAU,CAAC,SAAS,CAAC,sDAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAClE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAC5C,UAAU,CAAC,SAAS,CAAC,qDAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CACtD,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,aAAa,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CACrI,CAAC;IACF,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACxB,UAAU,CAAC,cAAc,CAAC,uDAAsB,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,CAAC;IACD,UAAU,CAAC,MAAM,EAAE,CAAC;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,WAAW,CAAC,kDAAiB,CAAC,IAAI,EAAE;QACjE,SAAS,EAAE,IAAI;QACf,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,oBAAoB;QAC1D,gBAAgB,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;KACnE,CAAC,CAAC;IACH,OAAO,CAAC,iBAAiB,EAAE,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,UAAU,CAAC,gBAAgB,CAAC,wDAAuB,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACpE,OAAO,MAAM,CAAC;AACjB,CAAC;AAED,sFAAsF;AACtF,SAAS,eAAe,CAAC,OAAe;IACrC,OAAO,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,OAAO,CAAC;AAC9G,CAAC"}
@@ -0,0 +1,71 @@
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
+ import { type DataPort, type Logger, type ResolvedMessage, type TransferredMessagePort } from '@hydranium/protocol';
10
+ import { Emitter, type MessageConnection } from 'vscode-jsonrpc/browser';
11
+ /**
12
+ * The host half of the data head for a page talking to a worker.
13
+ *
14
+ * This is the whole of what a browser host has to supply to reach the data
15
+ * head; everything above it (the open/watch order, `baseVersion` conflict
16
+ * handling, echo filtering by `sourceClientId`) is host-invariant and already
17
+ * in `DataConnection` and `DataSession`.
18
+ *
19
+ * `vscode-jsonrpc/browser`, never the package root: version 9's root entry ships
20
+ * no runtime abstraction layer and throws on the first message rather than at
21
+ * import, so the mistake surfaces as a dead connection rather than a stack
22
+ * trace.
23
+ */
24
+ export declare class WorkerDataPort implements DataPort {
25
+ /** The page's end of the data head's port, typed as the handshake types it. */
26
+ protected readonly port: TransferredMessagePort;
27
+ /** Where the connection logs its protocol faults, and what it does on its own. */
28
+ readonly logger?: Logger | undefined;
29
+ protected readonly disposeEmitter: Emitter<void>;
30
+ readonly onDispose: import("vscode-jsonrpc/browser").Event<void>;
31
+ protected disposed: boolean;
32
+ protected connection?: MessageConnection;
33
+ constructor(
34
+ /** The page's end of the data head's port, typed as the handshake types it. */
35
+ port: TransferredMessagePort,
36
+ /** Where the connection logs its protocol faults, and what it does on its own. */
37
+ logger?: Logger | undefined);
38
+ /**
39
+ * The transport is the port the worker was already handed at bootstrap, so
40
+ * there is nothing to open, but the connection must be `listen()`ing before
41
+ * it is returned, because the RPC proxy queues calls on this promise and
42
+ * never listens itself.
43
+ *
44
+ * Both ends use `createMessagePortTransport`; see it for why.
45
+ *
46
+ * Every generation gets the same connection. A fresh one per generation
47
+ * would leave the one a failed readiness check used still reading the port,
48
+ * and disposing that one is no way out: disposing any connection over the
49
+ * port ends the worker's head, and the retry would wait for an answer that
50
+ * never comes.
51
+ *
52
+ * **Rejects once this port is disposed, and a disposed port cannot be
53
+ * reused.** Its dispose disposes the connection, which signals the close to
54
+ * the worker, whose end of the port then stays closed: a later connection
55
+ * over it would send requests nothing answers, and hang.
56
+ */
57
+ connect(): Promise<MessageConnection>;
58
+ /**
59
+ * To the console: a page has no notification surface of its own, and an
60
+ * unreported data-head error presents as an empty model, which reads as a
61
+ * valid document. A host with a surface overrides this.
62
+ *
63
+ * No translation map: omitting the argument takes the English. The raw
64
+ * error goes alongside rather than into the sentence, because `reported`
65
+ * already carries the detail and only the throw carries a stack.
66
+ */
67
+ reportError(error: unknown, reported: ResolvedMessage): void;
68
+ /** The consumer hears it first; the connection goes even when no generation holds it, as after a failed readiness check. */
69
+ dispose(): void;
70
+ }
71
+ //# sourceMappingURL=worker-data-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-data-port.d.ts","sourceRoot":"","sources":["../src/worker-data-port.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAGJ,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC7B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAA2B,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAElG;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAe,YAAW,QAAQ;IAOzC,+EAA+E;IAC/E,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,sBAAsB;IAC/C,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAT3B,SAAS,CAAC,QAAQ,CAAC,cAAc,gBAAuB;IACxD,QAAQ,CAAC,SAAS,+CAA6B;IAC/C,SAAS,CAAC,QAAQ,UAAS;IAC3B,SAAS,CAAC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAEzC;IACG,+EAA+E;IAC5D,IAAI,EAAE,sBAAsB;IAC/C,kFAAkF;IACzE,MAAM,CAAC,EAAE,MAAM,YAAA,EACvB;IAEJ;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAUpC;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CAE3D;IAED,4HAA4H;IAC5H,OAAO,IAAI,IAAI,CAKd;CACH"}
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.WorkerDataPort = void 0;
12
+ const protocol_1 = require("@hydranium/protocol");
13
+ const browser_1 = require("vscode-jsonrpc/browser");
14
+ /**
15
+ * The host half of the data head for a page talking to a worker.
16
+ *
17
+ * This is the whole of what a browser host has to supply to reach the data
18
+ * head; everything above it (the open/watch order, `baseVersion` conflict
19
+ * handling, echo filtering by `sourceClientId`) is host-invariant and already
20
+ * in `DataConnection` and `DataSession`.
21
+ *
22
+ * `vscode-jsonrpc/browser`, never the package root: version 9's root entry ships
23
+ * no runtime abstraction layer and throws on the first message rather than at
24
+ * import, so the mistake surfaces as a dead connection rather than a stack
25
+ * trace.
26
+ */
27
+ class WorkerDataPort {
28
+ port;
29
+ logger;
30
+ disposeEmitter = new browser_1.Emitter();
31
+ onDispose = this.disposeEmitter.event;
32
+ disposed = false;
33
+ connection;
34
+ constructor(
35
+ /** The page's end of the data head's port, typed as the handshake types it. */
36
+ port,
37
+ /** Where the connection logs its protocol faults, and what it does on its own. */
38
+ logger) {
39
+ this.port = port;
40
+ this.logger = logger;
41
+ }
42
+ /**
43
+ * The transport is the port the worker was already handed at bootstrap, so
44
+ * there is nothing to open, but the connection must be `listen()`ing before
45
+ * it is returned, because the RPC proxy queues calls on this promise and
46
+ * never listens itself.
47
+ *
48
+ * Both ends use `createMessagePortTransport`; see it for why.
49
+ *
50
+ * Every generation gets the same connection. A fresh one per generation
51
+ * would leave the one a failed readiness check used still reading the port,
52
+ * and disposing that one is no way out: disposing any connection over the
53
+ * port ends the worker's head, and the retry would wait for an answer that
54
+ * never comes.
55
+ *
56
+ * **Rejects once this port is disposed, and a disposed port cannot be
57
+ * reused.** Its dispose disposes the connection, which signals the close to
58
+ * the worker, whose end of the port then stays closed: a later connection
59
+ * over it would send requests nothing answers, and hang.
60
+ */
61
+ connect() {
62
+ if (this.disposed) {
63
+ return Promise.reject(new Error('WorkerDataPort: the port is disposed, and the data head behind it has ended'));
64
+ }
65
+ if (!this.connection) {
66
+ const transport = (0, protocol_1.createMessagePortTransport)(this.port);
67
+ this.connection = (0, browser_1.createMessageConnection)(transport.reader, transport.writer, this.logger);
68
+ this.connection.listen();
69
+ }
70
+ return Promise.resolve(this.connection);
71
+ }
72
+ /**
73
+ * To the console: a page has no notification surface of its own, and an
74
+ * unreported data-head error presents as an empty model, which reads as a
75
+ * valid document. A host with a surface overrides this.
76
+ *
77
+ * No translation map: omitting the argument takes the English. The raw
78
+ * error goes alongside rather than into the sentence, because `reported`
79
+ * already carries the detail and only the throw carries a stack.
80
+ */
81
+ reportError(error, reported) {
82
+ console.error(`[data head] ${(0, protocol_1.renderFrameworkMessage)(reported)}`, error);
83
+ }
84
+ /** The consumer hears it first; the connection goes even when no generation holds it, as after a failed readiness check. */
85
+ dispose() {
86
+ this.disposed = true;
87
+ this.disposeEmitter.fire();
88
+ this.disposeEmitter.dispose();
89
+ this.connection?.dispose();
90
+ }
91
+ }
92
+ exports.WorkerDataPort = WorkerDataPort;
93
+ //# sourceMappingURL=worker-data-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-data-port.js","sourceRoot":"","sources":["../src/worker-data-port.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAElF,kDAO6B;AAC7B,oDAAkG;AAElG;;;;;;;;;;;;GAYG;AACH;IAQyB,IAAI;IAEd,MAAM;IATC,cAAc,GAAG,IAAI,iBAAO,EAAQ,CAAC;IAC/C,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;IACrC,QAAQ,GAAG,KAAK,CAAC;IACjB,UAAU,CAAqB;IAEzC;IACG,+EAA+E;IAC5D,IAA4B;IAC/C,kFAAkF;IACzE,MAAe;oBAFL,IAAI;sBAEd,MAAM;IACf,CAAC;IAEJ;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO;QACJ,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjB,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC,CAAC;QACnH,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,MAAM,SAAS,GAAG,IAAA,qCAA0B,EAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxD,IAAI,CAAC,UAAU,GAAG,IAAA,iCAAuB,EAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC3F,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;QAC5B,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC3C,CAAC;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,KAAc,EAAE,QAAyB;QAClD,OAAO,CAAC,KAAK,CAAC,eAAe,IAAA,iCAAsB,EAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAC3E,CAAC;IAED,4HAA4H;IAC5H,OAAO;QACJ,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;QAC9B,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;IAC9B,CAAC;CACH"}
@@ -0,0 +1,79 @@
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
+ import { type DataPort, type Logger, type TransferredMessagePort, type WorkerErrorMessage } from '@hydranium/protocol';
10
+ import { type Disposable, type Event, type MessageConnection } from 'vscode-jsonrpc/browser';
11
+ /** The phase a report synthesised from the `Worker`'s `error` event carries. */
12
+ export declare const WORKER_ERROR_EVENT_PHASE = "worker";
13
+ export interface WorkerHeadsOptions {
14
+ /** Where each head's connection logs its protocol faults, one component per head. Without one they log nothing. */
15
+ readonly logger?: Logger;
16
+ /**
17
+ * Build the data head's port over the page's end of its `MessagePort`, a
18
+ * {@link WorkerDataPort} by default. A host with a surface of its own for
19
+ * data-head failures supplies a subclass here.
20
+ */
21
+ readonly createDataPort?: (port: TransferredMessagePort, logger?: Logger) => DataPort & Disposable;
22
+ }
23
+ /** The page's end of each head, and what the worker says outside any head. */
24
+ export interface WorkerHeads {
25
+ /**
26
+ * The LSP head's connection, not yet listening: register the handlers for
27
+ * the server's first requests, then `listen()` and send `initialize`. A
28
+ * message that arrives before `listen` is dropped.
29
+ */
30
+ readonly lsp: MessageConnection;
31
+ /** The data head's port, for a `DataConnection`. */
32
+ readonly data: DataPort;
33
+ /**
34
+ * The GLSP head's connection, not listening: GLSP's client calls `listen()`
35
+ * when it starts, and a second call throws. Its port is started, so a
36
+ * message arriving before then is dropped; the GLSP head posts nothing on
37
+ * its port before the client speaks. Hand it to that client wrapped with
38
+ * `sendByMethodName`, where the client's own connection type is named:
39
+ * GLSP's typed messages come from its own copy of `vscode-jsonrpc`, and
40
+ * this connection's copy rejects them unless they go by method name.
41
+ */
42
+ readonly glsp: MessageConnection;
43
+ /**
44
+ * Every failure the worker reports on its global, and its `error` event
45
+ * under {@link WORKER_ERROR_EVENT_PHASE}, before and after the heads have
46
+ * ended. Whether a report ended them is {@link onDispose}'s to say.
47
+ */
48
+ readonly onWorkerError: Event<WorkerErrorMessage>;
49
+ /**
50
+ * Fires once when the heads end: on a failed bootstrap, on an `error` event
51
+ * before the worker has posted on any head's port, or on {@link dispose}.
52
+ * What the heads had pending is rejected then rather than left waiting. Any
53
+ * later report, and an `error` event once the worker has posted, ends
54
+ * nothing: the worker runs on after an uncaught error, and the heads with
55
+ * it. Subscribe before the first `await`: the end fires once, and a
56
+ * subscriber after it hears nothing.
57
+ */
58
+ readonly onDispose: Event<void>;
59
+ /** End every head. A head whose connection exists signals its close to the worker, which ends the head there. */
60
+ dispose(): void;
61
+ }
62
+ /**
63
+ * Hand each head its own channel into `worker` and build the page's end of
64
+ * each.
65
+ *
66
+ * The worker keeps `port2` of every pair and the page `port1`; neither end is
67
+ * given the worker global, which carries the bootstrap, the worker's failure
68
+ * reports and whatever the host adds for itself. See the handshake in
69
+ * `@hydranium/protocol` for why a head on the global is unusable.
70
+ *
71
+ * **A port reports nothing when its peer dies**, so a worker that fails to
72
+ * load, throws before its bootstrap or fails it would leave every request on
73
+ * every head waiting forever, which looks exactly like a server still working.
74
+ * Those signals dispose the heads instead, and disposing a connection is what
75
+ * rejects its pending requests; a close alone does not. A worker that dies
76
+ * later still cannot be told from one that is working.
77
+ */
78
+ export declare function connectWorkerHeads(worker: Worker, options?: WorkerHeadsOptions): WorkerHeads;
79
+ //# sourceMappingURL=worker-heads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-heads.d.ts","sourceRoot":"","sources":["../src/worker-heads.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAMJ,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,sBAAsB,EAE3B,KAAK,kBAAkB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAoC,KAAK,UAAU,EAAE,KAAK,KAAK,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAG/H,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD,MAAM,WAAW,kBAAkB;IAChC,mHAAmH;IACnH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,sBAAsB,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,QAAQ,GAAG,UAAU,CAAC;CACrG;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IACzB;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC;IAChC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAClD;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,iHAAiH;IACjH,OAAO,IAAI,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAwEhG"}
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.WORKER_ERROR_EVENT_PHASE = void 0;
12
+ exports.connectWorkerHeads = connectWorkerHeads;
13
+ const protocol_1 = require("@hydranium/protocol");
14
+ const browser_1 = require("vscode-jsonrpc/browser");
15
+ const worker_data_port_1 = require("./worker-data-port");
16
+ /** The phase a report synthesised from the `Worker`'s `error` event carries. */
17
+ exports.WORKER_ERROR_EVENT_PHASE = 'worker';
18
+ /**
19
+ * Hand each head its own channel into `worker` and build the page's end of
20
+ * each.
21
+ *
22
+ * The worker keeps `port2` of every pair and the page `port1`; neither end is
23
+ * given the worker global, which carries the bootstrap, the worker's failure
24
+ * reports and whatever the host adds for itself. See the handshake in
25
+ * `@hydranium/protocol` for why a head on the global is unusable.
26
+ *
27
+ * **A port reports nothing when its peer dies**, so a worker that fails to
28
+ * load, throws before its bootstrap or fails it would leave every request on
29
+ * every head waiting forever, which looks exactly like a server still working.
30
+ * Those signals dispose the heads instead, and disposing a connection is what
31
+ * rejects its pending requests; a close alone does not. A worker that dies
32
+ * later still cannot be told from one that is working.
33
+ */
34
+ function connectWorkerHeads(worker, options = {}) {
35
+ const lsp = new MessageChannel();
36
+ const data = new MessageChannel();
37
+ const glsp = new MessageChannel();
38
+ const bootstrap = {
39
+ type: protocol_1.WORKER_BOOTSTRAP_MESSAGE_TYPE,
40
+ ports: { lsp: lsp.port2, data: data.port2, glsp: glsp.port2 }
41
+ };
42
+ worker.postMessage(bootstrap, [lsp.port2, data.port2, glsp.port2]);
43
+ // Both ends use `createMessagePortTransport`; see it for why.
44
+ const lspTransport = (0, protocol_1.createMessagePortTransport)(lsp.port1);
45
+ const lspConnection = (0, browser_1.createMessageConnection)(lspTransport.reader, lspTransport.writer, options.logger?.with('LSP head'));
46
+ const dataLogger = options.logger?.with('data head');
47
+ const dataPort = options.createDataPort?.(data.port1, dataLogger) ?? new worker_data_port_1.WorkerDataPort(data.port1, dataLogger);
48
+ const glspTransport = (0, protocol_1.createMessagePortTransport)(glsp.port1);
49
+ const glspConnection = (0, browser_1.createMessageConnection)(glspTransport.reader, glspTransport.writer, options.logger?.with('GLSP head'));
50
+ const errorEmitter = new browser_1.Emitter();
51
+ const disposeEmitter = new browser_1.Emitter();
52
+ // Whether the worker has posted anything on a head's port, which only a
53
+ // worker past its bootstrap does. An `error` event before that is a worker
54
+ // that never started; one after it is an uncaught error the worker survives.
55
+ let workerPosted = false;
56
+ for (const port of [lsp.port1, data.port1, glsp.port1]) {
57
+ port.addEventListener('message', () => (workerPosted = true), { once: true });
58
+ }
59
+ let disposed = false;
60
+ const dispose = () => {
61
+ if (disposed) {
62
+ return;
63
+ }
64
+ disposed = true;
65
+ lspConnection.dispose();
66
+ dataPort.dispose();
67
+ glspConnection.dispose();
68
+ disposeEmitter.fire();
69
+ disposeEmitter.dispose();
70
+ };
71
+ // A worker that failed to load raises an event with no message, filename or
72
+ // line, indistinguishable from a crash on its first statement; the host
73
+ // adds the URL it tried.
74
+ worker.addEventListener('error', event => {
75
+ errorEmitter.fire({
76
+ type: protocol_1.WORKER_ERROR_MESSAGE_TYPE,
77
+ phase: exports.WORKER_ERROR_EVENT_PHASE,
78
+ message: event.message || 'no detail; check that the worker script loaded'
79
+ });
80
+ if (!workerPosted) {
81
+ dispose();
82
+ }
83
+ });
84
+ worker.addEventListener('message', event => {
85
+ if (!(0, protocol_1.isWorkerErrorMessage)(event.data)) {
86
+ return;
87
+ }
88
+ errorEmitter.fire(event.data);
89
+ if (event.data.phase === protocol_1.WORKER_BOOTSTRAP_PHASE) {
90
+ dispose();
91
+ }
92
+ });
93
+ return {
94
+ lsp: lspConnection,
95
+ data: dataPort,
96
+ glsp: glspConnection,
97
+ onWorkerError: errorEmitter.event,
98
+ onDispose: disposeEmitter.event,
99
+ dispose
100
+ };
101
+ }
102
+ //# sourceMappingURL=worker-heads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-heads.js","sourceRoot":"","sources":["../src/worker-heads.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "@hydranium/client-browser",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-next",
4
+ "description": "The page side of a hydranium host that runs its heads in a web worker: the worker bootstrap that hands each head its own MessagePort, the page's LSP handshake, the data head's port, and the rejection of what is pending when the worker dies.",
5
+ "keywords": [
6
+ "browser",
7
+ "hydranium",
8
+ "langium",
9
+ "language-server",
10
+ "lsp",
11
+ "web-worker"
12
+ ],
13
+ "homepage": "https://github.com/eclipse-emfcloud/hydranium/tree/main/packages/client-browser",
14
+ "bugs": {
15
+ "url": "https://github.com/eclipse-emfcloud/hydranium/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": {
19
+ "name": "Hydranium Team"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/eclipse-emfcloud/hydranium.git",
24
+ "directory": "packages/client-browser"
25
+ },
26
+ "files": [
27
+ "lib",
28
+ "src",
29
+ "!lib/**/*.tsbuildinfo"
30
+ ],
31
+ "sideEffects": false,
32
+ "main": "lib/index.js",
33
+ "types": "lib/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./lib/index.d.ts",
37
+ "default": "./lib/index.js"
38
+ }
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -b",
45
+ "clean": "rimraf lib tsconfig.tsbuildinfo",
46
+ "lint": "oxlint --format stylish --config ../../oxlint.config.cjs src test --max-warnings 0",
47
+ "prepack": "node -e \"const m=require('./package.json'),fs=require('node:fs');const missing=[m.main,...Object.values(m.bin||{})].filter(entry=>entry&&!fs.existsSync(entry));if(missing.length){console.error('prepack '+m.name+': not built ('+missing.join(', ')+' missing). Run the build before packing: a files entry that matches nothing is skipped silently, so the tarball would ship src only.');process.exit(1);}\"",
48
+ "test": "npm run typecheck:test && vitest run",
49
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
50
+ "watch": "tsc -b -w --preserveWatchOutput"
51
+ },
52
+ "devDependencies": {
53
+ "@hydranium/protocol": "1.0.0-next",
54
+ "@types/node": "^22.0.0",
55
+ "rimraf": "^5.0.0",
56
+ "typescript": "^7.0.2",
57
+ "vscode-jsonrpc": "9.0.0",
58
+ "vscode-languageserver-protocol": "3.18.1"
59
+ },
60
+ "peerDependencies": {
61
+ "@hydranium/protocol": "^1.0.0-next",
62
+ "vscode-jsonrpc": "^9.0.0",
63
+ "vscode-languageserver-protocol": "~3.18.1"
64
+ },
65
+ "engines": {
66
+ "node": ">=22.13"
67
+ },
68
+ "//peerDependencies": "`vscode-jsonrpc` takes a caret, so the framework shares the transport the LSP connection resolves, which an `init` scaffold pins: each `vscode-languageserver-protocol` release pins its transport exactly, so an exact peer would name a copy a fresh install rarely holds. The caret excludes 8.x, so a tree whose top holds GLSP's copy fails the peer with a named ERESOLVE warning, an error under `--strict-peer-deps`, rather than resolving silently. No peer reaches a NESTED copy, such as GLSP's or one the LSP packages nest. The wire compares message types and `ResponseError`s by copy identity, so the framework sends typed messages by method name and recognises errors with `isResponseError` rather than `instanceof`.",
69
+ "//prepack": "The publish guard, a `prepack` rather than a `prepare`: `prepack` runs only when a tarball is made (`npm pack`, `npm publish`) and never on install, so it cannot break the install it has no business touching. What it defends against: `files` lists `lib`, `lib` is gitignored, and a `files` entry matching nothing is skipped SILENTLY — so `npm publish` from an unbuilt tree emits a tarball of `src` and nothing else, with no error."
70
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
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 './worker-heads';
11
+ export * from './language-client';
12
+ export * from './worker-data-port';
@@ -0,0 +1,110 @@
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 { Locale } from '@hydranium/protocol';
11
+ import type { MessageConnection } from 'vscode-jsonrpc/browser';
12
+ import {
13
+ ConfigurationRequest,
14
+ InitializedNotification,
15
+ InitializeRequest,
16
+ LogMessageNotification,
17
+ RegistrationRequest,
18
+ UnregistrationRequest,
19
+ type ClientCapabilities,
20
+ type InitializeResult,
21
+ type LogMessageParams
22
+ } from 'vscode-languageserver-protocol';
23
+
24
+ /** The one configuration section the server reads, and what it reads there. */
25
+ export interface LanguageClientConfiguration {
26
+ /** The server's `lsp.configurationRoot`. A mismatch leaves every answer `null`, so the setting reads as unset. */
27
+ readonly section: string;
28
+ /** The section's whole subtree. The server replaces its cached section with what it is given, so a partial answer deletes the siblings it omits. */
29
+ settings(): unknown;
30
+ }
31
+
32
+ export interface LanguageClientOptions {
33
+ /** The workspace root, a URI, which is also the one workspace folder. */
34
+ readonly rootUri: string;
35
+ /** The folder's name, the root's last path segment by default. */
36
+ readonly workspaceName?: string;
37
+ /** The reading user's language, which only the page knows. Absent means the framework's English. */
38
+ readonly locale?: Locale;
39
+ /**
40
+ * The client's capabilities, `workspace.applyEdit` and
41
+ * `workspace.configuration` by default. `configuration` gates its request:
42
+ * absent, the server asks for no section ever. `applyEdit` does not: the
43
+ * server sends `workspace/applyEdit` to any client with the document open,
44
+ * and a client with no handler answers it as an unknown method, which loses
45
+ * the whole inbound direction. Declaring less than the handlers the host
46
+ * registers is the one safe direction; declaring more produces answers it
47
+ * cannot render.
48
+ */
49
+ readonly capabilities?: ClientCapabilities;
50
+ /** Answered from the start, since the server reads it while the workspace comes up. */
51
+ readonly configuration?: LanguageClientConfiguration;
52
+ /** The server's log, every head of it; a page with no handler loses it. Registered before `listen`, so the first build's lines arrive too. */
53
+ readonly onLogMessage?: (params: LogMessageParams) => void;
54
+ /**
55
+ * Called with the server's answer before `initialized` is sent: the window
56
+ * in which a handler for the server's first notifications is registered
57
+ * once the answer it needs is known. The server publishes nothing before
58
+ * `initialized`, so nothing is missed.
59
+ */
60
+ readonly beforeInitialized?: (result: InitializeResult) => void;
61
+ }
62
+
63
+ /** The capabilities the server's inbound requests rely on, declared unless the host declares its own. */
64
+ const DEFAULT_CAPABILITIES: ClientCapabilities = { workspace: { applyEdit: true, configuration: true } };
65
+
66
+ /**
67
+ * Run the page's end of the LSP handshake on `connection`, which must not be
68
+ * listening yet, and resolve with the server's answer.
69
+ *
70
+ * Registers the answers to the server's own requests before `listen`, because
71
+ * each is asked while the workspace comes up and an unanswered one is a
72
+ * failure the page sees as an error and the server as a client with no
73
+ * settings: capability registrations are acknowledged and kept nowhere, and a
74
+ * configuration request is answered item by item, since the response is
75
+ * positional and a dropped item mis-aligns every later answer. These
76
+ * registrations replace any the host made before the call, since a
77
+ * connection keeps one handler per method; a host serving more than one
78
+ * section registers its own configuration handler in `beforeInitialized`.
79
+ */
80
+ export async function initializeLanguageClient(connection: MessageConnection, options: LanguageClientOptions): Promise<InitializeResult> {
81
+ // Before `listen`: a root that is no URI throws here, and a connection left
82
+ // listening with no `initialize` sent would be one the host cannot reuse.
83
+ const workspaceName = options.workspaceName ?? workspaceNameOf(options.rootUri);
84
+ connection.onRequest(RegistrationRequest.type, () => undefined);
85
+ connection.onRequest(UnregistrationRequest.type, () => undefined);
86
+ const configuration = options.configuration;
87
+ connection.onRequest(ConfigurationRequest.type, params =>
88
+ params.items.map(item => (configuration !== undefined && item.section === configuration.section ? configuration.settings() : null))
89
+ );
90
+ if (options.onLogMessage) {
91
+ connection.onNotification(LogMessageNotification.type, options.onLogMessage);
92
+ }
93
+ connection.listen();
94
+
95
+ const result = await connection.sendRequest(InitializeRequest.type, {
96
+ processId: null,
97
+ rootUri: options.rootUri,
98
+ locale: options.locale,
99
+ capabilities: options.capabilities ?? DEFAULT_CAPABILITIES,
100
+ workspaceFolders: [{ uri: options.rootUri, name: workspaceName }]
101
+ });
102
+ options.beforeInitialized?.(result);
103
+ await connection.sendNotification(InitializedNotification.type, {});
104
+ return result;
105
+ }
106
+
107
+ /** The root's last path segment, decoded, or the root itself for one with no path. */
108
+ function workspaceNameOf(rootUri: string): string {
109
+ return decodeURIComponent(new URL(rootUri).pathname.replace(/\/+$/, '').split('/').pop() ?? '') || rootUri;
110
+ }
@@ -0,0 +1,97 @@
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
+ createMessagePortTransport,
12
+ renderFrameworkMessage,
13
+ type DataPort,
14
+ type Logger,
15
+ type ResolvedMessage,
16
+ type TransferredMessagePort
17
+ } from '@hydranium/protocol';
18
+ import { createMessageConnection, Emitter, type MessageConnection } from 'vscode-jsonrpc/browser';
19
+
20
+ /**
21
+ * The host half of the data head for a page talking to a worker.
22
+ *
23
+ * This is the whole of what a browser host has to supply to reach the data
24
+ * head; everything above it (the open/watch order, `baseVersion` conflict
25
+ * handling, echo filtering by `sourceClientId`) is host-invariant and already
26
+ * in `DataConnection` and `DataSession`.
27
+ *
28
+ * `vscode-jsonrpc/browser`, never the package root: version 9's root entry ships
29
+ * no runtime abstraction layer and throws on the first message rather than at
30
+ * import, so the mistake surfaces as a dead connection rather than a stack
31
+ * trace.
32
+ */
33
+ export class WorkerDataPort implements DataPort {
34
+ protected readonly disposeEmitter = new Emitter<void>();
35
+ readonly onDispose = this.disposeEmitter.event;
36
+ protected disposed = false;
37
+ protected connection?: MessageConnection;
38
+
39
+ constructor(
40
+ /** The page's end of the data head's port, typed as the handshake types it. */
41
+ protected readonly port: TransferredMessagePort,
42
+ /** Where the connection logs its protocol faults, and what it does on its own. */
43
+ readonly logger?: Logger
44
+ ) {}
45
+
46
+ /**
47
+ * The transport is the port the worker was already handed at bootstrap, so
48
+ * there is nothing to open, but the connection must be `listen()`ing before
49
+ * it is returned, because the RPC proxy queues calls on this promise and
50
+ * never listens itself.
51
+ *
52
+ * Both ends use `createMessagePortTransport`; see it for why.
53
+ *
54
+ * Every generation gets the same connection. A fresh one per generation
55
+ * would leave the one a failed readiness check used still reading the port,
56
+ * and disposing that one is no way out: disposing any connection over the
57
+ * port ends the worker's head, and the retry would wait for an answer that
58
+ * never comes.
59
+ *
60
+ * **Rejects once this port is disposed, and a disposed port cannot be
61
+ * reused.** Its dispose disposes the connection, which signals the close to
62
+ * the worker, whose end of the port then stays closed: a later connection
63
+ * over it would send requests nothing answers, and hang.
64
+ */
65
+ connect(): Promise<MessageConnection> {
66
+ if (this.disposed) {
67
+ return Promise.reject(new Error('WorkerDataPort: the port is disposed, and the data head behind it has ended'));
68
+ }
69
+ if (!this.connection) {
70
+ const transport = createMessagePortTransport(this.port);
71
+ this.connection = createMessageConnection(transport.reader, transport.writer, this.logger);
72
+ this.connection.listen();
73
+ }
74
+ return Promise.resolve(this.connection);
75
+ }
76
+
77
+ /**
78
+ * To the console: a page has no notification surface of its own, and an
79
+ * unreported data-head error presents as an empty model, which reads as a
80
+ * valid document. A host with a surface overrides this.
81
+ *
82
+ * No translation map: omitting the argument takes the English. The raw
83
+ * error goes alongside rather than into the sentence, because `reported`
84
+ * already carries the detail and only the throw carries a stack.
85
+ */
86
+ reportError(error: unknown, reported: ResolvedMessage): void {
87
+ console.error(`[data head] ${renderFrameworkMessage(reported)}`, error);
88
+ }
89
+
90
+ /** The consumer hears it first; the connection goes even when no generation holds it, as after a failed readiness check. */
91
+ dispose(): void {
92
+ this.disposed = true;
93
+ this.disposeEmitter.fire();
94
+ this.disposeEmitter.dispose();
95
+ this.connection?.dispose();
96
+ }
97
+ }
@@ -0,0 +1,167 @@
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
+ createMessagePortTransport,
12
+ isWorkerErrorMessage,
13
+ WORKER_BOOTSTRAP_MESSAGE_TYPE,
14
+ WORKER_BOOTSTRAP_PHASE,
15
+ WORKER_ERROR_MESSAGE_TYPE,
16
+ type DataPort,
17
+ type Logger,
18
+ type TransferredMessagePort,
19
+ type WorkerBootstrapMessage,
20
+ type WorkerErrorMessage
21
+ } from '@hydranium/protocol';
22
+ import { createMessageConnection, Emitter, type Disposable, type Event, type MessageConnection } from 'vscode-jsonrpc/browser';
23
+ import { WorkerDataPort } from './worker-data-port';
24
+
25
+ /** The phase a report synthesised from the `Worker`'s `error` event carries. */
26
+ export const WORKER_ERROR_EVENT_PHASE = 'worker';
27
+
28
+ export interface WorkerHeadsOptions {
29
+ /** Where each head's connection logs its protocol faults, one component per head. Without one they log nothing. */
30
+ readonly logger?: Logger;
31
+ /**
32
+ * Build the data head's port over the page's end of its `MessagePort`, a
33
+ * {@link WorkerDataPort} by default. A host with a surface of its own for
34
+ * data-head failures supplies a subclass here.
35
+ */
36
+ readonly createDataPort?: (port: TransferredMessagePort, logger?: Logger) => DataPort & Disposable;
37
+ }
38
+
39
+ /** The page's end of each head, and what the worker says outside any head. */
40
+ export interface WorkerHeads {
41
+ /**
42
+ * The LSP head's connection, not yet listening: register the handlers for
43
+ * the server's first requests, then `listen()` and send `initialize`. A
44
+ * message that arrives before `listen` is dropped.
45
+ */
46
+ readonly lsp: MessageConnection;
47
+ /** The data head's port, for a `DataConnection`. */
48
+ readonly data: DataPort;
49
+ /**
50
+ * The GLSP head's connection, not listening: GLSP's client calls `listen()`
51
+ * when it starts, and a second call throws. Its port is started, so a
52
+ * message arriving before then is dropped; the GLSP head posts nothing on
53
+ * its port before the client speaks. Hand it to that client wrapped with
54
+ * `sendByMethodName`, where the client's own connection type is named:
55
+ * GLSP's typed messages come from its own copy of `vscode-jsonrpc`, and
56
+ * this connection's copy rejects them unless they go by method name.
57
+ */
58
+ readonly glsp: MessageConnection;
59
+ /**
60
+ * Every failure the worker reports on its global, and its `error` event
61
+ * under {@link WORKER_ERROR_EVENT_PHASE}, before and after the heads have
62
+ * ended. Whether a report ended them is {@link onDispose}'s to say.
63
+ */
64
+ readonly onWorkerError: Event<WorkerErrorMessage>;
65
+ /**
66
+ * Fires once when the heads end: on a failed bootstrap, on an `error` event
67
+ * before the worker has posted on any head's port, or on {@link dispose}.
68
+ * What the heads had pending is rejected then rather than left waiting. Any
69
+ * later report, and an `error` event once the worker has posted, ends
70
+ * nothing: the worker runs on after an uncaught error, and the heads with
71
+ * it. Subscribe before the first `await`: the end fires once, and a
72
+ * subscriber after it hears nothing.
73
+ */
74
+ readonly onDispose: Event<void>;
75
+ /** End every head. A head whose connection exists signals its close to the worker, which ends the head there. */
76
+ dispose(): void;
77
+ }
78
+
79
+ /**
80
+ * Hand each head its own channel into `worker` and build the page's end of
81
+ * each.
82
+ *
83
+ * The worker keeps `port2` of every pair and the page `port1`; neither end is
84
+ * given the worker global, which carries the bootstrap, the worker's failure
85
+ * reports and whatever the host adds for itself. See the handshake in
86
+ * `@hydranium/protocol` for why a head on the global is unusable.
87
+ *
88
+ * **A port reports nothing when its peer dies**, so a worker that fails to
89
+ * load, throws before its bootstrap or fails it would leave every request on
90
+ * every head waiting forever, which looks exactly like a server still working.
91
+ * Those signals dispose the heads instead, and disposing a connection is what
92
+ * rejects its pending requests; a close alone does not. A worker that dies
93
+ * later still cannot be told from one that is working.
94
+ */
95
+ export function connectWorkerHeads(worker: Worker, options: WorkerHeadsOptions = {}): WorkerHeads {
96
+ const lsp = new MessageChannel();
97
+ const data = new MessageChannel();
98
+ const glsp = new MessageChannel();
99
+ const bootstrap: WorkerBootstrapMessage = {
100
+ type: WORKER_BOOTSTRAP_MESSAGE_TYPE,
101
+ ports: { lsp: lsp.port2, data: data.port2, glsp: glsp.port2 }
102
+ };
103
+ worker.postMessage(bootstrap, [lsp.port2, data.port2, glsp.port2]);
104
+
105
+ // Both ends use `createMessagePortTransport`; see it for why.
106
+ const lspTransport = createMessagePortTransport(lsp.port1);
107
+ const lspConnection = createMessageConnection(lspTransport.reader, lspTransport.writer, options.logger?.with('LSP head'));
108
+ const dataLogger = options.logger?.with('data head');
109
+ const dataPort = options.createDataPort?.(data.port1, dataLogger) ?? new WorkerDataPort(data.port1, dataLogger);
110
+ const glspTransport = createMessagePortTransport(glsp.port1);
111
+ const glspConnection = createMessageConnection(glspTransport.reader, glspTransport.writer, options.logger?.with('GLSP head'));
112
+ const errorEmitter = new Emitter<WorkerErrorMessage>();
113
+ const disposeEmitter = new Emitter<void>();
114
+
115
+ // Whether the worker has posted anything on a head's port, which only a
116
+ // worker past its bootstrap does. An `error` event before that is a worker
117
+ // that never started; one after it is an uncaught error the worker survives.
118
+ let workerPosted = false;
119
+ for (const port of [lsp.port1, data.port1, glsp.port1]) {
120
+ port.addEventListener('message', () => (workerPosted = true), { once: true });
121
+ }
122
+
123
+ let disposed = false;
124
+ const dispose = (): void => {
125
+ if (disposed) {
126
+ return;
127
+ }
128
+ disposed = true;
129
+ lspConnection.dispose();
130
+ dataPort.dispose();
131
+ glspConnection.dispose();
132
+ disposeEmitter.fire();
133
+ disposeEmitter.dispose();
134
+ };
135
+
136
+ // A worker that failed to load raises an event with no message, filename or
137
+ // line, indistinguishable from a crash on its first statement; the host
138
+ // adds the URL it tried.
139
+ worker.addEventListener('error', event => {
140
+ errorEmitter.fire({
141
+ type: WORKER_ERROR_MESSAGE_TYPE,
142
+ phase: WORKER_ERROR_EVENT_PHASE,
143
+ message: event.message || 'no detail; check that the worker script loaded'
144
+ });
145
+ if (!workerPosted) {
146
+ dispose();
147
+ }
148
+ });
149
+ worker.addEventListener('message', event => {
150
+ if (!isWorkerErrorMessage(event.data)) {
151
+ return;
152
+ }
153
+ errorEmitter.fire(event.data);
154
+ if (event.data.phase === WORKER_BOOTSTRAP_PHASE) {
155
+ dispose();
156
+ }
157
+ });
158
+
159
+ return {
160
+ lsp: lspConnection,
161
+ data: dataPort,
162
+ glsp: glspConnection,
163
+ onWorkerError: errorEmitter.event,
164
+ onDispose: disposeEmitter.event,
165
+ dispose
166
+ };
167
+ }