@hydranium/core 1.0.0-next.298 → 1.0.0-next.299

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,6 +49,13 @@ needs it.
49
49
  3. Start other heads on the same shared services, with `startSocketServer` and
50
50
  `publishPortOnLspConnection` beside the LSP head, or `startStdioServer`
51
51
  alone.
52
+ 4. In a web worker, answer the page's bootstrap with `hostHeadsInWorker` from
53
+ `./browser`: it awaits what you prepared, such as `persistentFileSystem`,
54
+ then calls your `start`, which starts the LSP head on its port with
55
+ `startLanguageServerInWorker` and the other heads with their own starters.
56
+ It posts the ready message once every head is up, runs the validating
57
+ build and reports a failed start, build or rejection to the page;
58
+ `reportWorkerError` reports a failure of your own.
52
59
 
53
60
  See *Compose a server by hand* and *Add a validation check* in
54
61
  [Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
@@ -59,7 +66,7 @@ See *Compose a server by hand* and *Add a validation check* in
59
66
  | ---------------------- | ---------------------------------------------------------------------------- | --------------- |
60
67
  | `.` | Composing services and extending the language's semantics | browser-neutral |
61
68
  | `./lsp` | The LSP head and its modules | browser-neutral |
62
- | `./browser` | The IndexedDB store behind a persistent filesystem in a web worker | browser-only |
69
+ | `./browser` | Hosting the heads in a web worker, and the IndexedDB store behind them | browser-only |
63
70
  | `./node` | The Node filesystem, the stdio and socket launchers, the headless tools | Node-only |
64
71
  | `./messages` | Validation messages with stable codes, and the codes this package raises | browser-neutral |
65
72
  | `./testing` | Parsing and service doubles for unit tests | browser-neutral |
@@ -0,0 +1,58 @@
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 HeadPorts } from '@hydranium/protocol';
10
+ import type { ServerSharedServicesMinimal } from '../langium/shared-services.js';
11
+ /**
12
+ * Report a failure to the page on the worker global, the channel no head is
13
+ * allowed to bind, which is what keeps it free for this. A report under
14
+ * {@link WORKER_BOOTSTRAP_PHASE} ends the page's heads; any other phase is
15
+ * shown there and the heads stay. The stack goes with it: a worker has no log
16
+ * anyone reads, so the page is the one place the stack can be seen.
17
+ */
18
+ export declare function reportWorkerError(phase: string, error: unknown): void;
19
+ /** What the host's `start` hands back: the shared tier, and the readiness of any head that reports it later. */
20
+ export interface StartedHeads {
21
+ readonly shared: ServerSharedServicesMinimal;
22
+ /**
23
+ * The `started` promise of every head that reports a failed start through
24
+ * one rather than by throwing, such as the GLSP head's. The ready message
25
+ * waits for them, and a rejection is a failed bootstrap.
26
+ */
27
+ readonly started?: ReadonlyArray<Promise<unknown>>;
28
+ }
29
+ /**
30
+ * Answer the page's bootstrap by starting the heads on its ports, once per
31
+ * worker, and tell the page when they are up and when something fails.
32
+ *
33
+ * `start` is synchronous by type, and that is the ordering contract: every
34
+ * head's transport starts its port as it is built but drops what arrives
35
+ * before the head listens, so an `await` between building a head and
36
+ * listening loses the page's first request with no error. Everything
37
+ * asynchronous, the filesystem's restore above all, is `prepared`, which is
38
+ * awaited before `start` is called. It may be a plain value where nothing has
39
+ * to be awaited.
40
+ *
41
+ * Once `start` has returned and every `started` it handed back has resolved,
42
+ * the ready message goes out, so a page never hears of heads that failed to
43
+ * come up, and an uncaught error from then on is a report rather than the end
44
+ * of the heads. Then every document is built with validation once the
45
+ * workspace has initialised: Langium's initial build validates nothing, so a
46
+ * page that only opens its ports would see a server that indexed the
47
+ * workspace and says nothing. An initial build that a write during startup
48
+ * cancelled is built the same, since every document exists by then.
49
+ *
50
+ * Call it once: a second call answers the same bootstrap with a second set of
51
+ * heads on the same ports and reports every rejection twice. The worker's
52
+ * `unhandledrejection` listener is this function's; it drops Langium's
53
+ * `OperationCancelled` marker, which a build preempted by a write rejects
54
+ * with, as the Node host does. An uncaught throw needs no listener, since it
55
+ * reaches the page as the `Worker`'s own `error` event.
56
+ */
57
+ export declare function hostHeadsInWorker<TPrepared>(prepared: TPrepared | PromiseLike<TPrepared>, start: (ports: HeadPorts, prepared: TPrepared) => ServerSharedServicesMinimal | StartedHeads): void;
58
+ //# sourceMappingURL=host-heads-in-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-heads-in-worker.d.ts","sourceRoot":"","sources":["../../src/browser/host-heads-in-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAMJ,KAAK,SAAS,EAGhB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAGjF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAGrE;AAED,gHAAgH;AAChH,MAAM,WAAW,YAAY;IAC1B,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EACxC,QAAQ,EAAE,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC,EAC5C,KAAK,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,KAAK,2BAA2B,GAAG,YAAY,GAC5F,IAAI,CAiDN"}
@@ -0,0 +1,113 @@
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 { describeErrorWithStack, isWorkerBootstrapMessage, WORKER_BOOTSTRAP_PHASE, WORKER_ERROR_MESSAGE_TYPE, WORKER_READY_MESSAGE_TYPE } from '@hydranium/protocol';
10
+ import { isOperationCancelled } from '@hydranium/langium';
11
+ import { isConnectionGoneError } from '../util/connection-liveness.js';
12
+ /**
13
+ * Report a failure to the page on the worker global, the channel no head is
14
+ * allowed to bind, which is what keeps it free for this. A report under
15
+ * {@link WORKER_BOOTSTRAP_PHASE} ends the page's heads; any other phase is
16
+ * shown there and the heads stay. The stack goes with it: a worker has no log
17
+ * anyone reads, so the page is the one place the stack can be seen.
18
+ */
19
+ export function reportWorkerError(phase, error) {
20
+ const report = { type: WORKER_ERROR_MESSAGE_TYPE, phase, message: describeErrorWithStack(error) };
21
+ postMessage(report);
22
+ }
23
+ /**
24
+ * Answer the page's bootstrap by starting the heads on its ports, once per
25
+ * worker, and tell the page when they are up and when something fails.
26
+ *
27
+ * `start` is synchronous by type, and that is the ordering contract: every
28
+ * head's transport starts its port as it is built but drops what arrives
29
+ * before the head listens, so an `await` between building a head and
30
+ * listening loses the page's first request with no error. Everything
31
+ * asynchronous, the filesystem's restore above all, is `prepared`, which is
32
+ * awaited before `start` is called. It may be a plain value where nothing has
33
+ * to be awaited.
34
+ *
35
+ * Once `start` has returned and every `started` it handed back has resolved,
36
+ * the ready message goes out, so a page never hears of heads that failed to
37
+ * come up, and an uncaught error from then on is a report rather than the end
38
+ * of the heads. Then every document is built with validation once the
39
+ * workspace has initialised: Langium's initial build validates nothing, so a
40
+ * page that only opens its ports would see a server that indexed the
41
+ * workspace and says nothing. An initial build that a write during startup
42
+ * cancelled is built the same, since every document exists by then.
43
+ *
44
+ * Call it once: a second call answers the same bootstrap with a second set of
45
+ * heads on the same ports and reports every rejection twice. The worker's
46
+ * `unhandledrejection` listener is this function's; it drops Langium's
47
+ * `OperationCancelled` marker, which a build preempted by a write rejects
48
+ * with, as the Node host does. An uncaught throw needs no listener, since it
49
+ * reaches the page as the `Worker`'s own `error` event.
50
+ */
51
+ export function hostHeadsInWorker(prepared, start) {
52
+ const preparation = Promise.resolve(prepared);
53
+ // Handled here, so a restore that fails before the bootstrap arrives reaches
54
+ // the page once, under the bootstrap phase, and not first as an unhandled
55
+ // rejection.
56
+ preparation.catch(() => undefined);
57
+ // Rejections only, and not the ones the framework's own handlers already
58
+ // settle: a rejected LSP handler is caught by the connection, and a failed
59
+ // workspace walk rejects `workspaceInitialized`, which is reported below.
60
+ addEventListener('unhandledrejection', event => {
61
+ if (isOperationCancelled(event.reason)) {
62
+ // Dropped from the console too, as a Node listener's presence drops
63
+ // it there; a reported rejection keeps its console entry, the one
64
+ // place its stack stays navigable.
65
+ event.preventDefault();
66
+ }
67
+ else {
68
+ reportWorkerError('unhandled rejection', event.reason);
69
+ }
70
+ });
71
+ // One bootstrap per worker: the heads are composed against a single shared
72
+ // Langium tree, so a second would build a second tree over the same seed and
73
+ // hand out sessions that silently disagree about the workspace.
74
+ let bootstrapped = false;
75
+ // The bare global rather than `self.addEventListener`: only the former is
76
+ // typed against the dedicated worker's event map, where `message` carries a
77
+ // `MessageEvent`.
78
+ addEventListener('message', event => {
79
+ if (bootstrapped || !isWorkerBootstrapMessage(event.data)) {
80
+ return;
81
+ }
82
+ bootstrapped = true;
83
+ // `.catch` rather than try/catch: a rejected restore, a `start` that
84
+ // throws and a head whose `started` rejects arrive by the same route, and
85
+ // all have to reach the page.
86
+ onBootstrap(event.data.ports).catch((error) => reportWorkerError(WORKER_BOOTSTRAP_PHASE, error));
87
+ });
88
+ async function onBootstrap(ports) {
89
+ const value = await preparation;
90
+ const result = start(ports, value);
91
+ const { shared, started } = 'shared' in result ? result : { shared: result, started: undefined };
92
+ // Safe to await now: every head listens already, so nothing is lost.
93
+ await Promise.all(started ?? []);
94
+ const ready = { type: WORKER_READY_MESSAGE_TYPE };
95
+ postMessage(ready);
96
+ validateWorkspace(shared).catch((error) => reportWorkerError('workspace validation', error));
97
+ }
98
+ }
99
+ /**
100
+ * The trailing validating build, after `workspaceInitialized`: building before
101
+ * it would validate a partial document set. A cancelled or connection-gone
102
+ * initial build is routine, so it is built through; any other failure is the
103
+ * caller's to report.
104
+ */
105
+ async function validateWorkspace(shared) {
106
+ await shared.workspace.WorkspaceManager.workspaceInitialized.catch((error) => {
107
+ if (!isOperationCancelled(error) && !isConnectionGoneError(error)) {
108
+ throw error;
109
+ }
110
+ });
111
+ await shared.workspace.DocumentBuilder.build(shared.workspace.LangiumDocuments.all.toArray(), { validation: true });
112
+ }
113
+ //# sourceMappingURL=host-heads-in-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-heads-in-worker.js","sourceRoot":"","sources":["../../src/browser/host-heads-in-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EACJ,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EAI3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAEvE;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,KAAc;IAC5D,MAAM,MAAM,GAAuB,EAAE,IAAI,EAAE,yBAAyB,EAAE,KAAK,EAAE,OAAO,EAAE,sBAAsB,CAAC,KAAK,CAAC,EAAE,CAAC;IACtH,WAAW,CAAC,MAAM,CAAC,CAAC;AACvB,CAAC;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,iBAAiB,CAC9B,QAA4C,EAC5C,KAA4F;IAE5F,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,6EAA6E;IAC7E,0EAA0E;IAC1E,aAAa;IACb,WAAW,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAEnC,yEAAyE;IACzE,2EAA2E;IAC3E,0EAA0E;IAC1E,gBAAgB,CAAC,oBAAoB,EAAE,KAAK,CAAC,EAAE;QAC5C,IAAI,oBAAoB,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YACtC,oEAAoE;YACpE,kEAAkE;YAClE,mCAAmC;YACnC,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1B,CAAC;aAAM,CAAC;YACL,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAC1D,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,2EAA2E;IAC3E,6EAA6E;IAC7E,gEAAgE;IAChE,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,0EAA0E;IAC1E,4EAA4E;IAC5E,kBAAkB;IAClB,gBAAgB,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE;QACjC,IAAI,YAAY,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACzD,OAAO;QACV,CAAC;QACD,YAAY,GAAG,IAAI,CAAC;QACpB,qEAAqE;QACrE,0EAA0E;QAC1E,8BAA8B;QAC9B,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC,CAAC;IAC7G,CAAC,CAAC,CAAC;IAEH,KAAK,UAAU,WAAW,CAAC,KAAgB;QACxC,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;QAChC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACnC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;QACjG,qEAAqE;QACrE,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;QACjC,MAAM,KAAK,GAAuB,EAAE,IAAI,EAAE,yBAAyB,EAAE,CAAC;QACtE,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC,CAAC;IACzG,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,iBAAiB,CAAC,MAAmC;IACjE,MAAM,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACnF,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,CAAC;YACjE,MAAM,KAAK,CAAC;QACf,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AACvH,CAAC"}
@@ -6,5 +6,7 @@
6
6
  *
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
+ export * from './host-heads-in-worker.js';
9
10
  export * from './indexeddb-file-system-store.js';
