@hydranium/glsp-client-theia 1.0.0-next.32 → 1.0.0-next.321
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 +60 -98
- package/lib/browser/client-contribution.d.ts +87 -25
- package/lib/browser/client-contribution.d.ts.map +1 -1
- package/lib/browser/client-contribution.js +233 -53
- package/lib/browser/client-contribution.js.map +1 -1
- package/lib/browser/diagram-only-marker-manager.d.ts.map +1 -1
- package/lib/browser/diagram-only-marker-manager.js.map +1 -1
- package/lib/browser/diagram-widget.d.ts +43 -25
- package/lib/browser/diagram-widget.d.ts.map +1 -1
- package/lib/browser/diagram-widget.js +76 -26
- package/lib/browser/diagram-widget.js.map +1 -1
- package/lib/browser/glsp-client-theia-module.d.ts +12 -5
- package/lib/browser/glsp-client-theia-module.d.ts.map +1 -1
- package/lib/browser/glsp-client-theia-module.js +27 -12
- package/lib/browser/glsp-client-theia-module.js.map +1 -1
- package/lib/browser/glsp-diagram-manager.d.ts +40 -1
- package/lib/browser/glsp-diagram-manager.d.ts.map +1 -1
- package/lib/browser/glsp-diagram-manager.js +100 -0
- package/lib/browser/glsp-diagram-manager.js.map +1 -1
- package/lib/browser/glsp-message-service.d.ts.map +1 -1
- package/lib/browser/glsp-message-service.js.map +1 -1
- package/lib/browser/glsp-saveable.d.ts +67 -0
- package/lib/browser/glsp-saveable.d.ts.map +1 -0
- package/lib/browser/glsp-saveable.js +110 -0
- package/lib/browser/glsp-saveable.js.map +1 -0
- package/lib/browser/glsp-theia-frontend-module.d.ts.map +1 -1
- package/lib/browser/glsp-theia-frontend-module.js +4 -1
- package/lib/browser/glsp-theia-frontend-module.js.map +1 -1
- package/lib/browser/hydranium-glsp-diagram-configuration.d.ts +3 -1
- package/lib/browser/hydranium-glsp-diagram-configuration.d.ts.map +1 -1
- package/lib/browser/hydranium-glsp-diagram-configuration.js +3 -1
- package/lib/browser/hydranium-glsp-diagram-configuration.js.map +1 -1
- package/lib/browser/index.d.ts +4 -3
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +7 -5
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/theia-diagram-loader.d.ts +25 -0
- package/lib/browser/theia-diagram-loader.d.ts.map +1 -0
- package/lib/browser/theia-diagram-loader.js +42 -0
- package/lib/browser/theia-diagram-loader.js.map +1 -0
- package/lib/browser/window-session.d.ts +57 -0
- package/lib/browser/window-session.d.ts.map +1 -0
- package/lib/browser/window-session.js +102 -0
- package/lib/browser/window-session.js.map +1 -0
- package/lib/node/connection-container-module.js.map +1 -1
- package/lib/node/glsp-server-connection-handler.d.ts +11 -20
- package/lib/node/glsp-server-connection-handler.d.ts.map +1 -1
- package/lib/node/glsp-server-connection-handler.js +12 -15
- package/lib/node/glsp-server-connection-handler.js.map +1 -1
- package/package.json +37 -46
- package/src/browser/client-contribution.ts +254 -55
- package/src/browser/diagram-widget.ts +80 -28
- package/src/browser/glsp-client-theia-module.ts +34 -13
- package/src/browser/glsp-diagram-manager.ts +112 -3
- package/src/browser/glsp-saveable.ts +111 -0
- package/src/browser/glsp-theia-frontend-module.ts +4 -1
- package/src/browser/hydranium-glsp-diagram-configuration.ts +3 -1
- package/src/browser/index.ts +12 -5
- package/src/browser/theia-diagram-loader.ts +31 -0
- package/src/browser/window-session.ts +114 -0
- package/src/node/glsp-server-connection-handler.ts +12 -24
- package/style/diagram-loading.css +2 -5
- package/lib/browser/action-dispatcher.d.ts +0 -85
- package/lib/browser/action-dispatcher.d.ts.map +0 -1
- package/lib/browser/action-dispatcher.js +0 -238
- package/lib/browser/action-dispatcher.js.map +0 -1
- package/lib/browser/diagram-loader.d.ts +0 -123
- package/lib/browser/diagram-loader.d.ts.map +0 -1
- package/lib/browser/diagram-loader.js +0 -153
- package/lib/browser/diagram-loader.js.map +0 -1
- package/lib/browser/hidden-bounds-updater.d.ts +0 -122
- package/lib/browser/hidden-bounds-updater.d.ts.map +0 -1
- package/lib/browser/hidden-bounds-updater.js +0 -207
- package/lib/browser/hidden-bounds-updater.js.map +0 -1
- package/lib/testing/bind-recorder.d.ts +0 -67
- package/lib/testing/bind-recorder.d.ts.map +0 -1
- package/lib/testing/bind-recorder.js +0 -72
- package/lib/testing/bind-recorder.js.map +0 -1
- package/lib/testing/index.d.ts +0 -10
- package/lib/testing/index.d.ts.map +0 -1
- package/lib/testing/index.js +0 -30
- package/lib/testing/index.js.map +0 -1
- package/src/browser/action-dispatcher.ts +0 -257
- package/src/browser/diagram-loader.ts +0 -173
- package/src/browser/hidden-bounds-updater.ts +0 -212
- package/src/testing/bind-recorder.ts +0 -126
- package/src/testing/index.ts +0 -15
|
@@ -22,38 +22,35 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.GlspServerConnectionHandler = void 0;
|
|
24
24
|
const common_1 = require("@eclipse-glsp/theia-integration/lib/common");
|
|
25
|
-
const node_1 = require("@
|
|
26
|
-
const node_2 = require("@hydranium/client-theia/lib/node");
|
|
25
|
+
const node_1 = require("@hydranium/client-theia/node");
|
|
27
26
|
const inversify_1 = require("@theia/core/shared/inversify");
|
|
28
27
|
/**
|
|
29
28
|
* Bridges a Theia browser-frontend channel to a GLSP server's TCP socket.
|
|
30
29
|
*
|
|
31
|
-
* The port
|
|
32
|
-
* on the cross-head
|
|
33
|
-
*
|
|
30
|
+
* The port discovery, the buffer-and-replay race fix, the connect orchestration
|
|
31
|
+
* and the byte forwarder live on the cross-head
|
|
32
|
+
* {@link AbstractSocketForwardingConnectionHandler} base; this subclass supplies
|
|
33
|
+
* only the GLSP specifics — the per-language servicePath
|
|
34
34
|
* (`GLSPContribution.servicePath + '/' + languageContributionId`), the port
|
|
35
|
-
* command, the log labels
|
|
36
|
-
*
|
|
37
|
-
* language ids or command names subclass with a one-line `super({...})` call.
|
|
38
|
-
* Sibling of `@hydranium/data-client-theia`'s `DataServerConnectionHandler`,
|
|
39
|
-
* which subclasses the same base with its own GLSP-free forwarder.
|
|
35
|
+
* command, the log labels. Adopters with different language ids or command
|
|
36
|
+
* names subclass with a one-line `super({...})` call.
|
|
40
37
|
*/
|
|
41
|
-
let GlspServerConnectionHandler = class GlspServerConnectionHandler extends
|
|
38
|
+
let GlspServerConnectionHandler = class GlspServerConnectionHandler extends node_1.AbstractSocketForwardingConnectionHandler {
|
|
42
39
|
constructor(options) {
|
|
43
40
|
super({
|
|
44
41
|
path: common_1.GLSPContribution.servicePath + '/' + options.languageContributionId,
|
|
45
42
|
portCommand: options.portCommand,
|
|
46
43
|
logComponent: 'GLSP',
|
|
47
|
-
serverName: options.serverName ?? '
|
|
44
|
+
serverName: options.serverName ?? 'Diagram Server',
|
|
48
45
|
findPortTimeout: options.findPortTimeout,
|
|
49
46
|
findPortAttempts: options.findPortAttempts,
|
|
50
47
|
connectTimeoutMs: options.connectTimeoutMs,
|
|
51
48
|
onSocketCreated: options.onSocketCreated
|
|
52
49
|
});
|
|
53
50
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
51
|
+
/** Logged only: the frontend contribution reports the failed start, and a
|
|
52
|
+
* second notification would duplicate it. */
|
|
53
|
+
reportConnectFailure() { }
|
|
57
54
|
};
|
|
58
55
|
exports.GlspServerConnectionHandler = GlspServerConnectionHandler;
|
|
59
56
|
exports.GlspServerConnectionHandler = GlspServerConnectionHandler = __decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glsp-server-connection-handler.js","sourceRoot":"","sources":["../../src/node/glsp-server-connection-handler.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;AAElF,uEAA8E;AAC9E,
|
|
1
|
+
{"version":3,"file":"glsp-server-connection-handler.js","sourceRoot":"","sources":["../../src/node/glsp-server-connection-handler.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;AAElF,uEAA8E;AAC9E,uDAAyF;AACzF,4DAAqE;AA0BrE;;;;;;;;;;GAUG;AAEI,IAAM,2BAA2B,GAAjC,MAAM,2BAA4B,SAAQ,gDAAyC;IACvF,YAAyB,OAA2C;QACjE,KAAK,CAAC;YACH,IAAI,EAAE,yBAAgB,CAAC,WAAW,GAAG,GAAG,GAAG,OAAO,CAAC,sBAAsB;YACzE,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,YAAY,EAAE,MAAM;YACpB,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,gBAAgB;YAClD,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;YAC1C,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;YAC1C,eAAe,EAAE,OAAO,CAAC,eAAe;SAC1C,CAAC,CAAC;IACN,CAAC;IAED;kDAC8C;IAC3B,oBAAoB,KAAU,CAAC;CACpD,CAAA;;sCAjBY,2BAA2B;IADvC,IAAA,sBAAU,GAAE;IAEG,WAAA,IAAA,qBAAS,GAAE,CAAA;;GADd,2BAA2B,CAiBvC"}
|
package/package.json
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hydranium/glsp-client-theia",
|
|
3
|
-
"version": "1.0.0-next.
|
|
4
|
-
"description": "Theia-coupled client primitives for GLSP heads built on @hydranium/glsp-server. Provides the
|
|
3
|
+
"version": "1.0.0-next.321",
|
|
4
|
+
"description": "Theia-coupled client primitives for GLSP heads built on @hydranium/glsp-server. Provides the diagram widget, manager, client contribution and connection-handler bases that Theia-based adopters extend, and binds @hydranium/glsp-client's host-neutral classes through the Output channel; companion of @hydranium/glsp-server.",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"diagram",
|
|
7
|
+
"glsp",
|
|
6
8
|
"hydranium",
|
|
7
9
|
"langium",
|
|
8
10
|
"language-server",
|
|
9
11
|
"lsp",
|
|
10
|
-
"glsp",
|
|
11
|
-
"diagram",
|
|
12
12
|
"theia"
|
|
13
13
|
],
|
|
14
14
|
"homepage": "https://github.com/eclipse-emfcloud/hydranium/tree/main/packages/glsp-client-theia",
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://github.com/eclipse-emfcloud/hydranium/issues"
|
|
17
17
|
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Hydranium Team"
|
|
21
|
+
},
|
|
18
22
|
"repository": {
|
|
19
23
|
"type": "git",
|
|
20
24
|
"url": "https://github.com/eclipse-emfcloud/hydranium.git",
|
|
21
25
|
"directory": "packages/glsp-client-theia"
|
|
22
26
|
},
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
+
"files": [
|
|
28
|
+
"lib",
|
|
29
|
+
"src",
|
|
30
|
+
"style",
|
|
31
|
+
"!lib/**/*.tsbuildinfo"
|
|
32
|
+
],
|
|
27
33
|
"sideEffects": [
|
|
28
34
|
"**/*.css"
|
|
29
35
|
],
|
|
36
|
+
"main": "lib/index.js",
|
|
37
|
+
"types": "lib/index.d.ts",
|
|
30
38
|
"exports": {
|
|
31
39
|
".": {
|
|
32
40
|
"types": "./lib/index.d.ts",
|
|
@@ -40,36 +48,15 @@
|
|
|
40
48
|
"types": "./lib/node/index.d.ts",
|
|
41
49
|
"default": "./lib/node/index.js"
|
|
42
50
|
},
|
|
43
|
-
"./testing": {
|
|
44
|
-
"types": "./lib/testing/index.d.ts",
|
|
45
|
-
"default": "./lib/testing/index.js"
|
|
46
|
-
},
|
|
47
|
-
"./lib/browser": {
|
|
48
|
-
"types": "./lib/browser/index.d.ts",
|
|
49
|
-
"default": "./lib/browser/index.js"
|
|
50
|
-
},
|
|
51
|
-
"./lib/node": {
|
|
52
|
-
"types": "./lib/node/index.d.ts",
|
|
53
|
-
"default": "./lib/node/index.js"
|
|
54
|
-
},
|
|
55
|
-
"./lib/testing": {
|
|
56
|
-
"types": "./lib/testing/index.d.ts",
|
|
57
|
-
"default": "./lib/testing/index.js"
|
|
58
|
-
},
|
|
59
51
|
"./style/diagram-loading.css": "./style/diagram-loading.css"
|
|
60
52
|
},
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"lib",
|
|
65
|
-
"src",
|
|
66
|
-
"style",
|
|
67
|
-
"!lib/**/*.tsbuildinfo"
|
|
68
|
-
],
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
69
56
|
"scripts": {
|
|
70
57
|
"build": "tsc -b",
|
|
71
58
|
"clean": "rimraf lib tsconfig.tsbuildinfo",
|
|
72
|
-
"lint": "
|
|
59
|
+
"lint": "oxlint --format stylish --config ../../oxlint.config.cjs src test --max-warnings 0",
|
|
73
60
|
"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);}\"",
|
|
74
61
|
"test": "npm run typecheck:test && vitest run",
|
|
75
62
|
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
@@ -77,38 +64,42 @@
|
|
|
77
64
|
},
|
|
78
65
|
"devDependencies": {
|
|
79
66
|
"@eclipse-glsp/client": "2.7.0",
|
|
67
|
+
"@eclipse-glsp/server": "2.7.0",
|
|
80
68
|
"@eclipse-glsp/theia-integration": "2.7.0",
|
|
81
|
-
"@hydranium/client-theia": "1.0.0-next.
|
|
82
|
-
"@hydranium/
|
|
69
|
+
"@hydranium/client-theia": "1.0.0-next.321",
|
|
70
|
+
"@hydranium/core": "1.0.0-next.321",
|
|
71
|
+
"@hydranium/glsp-client": "1.0.0-next.321",
|
|
72
|
+
"@hydranium/glsp-server": "1.0.0-next.321",
|
|
73
|
+
"@hydranium/protocol": "1.0.0-next.321",
|
|
83
74
|
"@theia/core": "^1.70.0",
|
|
84
|
-
"@theia/output": "^1.70.0",
|
|
85
75
|
"@theia/process": "^1.70.0",
|
|
86
76
|
"@theia/workspace": "^1.70.0",
|
|
87
77
|
"@types/node": "^22.0.0",
|
|
88
78
|
"inversify": "6.2.2",
|
|
79
|
+
"reflect-metadata": "0.2.2",
|
|
89
80
|
"rimraf": "^5.0.0",
|
|
90
81
|
"snabbdom": "3.5.1",
|
|
91
|
-
"typescript": "^
|
|
82
|
+
"typescript": "^7.0.2",
|
|
83
|
+
"vscode-jsonrpc": "9.0.0"
|
|
92
84
|
},
|
|
93
85
|
"peerDependencies": {
|
|
94
86
|
"@eclipse-glsp/client": "^2.6.0",
|
|
95
87
|
"@eclipse-glsp/theia-integration": "^2.6.0",
|
|
96
|
-
"@hydranium/client-theia": "1.0.0-next.
|
|
97
|
-
"@hydranium/
|
|
88
|
+
"@hydranium/client-theia": "1.0.0-next.321",
|
|
89
|
+
"@hydranium/glsp-client": "1.0.0-next.321",
|
|
90
|
+
"@hydranium/protocol": "1.0.0-next.321",
|
|
98
91
|
"@theia/core": "^1.70.0",
|
|
99
|
-
"@theia/output": "^1.70.0",
|
|
100
92
|
"@theia/process": "^1.70.0",
|
|
101
93
|
"@theia/workspace": "^1.70.0",
|
|
102
94
|
"inversify": "^6.0.0",
|
|
103
|
-
"snabbdom": "^3.5.1"
|
|
95
|
+
"snabbdom": "^3.5.1",
|
|
96
|
+
"vscode-jsonrpc": "^9.0.0"
|
|
104
97
|
},
|
|
105
98
|
"engines": {
|
|
106
99
|
"node": ">=22.13"
|
|
107
100
|
},
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"//exports": "The stylesheet key is an ASSET entry, not a module subpath, and it is spelled as the exact filename rather than `./style/*` so nothing becomes reachable automatically — the same no-wildcard property every other key here has. It exists because the widget's own import is package-RELATIVE, which `exports` does not gate, so the map's silence broke nobody inside the package while blocking every adopter who wants to `@import` the sheet, re-bundle it or override it by specifier; that failure reads as \"not exported under the conditions\" rather than as a missing entry. It has NO `./lib/style/...` twin, and must not grow one: the twin rule exists so a bare subpath is reachable from a node10 MODULE resolver, `tsc` emits no assets so there is no `lib/style/` to point at, and a bundler — which is what resolves a stylesheet — reads the map. `check:exports` carves out exactly this shape and asserts instead that the target file exists and is covered by `files`, which is the pair of mistakes an asset key can actually make.",
|
|
112
|
-
"//prepack": "The publish guard, and it deliberately is NOT a `prepare`: npm runs a workspace `prepare` BEFORE the root `postinstall` that applies patches/vscode-jsonrpc+9.0.1.patch, so building there fails on a cold clone and npm rolls the entire install back. `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. It FAILS rather than rebuilds, because the rebuild is exactly the part that ordering defeats. 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.",
|
|
101
|
+
"//exports": "The stylesheet key is an ASSET entry, not a module subpath, and it is spelled as the exact filename rather than `./style/*` so nothing becomes reachable automatically — the same no-wildcard property every other key here has. It exists because the widget's own import is package-RELATIVE, which `exports` does not gate, so the map's silence broke nobody inside the package while blocking every adopter who wants to `@import` the sheet, re-bundle it or override it by specifier; that failure reads as \"not exported under the conditions\" rather than as a missing entry. `tsc` emits no assets, so no build step produces the target; `check:exports` asserts that the file exists and is covered by `files`, which is the pair of mistakes an asset key can actually make.",
|
|
102
|
+
"//peerDependencies": "`vscode-jsonrpc` is a peer because the client loads its `browser` entry for one effect: GLSP's Theia channel connection requires the top-level copy's root entry, which installs no runtime layer, so without that entry the frontend receives no message. A peer resolves to that same top-level copy. The caret matches the other framework packages.",
|
|
103
|
+
"//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.",
|
|
113
104
|
"//sideEffects": "A stylesheet IS the side effect: `style/diagram-loading.css` is pulled in by a bare import in `browser/diagram-widget.js`, and a CSS module exports nothing, so under a blanket `false` a bundler drops the import as unused and the diagram loading overlay renders unstyled. Nothing fails at build time. The glob rather than the importing module, so a second stylesheet is covered without another edit; the widget itself is reached through its exported class and needs no flag."
|
|
114
105
|
}
|
|
@@ -7,94 +7,293 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { type GLSPClient } from '@eclipse-glsp/client';
|
|
10
|
+
import { ClientState, type GLSPClient, type InitializeResult } from '@eclipse-glsp/client';
|
|
11
11
|
import { BaseGLSPClientContribution } from '@eclipse-glsp/theia-integration';
|
|
12
|
+
import { createChannelConnection, GLSPContribution } from '@eclipse-glsp/theia-integration/lib/common';
|
|
13
|
+
import { ChannelLogger, ConnectionReporter, type ConnectionTarget } from '@hydranium/client-theia/browser';
|
|
14
|
+
import { Clock } from '@hydranium/client-theia/common';
|
|
15
|
+
import { HydraniumGlspClient } from '@hydranium/glsp-client';
|
|
16
|
+
import { sendByMethodName, describeError, type Stopwatch, SystemClock } from '@hydranium/protocol';
|
|
17
|
+
import { type Channel, Disposable, Emitter, Event, nls } from '@theia/core';
|
|
12
18
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
13
|
-
import { inject, injectable, unmanaged } from '@theia/core/shared/inversify';
|
|
14
|
-
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
|
19
|
+
import { inject, injectable, optional, unmanaged } from '@theia/core/shared/inversify';
|
|
15
20
|
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
21
|
+
// GLSP's channel connection uses the top-level `vscode-jsonrpc`'s root entry, which
|
|
22
|
+
// installs no runtime layer; without the browser entry, no message is received.
|
|
23
|
+
import 'vscode-jsonrpc/browser';
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* "the GLSP server is accepting client connections". */
|
|
25
|
+
export const DEFAULT_GLSP_CLIENT_STARTUP_TIMEOUT_MS = 30_000;
|
|
26
|
+
|
|
27
|
+
/** Options for `HydraniumGlspClientContribution`. */
|
|
21
28
|
export interface ClientContributionOptions {
|
|
22
29
|
readonly languageContributionId: string;
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
/** How long a start may take, from the workspace opening to the server
|
|
31
|
+
* answering `initializeServer`, before it fails; `0` or less waits for good. */
|
|
32
|
+
readonly startupTimeoutMs?: number;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/**
|
|
28
36
|
* Theia GLSP client contribution for adopters whose GLSP server runs in a
|
|
29
37
|
* sideloaded VS Code extension process (or any other deferred-start setup).
|
|
30
|
-
* The behaviours adopters reliably need are lifted:
|
|
31
38
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* "connecting" progress spinner) from firing prematurely.
|
|
39
|
+
* The client starts once a workspace is open and sends its first request at
|
|
40
|
+
* once; the backend handler holds it until its socket to the server is up. A
|
|
41
|
+
* start that fails, and a connection lost after one succeeded, start a fresh
|
|
42
|
+
* client after a backoff, for as long as the contribution lives. Each start is
|
|
43
|
+
* reported through the {@link ConnectionReporter}, and announced through
|
|
44
|
+
* {@link onDidStartClient} and {@link onDidLoseClient}.
|
|
39
45
|
*
|
|
40
46
|
* A `GLSPClient` override filtering inbound messages by clientId is
|
|
41
47
|
* deliberately NOT provided: upstream's `BaseJsonrpcGLSPClient.onActionMessage`
|
|
42
|
-
* already filters by clientId
|
|
43
|
-
* `BaseGLSPClientContribution.createGLSPClient` is correct as-is.
|
|
48
|
+
* already filters by clientId.
|
|
44
49
|
*/
|
|
45
50
|
@injectable()
|
|
46
51
|
export class HydraniumGlspClientContribution extends BaseGLSPClientContribution {
|
|
47
|
-
@inject(OutputChannelManager) protected outputChannelManager!: OutputChannelManager;
|
|
48
52
|
@inject(WorkspaceService) protected readonly workspaceService!: WorkspaceService;
|
|
53
|
+
@inject(ConnectionReporter) protected readonly connectionReporter!: ConnectionReporter;
|
|
54
|
+
@inject(ChannelLogger) protected readonly logger!: ChannelLogger;
|
|
55
|
+
/** Times the restart escalation reset: the container's {@link Clock}, or a `SystemClock`. */
|
|
56
|
+
@inject(Clock) @optional() protected readonly clock: Clock = new SystemClock();
|
|
49
57
|
|
|
50
58
|
readonly id: string;
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
|
|
60
|
+
/** Delay before the restart following each consecutive failed start or lost
|
|
61
|
+
* client; the last repeats. */
|
|
62
|
+
protected readonly restartDelaysMs: readonly number[] = [1_000, 2_000, 4_000, 8_000];
|
|
63
|
+
/** How long a client must stay up before the restart delays start over, so
|
|
64
|
+
* a server that fails right after starting is not restarted at once. */
|
|
65
|
+
protected readonly restartEscalationResetMs: number = 30_000;
|
|
66
|
+
protected consecutiveFailures = 0;
|
|
67
|
+
protected restartTimer?: ReturnType<typeof setTimeout>;
|
|
68
|
+
/** Running since the current client started; cleared once a restart has taken it into account. */
|
|
69
|
+
protected sinceStart?: Stopwatch;
|
|
70
|
+
/** Clients started so far, which numbers them in the log: they share {@link id}. */
|
|
71
|
+
protected startedClients = 0;
|
|
72
|
+
/** The attempt waiting for a channel, if any. */
|
|
73
|
+
protected channelRequest?: Deferred<Channel>;
|
|
74
|
+
protected disposed = false;
|
|
75
|
+
protected readonly clientStartedEmitter = new Emitter<GLSPClient>();
|
|
76
|
+
protected readonly clientLostEmitter = new Emitter<void>();
|
|
77
|
+
|
|
78
|
+
/** Fires with each client once it has started and its server has answered. */
|
|
79
|
+
readonly onDidStartClient: Event<GLSPClient> = this.clientStartedEmitter.event;
|
|
80
|
+
/** Fires when the started client stops; {@link glspClient} already waits
|
|
81
|
+
* for its replacement then. */
|
|
82
|
+
readonly onDidLoseClient: Event<void> = this.clientLostEmitter.event;
|
|
53
83
|
|
|
54
84
|
constructor(@unmanaged() options: ClientContributionOptions) {
|
|
55
85
|
super();
|
|
56
86
|
this.id = options.languageContributionId;
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
this.glspClientStartupTimeout = options.startupTimeoutMs ?? DEFAULT_GLSP_CLIENT_STARTUP_TIMEOUT_MS;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The client every diagram load awaits; after a failed start, a fresh one. */
|
|
91
|
+
override get glspClient(): Promise<GLSPClient> {
|
|
92
|
+
return this.glspClientDeferred.state === 'rejected' ? this.restart() : this.glspClientDeferred.promise;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Start a fresh client over a fresh channel at once, if the last start failed. */
|
|
96
|
+
restart(): Promise<GLSPClient> {
|
|
97
|
+
if (!this.disposed && this.glspClientDeferred.state === 'rejected') {
|
|
98
|
+
this.replaceClient();
|
|
99
|
+
void this.activateClient();
|
|
100
|
+
}
|
|
101
|
+
return this.glspClientDeferred.promise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Point {@link glspClient} at a fresh client still to start, and tear down
|
|
105
|
+
* the old one's channel. */
|
|
106
|
+
protected replaceClient(): void {
|
|
107
|
+
clearTimeout(this.restartTimer);
|
|
108
|
+
// Replaced before the old channel goes, so the old client stopping is
|
|
109
|
+
// not taken for a loss of the new one.
|
|
110
|
+
this.glspClientDeferred = new Deferred<GLSPClient>();
|
|
111
|
+
this.toDispose.dispose();
|
|
112
|
+
// Upstream refuses to open a channel while the collection is disposed.
|
|
113
|
+
this.toDispose.push(Disposable.NULL);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Settles the promise current when it began, so a start a restart overtook
|
|
117
|
+
* cannot settle its successor. */
|
|
118
|
+
protected override async activateClient(): Promise<void> {
|
|
119
|
+
const pending = this.glspClientDeferred;
|
|
120
|
+
await this.workspaceOpened();
|
|
121
|
+
if (this.disposed) {
|
|
69
122
|
return;
|
|
70
123
|
}
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
124
|
+
const attempt = this.connectionReporter.connecting(this.connectionTarget);
|
|
125
|
+
const timeout =
|
|
126
|
+
this.glspClientStartupTimeout > 0
|
|
127
|
+
? setTimeout(() => pending.reject(new Error(this.startupTimeoutMessage())), this.glspClientStartupTimeout)
|
|
128
|
+
: undefined;
|
|
129
|
+
pending.promise.then(
|
|
130
|
+
client => {
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
attempt.connected();
|
|
133
|
+
this.sinceStart = this.clock.stopwatch();
|
|
134
|
+
this.logger.info(`[${this.id}] Diagram client ${++this.startedClients} started.`);
|
|
135
|
+
this.clientStartedEmitter.fire(client);
|
|
136
|
+
this.restartOnLoss(client);
|
|
137
|
+
},
|
|
138
|
+
(error: unknown) => {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
if (this.disposed) {
|
|
141
|
+
attempt.cancelled();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
attempt.failed(describeError(error), () => void this.restart());
|
|
145
|
+
this.scheduleRestart(() => {
|
|
146
|
+
if (this.glspClientDeferred === pending) {
|
|
147
|
+
void this.restart();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
try {
|
|
153
|
+
const connection = await this.createConnection();
|
|
154
|
+
// Ahead of the client's own listener, whose teardown rejects with a
|
|
155
|
+
// transport message instead.
|
|
156
|
+
connection.onClose(() => pending.reject(new Error(this.unreachableMessage())));
|
|
157
|
+
// Upstream builds the connection from the `vscode-jsonrpc` hoisted
|
|
158
|
+
// beside it and the client's typed messages from its protocol's copy;
|
|
159
|
+
// a type sent over another copy's connection throws.
|
|
160
|
+
const client = await this.createGLSPClient(sendByMethodName(connection));
|
|
161
|
+
connection.onDispose(() => client.stop());
|
|
162
|
+
await this.start(client);
|
|
163
|
+
pending.resolve(client);
|
|
164
|
+
} catch (error: unknown) {
|
|
165
|
+
pending.reject(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Run `restart` after the delay for the failures so far, and return that delay. */
|
|
170
|
+
protected scheduleRestart(restart: () => void): number {
|
|
171
|
+
if (this.sinceStart !== undefined && this.sinceStart.elapsedMs >= this.restartEscalationResetMs) {
|
|
172
|
+
this.consecutiveFailures = 0;
|
|
173
|
+
}
|
|
174
|
+
this.sinceStart = undefined;
|
|
175
|
+
const delay = this.restartDelaysMs[Math.min(this.consecutiveFailures, this.restartDelaysMs.length - 1)];
|
|
176
|
+
this.consecutiveFailures++;
|
|
177
|
+
clearTimeout(this.restartTimer);
|
|
178
|
+
this.restartTimer = setTimeout(restart, delay);
|
|
179
|
+
return delay;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Once `client` stops, replace it and start its replacement after the
|
|
183
|
+
* restart delay, unless a dispose stopped it. */
|
|
184
|
+
protected restartOnLoss(client: GLSPClient): void {
|
|
185
|
+
const listener = client.onCurrentStateChanged(state => {
|
|
186
|
+
if (state !== ClientState.ServerError && state !== ClientState.Stopped) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
listener.dispose();
|
|
190
|
+
if (!this.disposed) {
|
|
191
|
+
this.replaceClient();
|
|
192
|
+
this.clientLostEmitter.fire();
|
|
193
|
+
const delay = this.scheduleRestart(() => void this.activateClient());
|
|
194
|
+
// Upstream's client has just logged that it will not be restarted, which holds for it alone.
|
|
195
|
+
this.logger.info(`[${this.id}] Diagram client ${this.startedClients} lost; starting a fresh one in ${delay} ms.`);
|
|
77
196
|
}
|
|
78
197
|
});
|
|
79
|
-
return deferred.promise;
|
|
80
198
|
}
|
|
81
199
|
|
|
200
|
+
/** Start the client and initialize its server, rejecting on failure; the
|
|
201
|
+
* caller settles {@link glspClient}. */
|
|
82
202
|
protected override async start(glspClient: GLSPClient): Promise<void> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
203
|
+
await glspClient.start();
|
|
204
|
+
await this.initialize(glspClient);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Without upstream's own error notification, which would duplicate the reporter's. */
|
|
208
|
+
protected override async initialize(glspClient: GLSPClient): Promise<InitializeResult> {
|
|
209
|
+
return glspClient.initializeServer(await this.createInitializeParameters());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Upstream's base client, without the notifications of its Theia subclass,
|
|
213
|
+
* which would duplicate the reporter's. */
|
|
214
|
+
protected override async createGLSPClient(
|
|
215
|
+
connectionProvider: Parameters<BaseGLSPClientContribution['createGLSPClient']>[0]
|
|
216
|
+
): Promise<GLSPClient> {
|
|
217
|
+
return new HydraniumGlspClient({ id: this.id, connectionProvider });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Whichever channel arrives goes to the attempt waiting now, and one that
|
|
222
|
+
* arrives with none waiting is closed. Theia holds each open until its
|
|
223
|
+
* websocket is back, and after an outage every held open past the first
|
|
224
|
+
* throws "already open" without reaching its handler, so the first to arrive
|
|
225
|
+
* has to serve the latest attempt. Without Theia's replay, which would be a
|
|
226
|
+
* second opener of the same path.
|
|
227
|
+
*/
|
|
228
|
+
protected override async createChannelConnection(): ReturnType<BaseGLSPClientContribution['createChannelConnection']> {
|
|
229
|
+
this.channelRequest?.reject(new Error('A newer start took over the channel request.'));
|
|
230
|
+
const request = new Deferred<Channel>();
|
|
231
|
+
this.channelRequest = request;
|
|
232
|
+
this.connectionProvider.listen(
|
|
233
|
+
GLSPContribution.getPath(this),
|
|
234
|
+
(_path, channel) => {
|
|
235
|
+
const waiting = this.channelRequest;
|
|
236
|
+
this.channelRequest = undefined;
|
|
237
|
+
if (waiting && !this.disposed) {
|
|
238
|
+
waiting.resolve(channel);
|
|
239
|
+
} else {
|
|
240
|
+
channel.close();
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
false
|
|
244
|
+
);
|
|
245
|
+
const channel = await request.promise;
|
|
246
|
+
const connection = createChannelConnection(channel, this.logger);
|
|
247
|
+
this.toDispose.push(Disposable.create(() => this.disposeChannel(connection, channel)));
|
|
248
|
+
return connection;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Also closes the channel: upstream leaves a Theia channel open, and the
|
|
252
|
+
* next one on the same path then cannot open. */
|
|
253
|
+
protected override async disposeChannel(
|
|
254
|
+
connection: Parameters<BaseGLSPClientContribution['disposeChannel']>[0],
|
|
255
|
+
channel: Channel
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
await super.disposeChannel(connection, channel);
|
|
258
|
+
channel.close();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Also fails a start in flight, so nothing reports it after this. */
|
|
262
|
+
override dispose(): void {
|
|
263
|
+
this.disposed = true;
|
|
264
|
+
clearTimeout(this.restartTimer);
|
|
265
|
+
this.glspClientDeferred.promise.catch(() => undefined);
|
|
266
|
+
this.glspClientDeferred.reject(new Error('The diagram client was disposed.'));
|
|
267
|
+
this.clientStartedEmitter.dispose();
|
|
268
|
+
this.clientLostEmitter.dispose();
|
|
269
|
+
super.dispose();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
protected async workspaceOpened(): Promise<void> {
|
|
86
273
|
const roots = this.workspaceService.tryGetRoots();
|
|
87
|
-
if (
|
|
88
|
-
await
|
|
89
|
-
const disposable = this.workspaceService.onWorkspaceChanged(changedRoots => {
|
|
90
|
-
if (changedRoots && changedRoots.length > 0) {
|
|
91
|
-
disposable.dispose();
|
|
92
|
-
resolve();
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
});
|
|
274
|
+
if (roots.length === 0) {
|
|
275
|
+
await Event.toPromise(Event.filter(this.workspaceService.onWorkspaceChanged, changedRoots => changedRoots.length > 0));
|
|
96
276
|
}
|
|
97
|
-
|
|
98
|
-
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
protected get connectionTarget(): ConnectionTarget {
|
|
280
|
+
return (this.cachedConnectionTarget ??= {
|
|
281
|
+
connectingMessage: nls.localize('hydranium/glsp-client-theia/diagram-server-connecting', 'Connecting to the diagram server…'),
|
|
282
|
+
connectedMessage: nls.localize('hydranium/glsp-client-theia/diagram-server-connected', 'Connected to the diagram server.')
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/** One object per contribution: the reporter keys what it has shown on it. */
|
|
286
|
+
protected cachedConnectionTarget?: ConnectionTarget;
|
|
287
|
+
|
|
288
|
+
protected unreachableMessage(): string {
|
|
289
|
+
return nls.localize('hydranium/glsp-client-theia/diagram-server-unreachable', 'Could not connect to the diagram server.');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
protected startupTimeoutMessage(): string {
|
|
293
|
+
return nls.localize(
|
|
294
|
+
'hydranium/glsp-client-theia/diagram-server-timeout',
|
|
295
|
+
'The diagram server did not answer within {0} seconds.',
|
|
296
|
+
String(Math.round(this.glspClientStartupTimeout / 1000))
|
|
297
|
+
);
|
|
99
298
|
}
|
|
100
299
|
}
|