@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.
Files changed (87) hide show
  1. package/README.md +60 -98
  2. package/lib/browser/client-contribution.d.ts +87 -25
  3. package/lib/browser/client-contribution.d.ts.map +1 -1
  4. package/lib/browser/client-contribution.js +233 -53
  5. package/lib/browser/client-contribution.js.map +1 -1
  6. package/lib/browser/diagram-only-marker-manager.d.ts.map +1 -1
  7. package/lib/browser/diagram-only-marker-manager.js.map +1 -1
  8. package/lib/browser/diagram-widget.d.ts +43 -25
  9. package/lib/browser/diagram-widget.d.ts.map +1 -1
  10. package/lib/browser/diagram-widget.js +76 -26
  11. package/lib/browser/diagram-widget.js.map +1 -1
  12. package/lib/browser/glsp-client-theia-module.d.ts +12 -5
  13. package/lib/browser/glsp-client-theia-module.d.ts.map +1 -1
  14. package/lib/browser/glsp-client-theia-module.js +27 -12
  15. package/lib/browser/glsp-client-theia-module.js.map +1 -1
  16. package/lib/browser/glsp-diagram-manager.d.ts +40 -1
  17. package/lib/browser/glsp-diagram-manager.d.ts.map +1 -1
  18. package/lib/browser/glsp-diagram-manager.js +100 -0
  19. package/lib/browser/glsp-diagram-manager.js.map +1 -1
  20. package/lib/browser/glsp-message-service.d.ts.map +1 -1
  21. package/lib/browser/glsp-message-service.js.map +1 -1
  22. package/lib/browser/glsp-saveable.d.ts +67 -0
  23. package/lib/browser/glsp-saveable.d.ts.map +1 -0
  24. package/lib/browser/glsp-saveable.js +110 -0
  25. package/lib/browser/glsp-saveable.js.map +1 -0
  26. package/lib/browser/glsp-theia-frontend-module.d.ts.map +1 -1
  27. package/lib/browser/glsp-theia-frontend-module.js +4 -1
  28. package/lib/browser/glsp-theia-frontend-module.js.map +1 -1
  29. package/lib/browser/hydranium-glsp-diagram-configuration.d.ts +3 -1
  30. package/lib/browser/hydranium-glsp-diagram-configuration.d.ts.map +1 -1
  31. package/lib/browser/hydranium-glsp-diagram-configuration.js +3 -1
  32. package/lib/browser/hydranium-glsp-diagram-configuration.js.map +1 -1
  33. package/lib/browser/index.d.ts +4 -3
  34. package/lib/browser/index.d.ts.map +1 -1
  35. package/lib/browser/index.js +7 -5
  36. package/lib/browser/index.js.map +1 -1
  37. package/lib/browser/theia-diagram-loader.d.ts +25 -0
  38. package/lib/browser/theia-diagram-loader.d.ts.map +1 -0
  39. package/lib/browser/theia-diagram-loader.js +42 -0
  40. package/lib/browser/theia-diagram-loader.js.map +1 -0
  41. package/lib/browser/window-session.d.ts +57 -0
  42. package/lib/browser/window-session.d.ts.map +1 -0
  43. package/lib/browser/window-session.js +102 -0
  44. package/lib/browser/window-session.js.map +1 -0
  45. package/lib/node/connection-container-module.js.map +1 -1
  46. package/lib/node/glsp-server-connection-handler.d.ts +11 -20
  47. package/lib/node/glsp-server-connection-handler.d.ts.map +1 -1
  48. package/lib/node/glsp-server-connection-handler.js +12 -15
  49. package/lib/node/glsp-server-connection-handler.js.map +1 -1
  50. package/package.json +37 -46
  51. package/src/browser/client-contribution.ts +254 -55
  52. package/src/browser/diagram-widget.ts +80 -28
  53. package/src/browser/glsp-client-theia-module.ts +34 -13
  54. package/src/browser/glsp-diagram-manager.ts +112 -3
  55. package/src/browser/glsp-saveable.ts +111 -0
  56. package/src/browser/glsp-theia-frontend-module.ts +4 -1
  57. package/src/browser/hydranium-glsp-diagram-configuration.ts +3 -1
  58. package/src/browser/index.ts +12 -5
  59. package/src/browser/theia-diagram-loader.ts +31 -0
  60. package/src/browser/window-session.ts +114 -0
  61. package/src/node/glsp-server-connection-handler.ts +12 -24
  62. package/style/diagram-loading.css +2 -5
  63. package/lib/browser/action-dispatcher.d.ts +0 -85
  64. package/lib/browser/action-dispatcher.d.ts.map +0 -1
  65. package/lib/browser/action-dispatcher.js +0 -238
  66. package/lib/browser/action-dispatcher.js.map +0 -1
  67. package/lib/browser/diagram-loader.d.ts +0 -123
  68. package/lib/browser/diagram-loader.d.ts.map +0 -1
  69. package/lib/browser/diagram-loader.js +0 -153
  70. package/lib/browser/diagram-loader.js.map +0 -1
  71. package/lib/browser/hidden-bounds-updater.d.ts +0 -122
  72. package/lib/browser/hidden-bounds-updater.d.ts.map +0 -1
  73. package/lib/browser/hidden-bounds-updater.js +0 -207
  74. package/lib/browser/hidden-bounds-updater.js.map +0 -1
  75. package/lib/testing/bind-recorder.d.ts +0 -67
  76. package/lib/testing/bind-recorder.d.ts.map +0 -1
  77. package/lib/testing/bind-recorder.js +0 -72
  78. package/lib/testing/bind-recorder.js.map +0 -1
  79. package/lib/testing/index.d.ts +0 -10
  80. package/lib/testing/index.d.ts.map +0 -1
  81. package/lib/testing/index.js +0 -30
  82. package/lib/testing/index.js.map +0 -1
  83. package/src/browser/action-dispatcher.ts +0 -257
  84. package/src/browser/diagram-loader.ts +0 -173
  85. package/src/browser/hidden-bounds-updater.ts +0 -212
  86. package/src/testing/bind-recorder.ts +0 -126
  87. 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("@eclipse-glsp/theia-integration/lib/node");
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-discovery + buffer-and-replay race fix + connect orchestration live
32
- * on the cross-head {@link AbstractSocketForwardingConnectionHandler} base; this
33
- * subclass supplies only the GLSP specifics — the per-language servicePath
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 — and plugs in `@eclipse-glsp/theia-integration`'s
36
- * `SocketConnectionForwarder` as the byte relay. Adopters with different
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 node_2.AbstractSocketForwardingConnectionHandler {
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 ?? 'Graphical Server',
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
- forwardToSocketConnection(clientChannel, socket) {
55
- return new node_1.SocketConnectionForwarder(clientChannel, socket);
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,mEAAqF;AACrF,2DAA6F;AAE7F,4DAAqE;AAkCrE;;;;;;;;;;;;GAYG;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,kBAAkB;YACpD,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;IAES,yBAAyB,CAAC,aAAsB,EAAE,MAAkB;QAC3E,OAAO,IAAI,gCAAyB,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IAC/D,CAAC;CACH,CAAA;AAjBY,kEAA2B;sCAA3B,2BAA2B;IADvC,IAAA,sBAAU,GAAE;IAEG,WAAA,IAAA,qBAAS,GAAE,CAAA;;GADd,2BAA2B,CAiBvC"}
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.32",
4
- "description": "Theia-coupled client primitives for GLSP heads built on @hydranium/glsp-server. Provides the action dispatcher, channel logger, and connection-handler bases that Theia-based adopters extend; companion of @hydranium/glsp-server.",
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
- "license": "MIT",
24
- "author": {
25
- "name": "Hydranium Team"
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
- "main": "lib/index.js",
62
- "types": "lib/index.d.ts",
63
- "files": [
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": "eslint src test --max-warnings 0",
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.32",
82
- "@hydranium/protocol": "1.0.0-next.32",
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": "^5.8.0"
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.32",
97
- "@hydranium/protocol": "1.0.0-next.32",
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
- "publishConfig": {
109
- "access": "public"
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
- /** Options for `HydraniumGlspClientContribution`. The channel name is the
18
- * Theia Output channel both the LSP and GLSP sides write to (so log lines
19
- * interleave); the ready marker is a server-printed substring that signals
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
- readonly channelName: string;
24
- readonly readyMarker: string;
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
- * 1. `waitForBackendConnected`: tail the OutputChannel for a server-printed
33
- * ready marker. While a socket connection might open earlier, the GLSP
34
- * server typically does internal initialisation before it can accept
35
- * client connections — we wait for it to say so.
36
- * 2. `start`: defer until a workspace is opened. If Theia starts without a
37
- * workspace, this prevents the frontend-backend connection (and its
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, so the `TheiaJsonrpcGLSPClient` returned by
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
- protected readonly channelName: string;
52
- protected readonly readyMarker: string;
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.channelName = options.channelName;
58
- this.readyMarker = options.readyMarker;
59
- }
60
-
61
- /** Wait for the server to print its ready marker into the OutputChannel.
62
- * Resolves immediately if the marker is already present (server started
63
- * before the client contribution mounted). */
64
- protected async waitForBackendConnected(): Promise<void> {
65
- const channel = this.outputChannelManager.getChannel(this.channelName);
66
- const channelText =
67
- (channel as unknown as { resource?: { textModel?: { getValue(): string } } }).resource?.textModel?.getValue() ?? '';
68
- if (channelText.includes(this.readyMarker)) {
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 deferred = new Deferred<void>();
72
- const channelListener = channel.onContentChange(() => {
73
- const text = (channel as unknown as { resource?: { textModel?: { getValue(): string } } }).resource?.textModel?.getValue() ?? '';
74
- if (text.includes(this.readyMarker)) {
75
- channelListener.dispose();
76
- deferred.resolve();
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
- // Defer starting the GLSP client until a workspace is opened. If Theia
84
- // starts without a workspace, this prevents creating the frontend-
85
- // backend connection (and showing the "connecting" progress) prematurely.
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 (!roots || roots.length === 0) {
88
- await new Promise<void>(resolve => {
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
- await this.waitForBackendConnected();
98
- return super.start(glspClient);
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
  }