11
+ export * from './start-language-server-in-worker.js';
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAMlF,cAAc,kCAAkC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAOlF,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,sCAAsC,CAAC"}
@@ -7,8 +7,11 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
  // Worker-only entry (`@hydranium/core/browser`). Compiled as its own project
10
- // against `lib.webworker`, so a worker global such as `indexedDB` resolves here
11
- // and nowhere else in the package: the portable `.` barrel must stay free of
12
- // DOM and worker globals, since it also loads in a plain Node consumer.
10
+ // against `lib.webworker`, so a worker global such as `indexedDB` or
11
+ // `postMessage` resolves here and nowhere else in the package: the portable
12
+ // `.` barrel must stay free of DOM and worker globals, since it also loads in a
13
+ // plain Node consumer.
14
+ export * from './host-heads-in-worker.js';
13
15
  export * from './indexeddb-file-system-store.js';
16
+ export * from './start-language-server-in-worker.js';
14
17
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,6EAA6E;AAC7E,gFAAgF;AAChF,6EAA6E;AAC7E,wEAAwE;AACxE,cAAc,kCAAkC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,6EAA6E;AAC7E,qEAAqE;AACrE,4EAA4E;AAC5E,gFAAgF;AAChF,uBAAuB;AACvB,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,sCAAsC,CAAC"}
@@ -0,0 +1,29 @@
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 LatencyCollector, type TransferredMessagePort } from '@hydranium/protocol';
10
+ import { type Connection } from 'vscode-languageserver/browser';
11
+ import type { ServerSharedServices } from '../langium/module.js';
12
+ export interface WorkerLanguageServerOptions {
13
+ /** Records every request the head answers. One collector serves every head, so a report covers them all. */
14
+ readonly latency?: LatencyCollector;
15
+ }
16
+ /**
17
+ * Start the LSP head on the port the page transferred for it, and return the
18
+ * shared tier `compose` built around its connection.
19
+ *
20
+ * `compose` is the adopter's services factory, called with the connection
21
+ * exactly as a Node entry calls it with a stdio one; the filesystem it spreads
22
+ * in must already be restored, since every read the head makes is synchronous
23
+ * and the transport drops a message that arrives before `startLanguageServer`
24
+ * listens. The page's end of the port closing ends only this head's
25
+ * connection, not the worker the other heads share, so the language client's
26
+ * documents are closed here, each reverting as its last close does.
27
+ */
28
+ export declare function startLanguageServerInWorker<TShared extends ServerSharedServices>(port: TransferredMessagePort, compose: (connection: Connection) => TShared, options?: WorkerLanguageServerOptions): TShared;
29
+ //# sourceMappingURL=start-language-server-in-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-language-server-in-worker.d.ts","sourceRoot":"","sources":["../../src/browser/start-language-server-in-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAA8B,KAAK,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACrH,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,+BAA+B,CAAC;AACpG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAKjE,MAAM,WAAW,2BAA2B;IACzC,4GAA4G;IAC5G,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,SAAS,oBAAoB,EAC7E,IAAI,EAAE,sBAAsB,EAC5B,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,OAAO,EAC5C,OAAO,GAAE,2BAAgC,GACzC,OAAO,CAaT"}
@@ -0,0 +1,35 @@
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 { createMessagePortTransport } from '@hydranium/protocol';
10
+ import { createConnection, ProposedFeatures } from 'vscode-languageserver/browser';
11
+ import { withHydraniumLspFeatures } from '../lsp/connection-features.js';
12
+ import { lspLatencyOptions } from '../lsp/lsp-latency.js';
13
+ import { startLanguageServer } from '../lsp/start-language-server.js';
14
+ /**
15
+ * Start the LSP head on the port the page transferred for it, and return the
16
+ * shared tier `compose` built around its connection.
17
+ *
18
+ * `compose` is the adopter's services factory, called with the connection
19
+ * exactly as a Node entry calls it with a stdio one; the filesystem it spreads
20
+ * in must already be restored, since every read the head makes is synchronous
21
+ * and the transport drops a message that arrives before `startLanguageServer`
22
+ * listens. The page's end of the port closing ends only this head's
23
+ * connection, not the worker the other heads share, so the language client's
24
+ * documents are closed here, each reverting as its last close does.
25
+ */
26
+ export function startLanguageServerInWorker(port, compose, options = {}) {
27
+ // Both ends use `createMessagePortTransport`; see it for why.
28
+ const transport = createMessagePortTransport(port);
29
+ const connection = createConnection(withHydraniumLspFeatures(ProposedFeatures.all), transport.reader, transport.writer, lspLatencyOptions(options.latency));
30
+ const shared = compose(connection);
31
+ startLanguageServer(shared);
32
+ transport.reader.onClose(() => void shared.workspace.TextDocuments.closeLanguageClientDocuments());
33
+ return shared;
34
+ }
35
+ //# sourceMappingURL=start-language-server-in-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-language-server-in-worker.js","sourceRoot":"","sources":["../../src/browser/start-language-server-in-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,0BAA0B,EAAsD,MAAM,qBAAqB,CAAC;AACrH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAmB,MAAM,+BAA+B,CAAC;AAEpG,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAOtE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,2BAA2B,CACxC,IAA4B,EAC5B,OAA4C,EAC5C,OAAO,GAAgC,EAAE;IAEzC,8DAA8D;IAC9D,MAAM,SAAS,GAAG,0BAA0B,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,gBAAgB,CAChC,wBAAwB,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAC9C,SAAS,CAAC,MAAM,EAChB,SAAS,CAAC,MAAM,EAChB,iBAAiB,CAAC,OAAO,CAAC,OAAO,CAAC,CACpC,CAAC;IACF,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACnC,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,4BAA4B,EAAE,CAAC,CAAC;IACnG,OAAO,MAAM,CAAC;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydranium/core",
3
- "version": "1.0.0-next.298",
3
+ "version": "1.0.0-next.299",
4
4
  "description": "Foundational runtime of the hydranium framework: AST coordination layer (services, integrity, AST extension, serialization, multi-client documents) plus the LSP textual head at the /lsp subpath. Consumed by protocol-head packages (@hydranium/data-server, @hydranium/glsp-server).",
