@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 +21 -0
- package/README.md +101 -2
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/language-client.d.ts +64 -0
- package/lib/language-client.d.ts.map +1 -0
- package/lib/language-client.js +56 -0
- package/lib/language-client.js.map +1 -0
- package/lib/worker-data-port.d.ts +71 -0
- package/lib/worker-data-port.d.ts.map +1 -0
- package/lib/worker-data-port.js +93 -0
- package/lib/worker-data-port.js.map +1 -0
- package/lib/worker-heads.d.ts +79 -0
- package/lib/worker-heads.d.ts.map +1 -0
- package/lib/worker-heads.js +102 -0
- package/lib/worker-heads.js.map +1 -0
- package/package.json +68 -4
- package/src/index.ts +12 -0
- package/src/language-client.ts +110 -0
- package/src/worker-data-port.ts +97 -0
- package/src/worker-heads.ts +167 -0
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
|
-
#
|
|
1
|
+
# `@hydranium/client-browser`
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/lib/index.js.map
ADDED
|
@@ -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":";AAAA;;;;;;;kFAOkF;;;;AAElF,kDAW6B;AAC7B,oDAA+H;AAC/H,yDAAoD;AAEpD,gFAAgF;AACnE,QAAA,wBAAwB,GAAG,QAAQ,CAAC;AAqDjD;;;;;;;;;;;;;;;GAeG;AACH,4BAAmC,MAAc,EAAE,OAAO,GAAuB,EAAE;IAChF,MAAM,GAAG,GAAG,IAAI,cAAc,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,cAAc,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,cAAc,EAAE,CAAC;IAClC,MAAM,SAAS,GAA2B;QACvC,IAAI,EAAE,wCAA6B;QACnC,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE;KAC/D,CAAC;IACF,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAEnE,8DAA8D;IAC9D,MAAM,YAAY,GAAG,IAAA,qCAA0B,EAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC3D,MAAM,aAAa,GAAG,IAAA,iCAAuB,EAAC,YAAY,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IAC1H,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,IAAI,IAAI,iCAAc,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAChH,MAAM,aAAa,GAAG,IAAA,qCAA0B,EAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7D,MAAM,cAAc,GAAG,IAAA,iCAAuB,EAAC,aAAa,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAC9H,MAAM,YAAY,GAAG,IAAI,iBAAO,EAAsB,CAAC;IACvD,MAAM,cAAc,GAAG,IAAI,iBAAO,EAAQ,CAAC;IAE3C,wEAAwE;IACxE,2EAA2E;IAC3E,6EAA6E;IAC7E,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtD,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACjF,CAAC;IAED,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,OAAO,GAAG,GAAS,EAAE;QACxB,IAAI,QAAQ,EAAE,CAAC;YACZ,OAAO;QACV,CAAC;QACD,QAAQ,GAAG,IAAI,CAAC;QAChB,aAAa,CAAC,OAAO,EAAE,CAAC;QACxB,QAAQ,CAAC,OAAO,EAAE,CAAC;QACnB,cAAc,CAAC,OAAO,EAAE,CAAC;QACzB,cAAc,CAAC,IAAI,EAAE,CAAC;QACtB,cAAc,CAAC,OAAO,EAAE,CAAC;IAC5B,CAAC,CAAC;IAEF,4EAA4E;IAC5E,wEAAwE;IACxE,yBAAyB;IACzB,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE;QACtC,YAAY,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,oCAAyB;YAC/B,KAAK,EAAE,QAAA,wBAAwB;YAC/B,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,gDAAgD;SAC5E,CAAC,CAAC;QACH,IAAI,CAAC,YAAY,EAAE,CAAC;YACjB,OAAO,EAAE,CAAC;QACb,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE;QACxC,IAAI,CAAC,IAAA,+BAAoB,EAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,OAAO;QACV,CAAC;QACD,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,iCAAsB,EAAE,CAAC;YAC/C,OAAO,EAAE,CAAC;QACb,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO;QACJ,GAAG,EAAE,aAAa;QAClB,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,cAAc;QACpB,aAAa,EAAE,YAAY,CAAC,KAAK;QACjC,SAAS,EAAE,cAAc,CAAC,KAAK;QAC/B,OAAO;KACT,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hydranium/client-browser",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|