@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 +8 -1
- package/lib/browser/host-heads-in-worker.d.ts +58 -0
- package/lib/browser/host-heads-in-worker.d.ts.map +1 -0
- package/lib/browser/host-heads-in-worker.js +113 -0
- package/lib/browser/host-heads-in-worker.js.map +1 -0
- package/lib/browser/index.d.ts +2 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +6 -3
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/start-language-server-in-worker.d.ts +29 -0
- package/lib/browser/start-language-server-in-worker.d.ts.map +1 -0
- package/lib/browser/start-language-server-in-worker.js +35 -0
- package/lib/browser/start-language-server-in-worker.js.map +1 -0
- package/package.json +5 -5
- package/src/browser/host-heads-in-worker.ts +142 -0
- package/src/browser/index.ts +6 -3
- package/src/browser/start-language-server-in-worker.ts +51 -0
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` |
|
|
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":"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"}
|
package/lib/browser/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/lib/browser/index.js
CHANGED
|
@@ -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`
|
|
11
|
-
// and nowhere else in the package: the portable
|
|
12
|
-
// DOM and worker globals, since it also loads in a
|
|
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
|
package/lib/browser/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,6EAA6E;AAC7E,gFAAgF;AAChF,
|
|
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.
|
|
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.
|
|
91
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
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.
|
|
105
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
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
|
+
}
|
package/src/browser/index.ts
CHANGED
|
@@ -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`
|
|
12
|
-
// and nowhere else in the package: the portable
|
|
13
|
-
// DOM and worker globals, since it also loads in a
|
|
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
|
+
}
|