5
5
  "keywords": [
6
6
  "ast",
@@ -87,8 +87,8 @@
87
87
  "diff": "^5.2.0"
88
88
  },
89
89
  "devDependencies": {
90
- "@hydranium/langium": "1.0.0-next.298",
91
- "@hydranium/protocol": "1.0.0-next.298",
90
+ "@hydranium/langium": "1.0.0-next.299",
91
+ "@hydranium/protocol": "1.0.0-next.299",
92
92
  "@playwright/test": "^1.40.0",
93
93
  "@types/diff": "^5.2.0",
94
94
  "fake-indexeddb": "^6.2.5",
@@ -101,8 +101,8 @@
101
101
  "vscode-languageserver-types": "^3.17.5"
102
102
  },
103
103
  "peerDependencies": {
104
- "@hydranium/langium": "1.0.0-next.298",
105
- "@hydranium/protocol": "1.0.0-next.298",
104
+ "@hydranium/langium": "1.0.0-next.299",
105
+ "@hydranium/protocol": "1.0.0-next.299",
106
106
  "@playwright/test": "^1.40.0",
107
107
  "vscode-jsonrpc": "^9.0.0",
108
108
  "vscode-languageserver": "~10.0.1",
@@ -0,0 +1,142 @@
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
+ describeErrorWithStack,
12
+ isWorkerBootstrapMessage,
13
+ WORKER_BOOTSTRAP_PHASE,
14
+ WORKER_ERROR_MESSAGE_TYPE,
15
+ WORKER_READY_MESSAGE_TYPE,
16
+ type HeadPorts,
17
+ type WorkerErrorMessage,
18
+ type WorkerReadyMessage
19
+ } from '@hydranium/protocol';
20
+ import { isOperationCancelled } from '@hydranium/langium';
21
+ import type { ServerSharedServicesMinimal } from '../langium/shared-services.js';
22
+ import { isConnectionGoneError } from '../util/connection-liveness.js';
23
+
24
+ /**
25
+ * Report a failure to the page on the worker global, the channel no head is
26
+ * allowed to bind, which is what keeps it free for this. A report under
27
+ * {@link WORKER_BOOTSTRAP_PHASE} ends the page's heads; any other phase is
28
+ * shown there and the heads stay. The stack goes with it: a worker has no log
29
+ * anyone reads, so the page is the one place the stack can be seen.
30
+ */
31
+ export function reportWorkerError(phase: string, error: unknown): void {
32
+ const report: WorkerErrorMessage = { type: WORKER_ERROR_MESSAGE_TYPE, phase, message: describeErrorWithStack(error) };
33
+ postMessage(report);
34
+ }
35
+
36
+ /** What the host's `start` hands back: the shared tier, and the readiness of any head that reports it later. */
37
+ export interface StartedHeads {
38
+ readonly shared: ServerSharedServicesMinimal;
39
+ /**
40
+ * The `started` promise of every head that reports a failed start through
41
+ * one rather than by throwing, such as the GLSP head's. The ready message
42
+ * waits for them, and a rejection is a failed bootstrap.
43
+ */
44
+ readonly started?: ReadonlyArray<Promise<unknown>>;
45
+ }
46
+
47
+ /**
48
+ * Answer the page's bootstrap by starting the heads on its ports, once per
49
+ * worker, and tell the page when they are up and when something fails.
50
+ *
51
+ * `start` is synchronous by type, and that is the ordering contract: every
52
+ * head's transport starts its port as it is built but drops what arrives
53
+ * before the head listens, so an `await` between building a head and
54
+ * listening loses the page's first request with no error. Everything
55
+ * asynchronous, the filesystem's restore above all, is `prepared`, which is
56
+ * awaited before `start` is called. It may be a plain value where nothing has
57
+ * to be awaited.
58
+ *
59
+ * Once `start` has returned and every `started` it handed back has resolved,
60
+ * the ready message goes out, so a page never hears of heads that failed to
61
+ * come up, and an uncaught error from then on is a report rather than the end
62
+ * of the heads. Then every document is built with validation once the
63
+ * workspace has initialised: Langium's initial build validates nothing, so a
64
+ * page that only opens its ports would see a server that indexed the
65
+ * workspace and says nothing. An initial build that a write during startup
66
+ * cancelled is built the same, since every document exists by then.
67
+ *
68
+ * Call it once: a second call answers the same bootstrap with a second set of
69
+ * heads on the same ports and reports every rejection twice. The worker's
70
+ * `unhandledrejection` listener is this function's; it drops Langium's
71
+ * `OperationCancelled` marker, which a build preempted by a write rejects
72
+ * with, as the Node host does. An uncaught throw needs no listener, since it
73
+ * reaches the page as the `Worker`'s own `error` event.
74
+ */
75
+ export function hostHeadsInWorker<TPrepared>(
76
+ prepared: TPrepared | PromiseLike<TPrepared>,
77
+ start: (ports: HeadPorts, prepared: TPrepared) => ServerSharedServicesMinimal | StartedHeads
78
+ ): void {
79
+ const preparation = Promise.resolve(prepared);
80
+ // Handled here, so a restore that fails before the bootstrap arrives reaches
81
+ // the page once, under the bootstrap phase, and not first as an unhandled
82
+ // rejection.
83
+ preparation.catch(() => undefined);
84
+
85
+ // Rejections only, and not the ones the framework's own handlers already
86
+ // settle: a rejected LSP handler is caught by the connection, and a failed
87
+ // workspace walk rejects `workspaceInitialized`, which is reported below.
88
+ addEventListener('unhandledrejection', event => {
89
+ if (isOperationCancelled(event.reason)) {
90
+ // Dropped from the console too, as a Node listener's presence drops
91
+ // it there; a reported rejection keeps its console entry, the one
92
+ // place its stack stays navigable.
93
+ event.preventDefault();
94
+ } else {
95
+ reportWorkerError('unhandled rejection', event.reason);
96
+ }
97
+ });
98
+
99
+ // One bootstrap per worker: the heads are composed against a single shared
100
+ // Langium tree, so a second would build a second tree over the same seed and
101
+ // hand out sessions that silently disagree about the workspace.
102
+ let bootstrapped = false;
103
+ // The bare global rather than `self.addEventListener`: only the former is
104
+ // typed against the dedicated worker's event map, where `message` carries a
105
+ // `MessageEvent`.
106
+ addEventListener('message', event => {
107
+ if (bootstrapped || !isWorkerBootstrapMessage(event.data)) {
108
+ return;
109
+ }
110
+ bootstrapped = true;
111
+ // `.catch` rather than try/catch: a rejected restore, a `start` that
112
+ // throws and a head whose `started` rejects arrive by the same route, and
113
+ // all have to reach the page.
114
+ onBootstrap(event.data.ports).catch((error: unknown) => reportWorkerError(WORKER_BOOTSTRAP_PHASE, error));
115
+ });
116
+
117
+ async function onBootstrap(ports: HeadPorts): Promise<void> {
118
+ const value = await preparation;
119
+ const result = start(ports, value);
120
+ const { shared, started } = 'shared' in result ? result : { shared: result, started: undefined };
121
+ // Safe to await now: every head listens already, so nothing is lost.
122
+ await Promise.all(started ?? []);
123
+ const ready: WorkerReadyMessage = { type: WORKER_READY_MESSAGE_TYPE };
124
+ postMessage(ready);
125
+ validateWorkspace(shared).catch((error: unknown) => reportWorkerError('workspace validation', error));
126
+ }
127
+ }
128
+
129
+ /**
130
+ * The trailing validating build, after `workspaceInitialized`: building before
131
+ * it would validate a partial document set. A cancelled or connection-gone
132
+ * initial build is routine, so it is built through; any other failure is the
133
+ * caller's to report.
134
+ */
135
+ async function validateWorkspace(shared: ServerSharedServicesMinimal): Promise<void> {
136
+ await shared.workspace.WorkspaceManager.workspaceInitialized.catch((error: unknown) => {
137
+ if (!isOperationCancelled(error) && !isConnectionGoneError(error)) {
138
+ throw error;
139
+ }
140
+ });
141
+ await shared.workspace.DocumentBuilder.build(shared.workspace.LangiumDocuments.all.toArray(), { validation: true });
142
+ }
@@ -8,7 +8,10 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  // Worker-only entry (`@hydranium/core/browser`). Compiled as its own project
11
- // against `lib.webworker`, so a worker global such as `indexedDB` resolves here
12
- // and nowhere else in the package: the portable `.` barrel must stay free of
13
- // DOM and worker globals, since it also loads in a plain Node consumer.
11
+ // against `lib.webworker`, so a worker global such as `indexedDB` or
12
+ // `postMessage` resolves here and nowhere else in the package: the portable
13
+ // `.` barrel must stay free of DOM and worker globals, since it also loads in a
14
+ // plain Node consumer.
15
+ export * from './host-heads-in-worker.js';
14
16
  export * from './indexeddb-file-system-store.js';
17
+ export * from './start-language-server-in-worker.js';
@@ -0,0 +1,51 @@
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 { createMessagePortTransport, type LatencyCollector, type TransferredMessagePort } from '@hydranium/protocol';
11
+ import { createConnection, ProposedFeatures, type Connection } from 'vscode-languageserver/browser';
12
+ import type { ServerSharedServices } from '../langium/module.js';
13
+ import { withHydraniumLspFeatures } from '../lsp/connection-features.js';
14
+ import { lspLatencyOptions } from '../lsp/lsp-latency.js';
15
+ import { startLanguageServer } from '../lsp/start-language-server.js';
16
+
17
+ export interface WorkerLanguageServerOptions {
18
+ /** Records every request the head answers. One collector serves every head, so a report covers them all. */
19
+ readonly latency?: LatencyCollector;
20
+ }
21
+
22
+ /**
23
+ * Start the LSP head on the port the page transferred for it, and return the
24
+ * shared tier `compose` built around its connection.
25
+ *
26
+ * `compose` is the adopter's services factory, called with the connection
27
+ * exactly as a Node entry calls it with a stdio one; the filesystem it spreads
28
+ * in must already be restored, since every read the head makes is synchronous
29
+ * and the transport drops a message that arrives before `startLanguageServer`
30
+ * listens. The page's end of the port closing ends only this head's
31
+ * connection, not the worker the other heads share, so the language client's
32
+ * documents are closed here, each reverting as its last close does.
33
+ */
34
+ export function startLanguageServerInWorker<TShared extends ServerSharedServices>(
35
+ port: TransferredMessagePort,
36
+ compose: (connection: Connection) => TShared,
37
+ options: WorkerLanguageServerOptions = {}
38
+ ): TShared {
39
+ // Both ends use `createMessagePortTransport`; see it for why.
40
+ const transport = createMessagePortTransport(port);
41
+ const connection = createConnection(
42
+ withHydraniumLspFeatures(ProposedFeatures.all),
43
+ transport.reader,
44
+ transport.writer,
45
+ lspLatencyOptions(options.latency)
46
+ );
47
+ const shared = compose(connection);
48
+ startLanguageServer(shared);
49
+ transport.reader.onClose(() => void shared.workspace.TextDocuments.closeLanguageClientDocuments());
50
+ return shared;
51
+ }