@hydranium/client-theia 1.0.0-next.22 → 1.0.0-next.220

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 (94) hide show
  1. package/README.md +49 -12
  2. package/lib/browser/browser-capture.js.map +1 -1
  3. package/lib/browser/channel-logger.d.ts +4 -3
  4. package/lib/browser/channel-logger.d.ts.map +1 -1
  5. package/lib/browser/channel-logger.js +10 -7
  6. package/lib/browser/channel-logger.js.map +1 -1
  7. package/lib/browser/connection-diagnostics-contribution.d.ts +69 -0
  8. package/lib/browser/connection-diagnostics-contribution.d.ts.map +1 -0
  9. package/lib/browser/connection-diagnostics-contribution.js +153 -0
  10. package/lib/browser/connection-diagnostics-contribution.js.map +1 -0
  11. package/lib/browser/connection-reporter.d.ts +57 -0
  12. package/lib/browser/connection-reporter.d.ts.map +1 -0
  13. package/lib/browser/connection-reporter.js +104 -0
  14. package/lib/browser/connection-reporter.js.map +1 -0
  15. package/lib/browser/editor-disk-sync.d.ts +142 -0
  16. package/lib/browser/editor-disk-sync.d.ts.map +1 -0
  17. package/lib/browser/editor-disk-sync.js +250 -0
  18. package/lib/browser/editor-disk-sync.js.map +1 -0
  19. package/lib/browser/hydranium-file-service.d.ts +44 -0
  20. package/lib/browser/hydranium-file-service.d.ts.map +1 -0
  21. package/lib/browser/hydranium-file-service.js +61 -0
  22. package/lib/browser/hydranium-file-service.js.map +1 -0
  23. package/lib/browser/index.d.ts +5 -0
  24. package/lib/browser/index.d.ts.map +1 -1
  25. package/lib/browser/index.js +5 -0
  26. package/lib/browser/index.js.map +1 -1
  27. package/lib/browser/log-level-preference.d.ts.map +1 -1
  28. package/lib/browser/log-level-preference.js +11 -0
  29. package/lib/browser/log-level-preference.js.map +1 -1
  30. package/lib/browser/memory-diagnostics-contribution.d.ts +40 -4
  31. package/lib/browser/memory-diagnostics-contribution.d.ts.map +1 -1
  32. package/lib/browser/memory-diagnostics-contribution.js +161 -31
  33. package/lib/browser/memory-diagnostics-contribution.js.map +1 -1
  34. package/lib/browser/session-aware-connection-source.d.ts +101 -0
  35. package/lib/browser/session-aware-connection-source.d.ts.map +1 -0
  36. package/lib/browser/session-aware-connection-source.js +184 -0
  37. package/lib/browser/session-aware-connection-source.js.map +1 -0
  38. package/lib/common/clock.d.ts +19 -0
  39. package/lib/common/clock.d.ts.map +1 -0
  40. package/lib/common/clock.js +13 -0
  41. package/lib/common/clock.js.map +1 -0
  42. package/lib/common/connection-resilience-options.d.ts +30 -0
  43. package/lib/common/connection-resilience-options.d.ts.map +1 -0
  44. package/lib/common/connection-resilience-options.js +11 -0
  45. package/lib/common/connection-resilience-options.js.map +1 -0
  46. package/lib/common/framed-socket-write-buffer.d.ts +173 -0
  47. package/lib/common/framed-socket-write-buffer.d.ts.map +1 -0
  48. package/lib/common/framed-socket-write-buffer.js +293 -0
  49. package/lib/common/framed-socket-write-buffer.js.map +1 -0
  50. package/lib/common/inbound-message-sequence.d.ts +56 -0
  51. package/lib/common/inbound-message-sequence.d.ts.map +1 -0
  52. package/lib/common/inbound-message-sequence.js +98 -0
  53. package/lib/common/inbound-message-sequence.js.map +1 -0
  54. package/lib/common/index.d.ts +13 -0
  55. package/lib/common/index.d.ts.map +1 -0
  56. package/lib/common/index.js +32 -0
  57. package/lib/common/index.js.map +1 -0
  58. package/lib/node/abstract-socket-forwarding-connection-handler.d.ts +12 -8
  59. package/lib/node/abstract-socket-forwarding-connection-handler.d.ts.map +1 -1
  60. package/lib/node/abstract-socket-forwarding-connection-handler.js +70 -13
  61. package/lib/node/abstract-socket-forwarding-connection-handler.js.map +1 -1
  62. package/lib/node/index.d.ts +2 -0
  63. package/lib/node/index.d.ts.map +1 -1
  64. package/lib/node/index.js +2 -0
  65. package/lib/node/index.js.map +1 -1
  66. package/lib/node/session-bound-frontend-connection-service.d.ts +118 -0
  67. package/lib/node/session-bound-frontend-connection-service.d.ts.map +1 -0
  68. package/lib/node/session-bound-frontend-connection-service.js +224 -0
  69. package/lib/node/session-bound-frontend-connection-service.js.map +1 -0
  70. package/lib/node/socket-channel-forwarder.d.ts +54 -0
  71. package/lib/node/socket-channel-forwarder.d.ts.map +1 -0
  72. package/lib/node/socket-channel-forwarder.js +95 -0
  73. package/lib/node/socket-channel-forwarder.js.map +1 -0
  74. package/lib/testing/stub-inversify-context.js.map +1 -1
  75. package/lib/testing/stub-output-channel.js.map +1 -1
  76. package/package.json +31 -32
  77. package/src/browser/channel-logger.ts +10 -7
  78. package/src/browser/connection-diagnostics-contribution.ts +142 -0
  79. package/src/browser/connection-reporter.ts +121 -0
  80. package/src/browser/editor-disk-sync.ts +253 -0
  81. package/src/browser/hydranium-file-service.ts +56 -0
  82. package/src/browser/index.ts +5 -0
  83. package/src/browser/log-level-preference.ts +11 -0
  84. package/src/browser/memory-diagnostics-contribution.ts +268 -45
  85. package/src/browser/session-aware-connection-source.ts +196 -0
  86. package/src/common/clock.ts +20 -0
  87. package/src/common/connection-resilience-options.ts +30 -0
  88. package/src/common/framed-socket-write-buffer.ts +345 -0
  89. package/src/common/inbound-message-sequence.ts +102 -0
  90. package/src/common/index.ts +16 -0
  91. package/src/node/abstract-socket-forwarding-connection-handler.ts +61 -15
  92. package/src/node/index.ts +2 -0
  93. package/src/node/session-bound-frontend-connection-service.ts +260 -0
  94. package/src/node/socket-channel-forwarder.ts +101 -0
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.SocketChannelForwarder = void 0;
12
+ const protocol_1 = require("@hydranium/protocol");
13
+ const core_1 = require("@theia/core");
14
+ const node_1 = require("vscode-jsonrpc/node");
15
+ /**
16
+ * Relays JSON-RPC messages between a Theia browser-frontend {@link Channel} and
17
+ * a server's TCP socket — the backend half of a head's transport. The server
18
+ * speaks vscode-jsonrpc (Content-Length framed) on the socket; the frontend
19
+ * speaks the same protocol over the channel. This forwarder decodes each side
20
+ * and re-emits on the other.
21
+ *
22
+ * Behaves as `@eclipse-glsp/theia-integration`'s `SocketConnectionForwarder`
23
+ * does, and both heads use it, except that when the channel closes, the writes
24
+ * still queued go out before the socket ends. A page going away sends its last
25
+ * messages and closes its channel in the same tick, and the writer takes
26
+ * several turns per message, so destroying the socket on the close loses them.
27
+ */
28
+ class SocketChannelForwarder {
29
+ channel;
30
+ socket;
31
+ clock;
32
+ toDispose = new core_1.DisposableCollection();
33
+ /** The last write to the socket; the writer runs its writes one at a time, so this settles last. */
34
+ lastWrite = Promise.resolve();
35
+ /**
36
+ * How long {@link endSocket} waits for the queued writes before it destroys
37
+ * the socket anyway. Without a bound, a server that stops reading holds the
38
+ * socket open for good, and with it the server's end of the connection.
39
+ */
40
+ flushTimeoutMs = 10_000;
41
+ constructor(channel, socket,
42
+ /** Times {@link flushTimeoutMs}. */
43
+ clock = new protocol_1.SystemClock()) {
44
+ this.channel = channel;
45
+ this.socket = socket;
46
+ this.clock = clock;
47
+ const reader = new node_1.SocketMessageReader(socket);
48
+ const writer = new node_1.SocketMessageWriter(socket);
49
+ // Never listens, so it handles no message and has nothing to log.
50
+ // ast-grep-ignore: connection-without-logger
51
+ const connection = (0, node_1.createMessageConnection)(reader, writer);
52
+ // Nothing here destroys the socket directly. `SocketMessageWriter.dispose()`
53
+ // destroys it itself, which covers both `dispose` and `endSocket`; and
54
+ // `connection.onClose` fires only from the reader's or writer's own close,
55
+ // which for a socket means the socket has already gone — so a destroy
56
+ // handler there is downstream of the effect it would be trying to cause.
57
+ this.toDispose.pushAll([
58
+ reader.listen(message => this.writeToChannel(message)),
59
+ channel.onMessage(provider => {
60
+ // A failed write means the socket is gone, which its own close reports.
61
+ this.lastWrite = writer.write(this.decodeChannelMessage(provider)).catch(() => undefined);
62
+ }),
63
+ channel.onClose(() => void this.endSocket(connection)),
64
+ connection.onClose(() => channel.close()),
65
+ core_1.Disposable.create(() => {
66
+ channel.close();
67
+ connection.dispose();
68
+ })
69
+ ]);
70
+ }
71
+ /**
72
+ * Dispose `connection`, which destroys the socket, once the writes queued
73
+ * before the channel closed are out, or {@link flushTimeoutMs} has passed. A
74
+ * write has settled once the socket handed its bytes to the OS, and
75
+ * destroying the socket then loses nothing.
76
+ */
77
+ async endSocket(connection) {
78
+ await this.clock.raceTimer(this.lastWrite, this.flushTimeoutMs);
79
+ connection.dispose();
80
+ }
81
+ decodeChannelMessage(provider) {
82
+ const buffer = provider().readBytes();
83
+ return JSON.parse(new TextDecoder().decode(buffer));
84
+ }
85
+ writeToChannel(message) {
86
+ const writeBuffer = this.channel.getWriteBuffer();
87
+ writeBuffer.writeBytes(new TextEncoder().encode(JSON.stringify(message)));
88
+ writeBuffer.commit();
89
+ }
90
+ dispose() {
91
+ this.toDispose.dispose();
92
+ }
93
+ }
94
+ exports.SocketChannelForwarder = SocketChannelForwarder;
95
+ //# sourceMappingURL=socket-channel-forwarder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-channel-forwarder.js","sourceRoot":"","sources":["../../src/node/socket-channel-forwarder.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAElF,kDAA8D;AAC9D,sCAAmG;AAEnG,8CAM6B;AAE7B;;;;;;;;;;;;GAYG;AACH;IAYyB,OAAO;IACP,MAAM;IAEN,KAAK;IAdR,SAAS,GAAG,IAAI,2BAAoB,EAAE,CAAC;IAC1D,oGAAoG;IAC1F,SAAS,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IACvD;;;;OAIG;IACgB,cAAc,GAAW,MAAM,CAAC;IAEnD,YACsB,OAAgB,EAChB,MAAkB;IACrC,oCAAoC;IACjB,KAAK,GAAU,IAAI,sBAAW,EAAE;uBAHhC,OAAO;sBACP,MAAM;qBAEN,KAAK;QAExB,MAAM,MAAM,GAAG,IAAI,0BAAmB,CAAC,MAAM,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,0BAAmB,CAAC,MAAM,CAAC,CAAC;QAC/C,kEAAkE;QAClE,6CAA6C;QAC7C,MAAM,UAAU,GAAG,IAAA,8BAAuB,EAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC3D,6EAA6E;QAC7E,uEAAuE;QACvE,2EAA2E;QAC3E,sEAAsE;QACtE,yEAAyE;QACzE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;YACpB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YACtD,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE;gBAC1B,wEAAwE;gBACxE,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAC7F,CAAC,CAAC;YACF,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;YACtD,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACzC,iBAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBACpB,OAAO,CAAC,KAAK,EAAE,CAAC;gBAChB,UAAU,CAAC,OAAO,EAAE,CAAC;YACxB,CAAC,CAAC;SACJ,CAAC,CAAC;IACN,CAAC;IAED;;;;;OAKG;IACO,KAAK,CAAC,SAAS,CAAC,UAA6B;QACpD,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;QAChE,UAAU,CAAC,OAAO,EAAE,CAAC;IACxB,CAAC;IAES,oBAAoB,CAAC,QAAyB;QACrD,MAAM,MAAM,GAAG,QAAQ,EAAE,CAAC,SAAS,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAY,CAAC;IAClE,CAAC;IAES,cAAc,CAAC,OAAgB;QACtC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;QAClD,WAAW,CAAC,UAAU,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC1E,WAAW,CAAC,MAAM,EAAE,CAAC;IACxB,CAAC;IAED,OAAO;QACJ,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;IAC5B,CAAC;CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"stub-inversify-context.js","sourceRoot":"","sources":["../../src/testing/stub-inversify-context.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;AAQlF,4DAgBC;AApBD;;;2CAG2C;AAC3C,SAAgB,wBAAwB,CAAC,eAAwB;IAC9D,MAAM,aAAa,GAChB,eAAe,KAAK,SAAS;QAC1B,CAAC,CAAC,IAAI;QACN,CAAC,CAAC;YACG,QAAQ,EAAE;gBACP;oBACG,kBAAkB,EAAE,eAAe,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE;iBAC7E;aACH;SACH,CAAC;IACV,OAAO;QACJ,cAAc,EAAE;YACb,aAAa;SACf;KAC8B,CAAC;AACtC,CAAC"}
1
+ {"version":3,"file":"stub-inversify-context.js","sourceRoot":"","sources":["../../src/testing/stub-inversify-context.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAIlF;;;2CAG2C;AAC3C,kCAAyC,eAAwB;IAC9D,MAAM,aAAa,GAChB,eAAe,KAAK,SAAS;QAC1B,CAAC,CAAC,IAAI;QACN,CAAC,CAAC;YACG,QAAQ,EAAE;gBACP;oBACG,kBAAkB,EAAE,eAAe,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE;iBAC7E;aACH;SACH,CAAC;IACV,OAAO;QACJ,cAAc,EAAE;YACb,aAAa;SACf;KAC8B,CAAC;AACtC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"stub-output-channel.js","sourceRoot":"","sources":["../../src/testing/stub-output-channel.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;AAmBlF,oEAoBC;AApBD,SAAgB,4BAA4B;IACzC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6B,CAAC;IACtD,OAAO;QACJ,QAAQ;QACR,UAAU,CAAC,IAAY;YACpB,IAAI,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACZ,MAAM,KAAK,GAAa,EAAE,CAAC;gBAC3B,OAAO,GAAG;oBACP,IAAI;oBACJ,KAAK;oBACL,UAAU,CAAC,IAAY;wBACpB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACpB,CAAC;iBACH,CAAC;gBACF,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC/B,CAAC;YACD,OAAO,OAAO,CAAC;QAClB,CAAC;KACH,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"stub-output-channel.js","sourceRoot":"","sources":["../../src/testing/stub-output-channel.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAmBlF;IACG,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6B,CAAC;IACtD,OAAO;QACJ,QAAQ;QACR,UAAU,CAAC,IAAY;YACpB,IAAI,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACZ,MAAM,KAAK,GAAa,EAAE,CAAC;gBAC3B,OAAO,GAAG;oBACP,IAAI;oBACJ,KAAK;oBACL,UAAU,CAAC,IAAY;wBACpB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACpB,CAAC;iBACH,CAAC;gBACF,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC/B,CAAC;YACD,OAAO,OAAO,CAAC;QAClB,CAAC;KACH,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydranium/client-theia",
3
- "version": "1.0.0-next.22",
3
+ "version": "1.0.0-next.220",
4
4
  "description": "Cross-head Theia client primitives shared by the hydranium protocol heads. Provides the socket-forwarding connection-handler base that the data-server and GLSP Theia integrations subclass, plus the preference-driven Output-channel logger; carries no head-specific dependency.",
5
5
  "keywords": [
6
6
  "hydranium",
@@ -13,16 +13,23 @@
13
13
  "bugs": {
14
14
  "url": "https://github.com/eclipse-emfcloud/hydranium/issues"
15
15
  },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Hydranium Team"
19
+ },
16
20
  "repository": {
17
21
  "type": "git",
18
22
  "url": "https://github.com/eclipse-emfcloud/hydranium.git",
19
23
  "directory": "packages/client-theia"
20
24
  },
21
- "license": "MIT",
22
- "author": {
23
- "name": "Hydranium Team"
24
- },
25
+ "files": [
26
+ "lib",
27
+ "src",
28
+ "!lib/**/*.tsbuildinfo"
29
+ ],
25
30
  "sideEffects": false,
31
+ "main": "lib/index.js",
32
+ "types": "lib/index.d.ts",
26
33
  "exports": {
27
34
  ".": {
28
35
  "types": "./lib/index.d.ts",
@@ -32,64 +39,56 @@
32
39
  "types": "./lib/browser/index.d.ts",
33
40
  "default": "./lib/browser/index.js"
34
41
  },
35
- "./lib/browser": {
36
- "types": "./lib/browser/index.d.ts",
37
- "default": "./lib/browser/index.js"
42
+ "./common": {
43
+ "types": "./lib/common/index.d.ts",
44
+ "default": "./lib/common/index.js"
38
45
  },
39
46
  "./node": {
40
47
  "types": "./lib/node/index.d.ts",
41
48
  "default": "./lib/node/index.js"
42
49
  },
43
- "./lib/node": {
44
- "types": "./lib/node/index.d.ts",
45
- "default": "./lib/node/index.js"
46
- },
47
50
  "./testing": {
48
51
  "types": "./lib/testing/index.d.ts",
49
52
  "default": "./lib/testing/index.js"
50
- },
51
- "./lib/testing": {
52
- "types": "./lib/testing/index.d.ts",
53
- "default": "./lib/testing/index.js"
54
53
  }
55
54
  },
56
- "main": "lib/index.js",
57
- "types": "lib/index.d.ts",
58
- "files": [
59
- "lib",
60
- "src",
61
- "!lib/**/*.tsbuildinfo"
62
- ],
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
63
58
  "scripts": {
64
59
  "build": "tsc -b",
65
60
  "clean": "rimraf lib tsconfig.tsbuildinfo",
66
- "lint": "eslint src test --max-warnings 0",
61
+ "lint": "oxlint --format stylish --config ../../oxlint.config.cjs src test --max-warnings 0",
67
62
  "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);}\"",
68
63
  "test": "npm run typecheck:test && vitest run",
69
64
  "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
70
65
  "watch": "tsc -b -w --preserveWatchOutput"
71
66
  },
72
67
  "devDependencies": {
73
- "@hydranium/protocol": "1.0.0-next.22",
68
+ "@hydranium/protocol": "1.0.0-next.220",
74
69
  "@theia/core": "^1.70.0",
70
+ "@theia/editor": "^1.70.0",
71
+ "@theia/filesystem": "^1.70.0",
75
72
  "@theia/output": "^1.70.0",
76
73
  "@types/node": "^22.0.0",
77
74
  "inversify": "6.2.2",
78
75
  "reflect-metadata": "0.2.2",
79
76
  "rimraf": "^5.0.0",
80
- "typescript": "^5.8.0"
77
+ "typescript": "^7.0.2",
78
+ "vscode-jsonrpc": "9.0.0"
81
79
  },
82
80
  "peerDependencies": {
83
- "@hydranium/protocol": "1.0.0-next.22",
81
+ "@hydranium/protocol": "1.0.0-next.220",
84
82
  "@theia/core": "^1.70.0",
83
+ "@theia/editor": "^1.70.0",
84
+ "@theia/filesystem": "^1.70.0",
85
85
  "@theia/output": "^1.70.0",
86
- "inversify": "^6.0.0"
86
+ "inversify": "^6.0.0",
87
+ "vscode-jsonrpc": "^9.0.0"
87
88
  },
88
89
  "engines": {
89
90
  "node": ">=22.13"
90
91
  },
91
- "publishConfig": {
92
- "access": "public"
93
- },
94
- "//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."
92
+ "//peerDependencies": "@theia/core stays at 1.70 here, matching the sibling Theia packages, even though the connection-resilience bindings need 1.71: Theia only began binding `SocketWriteBuffer` then, and before that each connection built one privately, so there is no binding to rebind. Rather than split the package's floor for one feature, `bindConnectionResilience` probes for that binding and warns instead of installing when it is absent — a capability check rather than a version parse, so it answers about the container in front of it. Everything else the feature touches exists at 1.70, and the emitted declarations deliberately name no type that 1.70 lacks, so the package still compiles there. Verified by unpacking 1.70, 1.71 and 1.72, not inferred from a changelog. `vscode-jsonrpc` takes a caret: each `vscode-languageserver-protocol` release pins its transport exactly, so an exact peer would name a copy a fresh install rarely holds, while the caret still excludes the 8.x line.",
93
+ "//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."
95
94
  }
@@ -10,6 +10,7 @@
10
10
  import { AbstractLogger, DefaultTracer, type LogLevel, type MemoryReader, SystemClock } from '@hydranium/protocol';
11
11
  import { injectable, type interfaces, unmanaged, inject } from '@theia/core/shared/inversify';
12
12
  import { type OutputChannel, OutputChannelManager } from '@theia/output/lib/browser/output-channel';
13
+ import { Clock } from '../common/clock';
13
14
 
14
15
  /**
15
16
  * Configuration for a {@link ChannelLogger}, supplied once at bind time via
@@ -65,10 +66,8 @@ export class ChannelLogger extends AbstractLogger {
65
66
  }
66
67
 
67
68
  protected emit(_level: LogLevel, label: string, message: string, args: readonly unknown[]): void {
68
- const componentSegment = this.component ? ` [${this.component}]` : '';
69
69
  const formattedArgs = args.length > 0 ? ' ' + args.map(stringifyArg).join(' ') : '';
70
- // Pad to match the server's 5-char label width so timestamps align across sources.
71
- this.channel.appendLine(`[${label.padEnd(5)} - ${this.timestamp()}]${componentSegment} ${message}${formattedArgs}`);
70
+ this.channel.appendLine(`${this.formatLinePrefix(label)} ${message}${formattedArgs}`);
72
71
  }
73
72
 
74
73
  /**
@@ -94,9 +93,10 @@ const ChannelLoggerBase = Symbol('ChannelLoggerBase');
94
93
 
95
94
  /**
96
95
  * Inversify token for the browser `Tracer` — a {@link DefaultTracer} over
97
- * the {@link ChannelLogger} (emit sink) + a {@link SystemClock} + a browser
98
- * heap reader. The browser head has no Langium `services.Tracer` slot, so this
99
- * is how frontend services that time obtain a tracer: `@inject(ChannelTracer)`.
96
+ * the {@link ChannelLogger} (emit sink) + the container's {@link Clock}, or a
97
+ * `SystemClock` when it binds none, + a browser heap reader. The browser head
98
+ * has no Langium `services.Tracer` slot, so this is how frontend services that
99
+ * time obtain a tracer: `@inject(ChannelTracer)`.
100
100
  * Bound by {@link bindChannelLogger}.
101
101
  */
102
102
  export const ChannelTracer = Symbol('ChannelTracer');
@@ -134,7 +134,10 @@ export function bindChannelLogger(bind: interfaces.Bind, options: ChannelLoggerO
134
134
  return parentName ? base.for(parentName) : base;
135
135
  });
136
136
  bind(ChannelTracer)
137
- .toDynamicValue(ctx => new DefaultTracer(ctx.container.get<ChannelLogger>(ChannelLoggerBase), new SystemClock(), readBrowserMemory))
137
+ .toDynamicValue(ctx => {
138
+ const clock = ctx.container.isBound(Clock) ? ctx.container.get<Clock>(Clock) : new SystemClock();
139
+ return new DefaultTracer(ctx.container.get<ChannelLogger>(ChannelLoggerBase), clock, readBrowserMemory);
140
+ })
138
141
  .inSingletonScope();
139
142
  }
140
143
 
@@ -0,0 +1,142 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, 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 { MessageService, nls } from '@theia/core';
11
+ import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
12
+ import { ConnectionStatus, ConnectionStatusService } from '@theia/core/lib/browser/connection-status-service';
13
+ import { WebSocketConnectionSource } from '@theia/core/lib/browser/messaging/ws-connection-source';
14
+ import { inject, injectable, type interfaces } from '@theia/core/shared/inversify';
15
+ import { type ConnectionBufferOverflow } from '../common/framed-socket-write-buffer';
16
+ import { ChannelLogger } from './channel-logger';
17
+ import { SessionAwareConnectionSource } from './session-aware-connection-source';
18
+
19
+ /**
20
+ * Records the browser side of the websocket lifecycle into the adopter's Output
21
+ * channel, and tells the user the one thing they cannot recover from on their
22
+ * own.
23
+ *
24
+ * Theia reports an outage only as an "Offline" status-bar item, which cannot
25
+ * distinguish a plain network drop from the server having stopped answering on a
26
+ * socket that is still up. The latter is what a connection defect looks like
27
+ * from here, so the two are logged differently — that distinction is what says
28
+ * whether reopening an editor helps or only a reload does.
29
+ *
30
+ * Purely observational apart from the overflow message: nothing here changes how
31
+ * the connection behaves.
32
+ */
33
+ @injectable()
34
+ export class ConnectionDiagnosticsContribution implements FrontendApplicationContribution {
35
+ @inject(WebSocketConnectionSource) protected readonly connectionSource: WebSocketConnectionSource;
36
+ @inject(ConnectionStatusService) protected readonly connectionStatus: ConnectionStatusService;
37
+ @inject(ChannelLogger) protected readonly logger: ChannelLogger;
38
+ @inject(MessageService) protected readonly messageService: MessageService;
39
+
40
+ /** Main channels created so far; more than one means the server did not recognise this frontend. */
41
+ protected channelCount = 0;
42
+ protected socketClosedAt?: number;
43
+ protected offlineAt?: number;
44
+
45
+ initialize(): void {
46
+ this.connectionSource.onSocketDidOpen(() => this.handleSocketOpen());
47
+ this.connectionSource.onSocketDidClose(() => this.handleSocketClose());
48
+ this.connectionSource.onConnectionDidOpen(() => this.handleChannelOpen());
49
+ this.connectionStatus.onStatusChange(status => this.handleStatusChange(status));
50
+ if (this.connectionSource instanceof SessionAwareConnectionSource) {
51
+ this.connectionSource.onBufferOverflow(overflow => this.handleBufferOverflow(overflow));
52
+ }
53
+ // The socket is opened by the preloader, before this contribution exists, so the first
54
+ // `onSocketDidOpen` has already fired; state the current situation instead.
55
+ this.logger.info(`diagnostics active; socket ${this.socketId()}, backend ${this.statusName(this.connectionStatus.currentStatus)}`);
56
+ }
57
+
58
+ protected handleSocketOpen(): void {
59
+ const downFor = this.socketClosedAt === undefined ? '' : ` after ${Date.now() - this.socketClosedAt} ms down`;
60
+ this.socketClosedAt = undefined;
61
+ this.logger.info(`websocket connected: socket ${this.socketId()}${downFor}`);
62
+ }
63
+
64
+ protected handleSocketClose(): void {
65
+ this.socketClosedAt = Date.now();
66
+ this.logger.info(`websocket disconnected: socket ${this.socketId()}; outgoing messages are buffered until it returns`);
67
+ }
68
+
69
+ /**
70
+ * A second channel means the reconnect was refused and the session discarded.
71
+ * Only reachable with `reloadOnReconnect` off — with it on, Theia reloads the
72
+ * page instead of opening another channel.
73
+ */
74
+ protected handleChannelOpen(): void {
75
+ this.channelCount++;
76
+ if (this.channelCount === 1) {
77
+ this.logger.info(`initial channel opened on socket ${this.socketId()}`);
78
+ return;
79
+ }
80
+ this.logger.warn(
81
+ `new channel #${this.channelCount} on socket ${this.socketId()}: the backend did not recognise this frontend, ` +
82
+ 'so its session was discarded and every pending request rejected'
83
+ );
84
+ }
85
+
86
+ protected handleStatusChange(status: ConnectionStatus): void {
87
+ if (status === ConnectionStatus.ONLINE) {
88
+ const offlineFor = this.offlineAt === undefined ? '' : ` after ${Date.now() - this.offlineAt} ms`;
89
+ this.offlineAt = undefined;
90
+ this.logger.info(`backend reachable again${offlineFor}`);
91
+ return;
92
+ }
93
+ this.offlineAt = Date.now();
94
+ const socket = this.connectionSource.socket;
95
+ if (socket?.connected) {
96
+ this.logger.warn(
97
+ `backend stopped answering while socket ${socket.id} is still connected: nothing this session sends will ` +
98
+ 'arrive, and socket.io will not reconnect on its own. Reload the page to recover.'
99
+ );
100
+ return;
101
+ }
102
+ this.logger.info(`backend unreachable; websocket is disconnected (socket ${this.socketId()})`);
103
+ }
104
+
105
+ /**
106
+ * The outage outlasted what the buffer can hold, so changes made from here on
107
+ * are being thrown away rather than queued. Reconnecting resumes sending but
108
+ * cannot bring those back, which leaves the editor disagreeing with the
109
+ * server — and without a message the user would see nothing but an editor
110
+ * that has quietly stopped saving, so say it plainly.
111
+ */
112
+ protected handleBufferOverflow(overflow: ConnectionBufferOverflow): void {
113
+ this.logger.error(`buffer full at ${overflow.maxBytes} bytes holding ${overflow.messages} message(s)`);
114
+ this.messageService.error(
115
+ nls.localize(
116
+ 'hydranium/connection/offlineLimitReached',
117
+ 'Disconnected for too long: recent changes were not sent. Reload the page before continuing.'
118
+ )
119
+ );
120
+ }
121
+
122
+ protected socketId(): string {
123
+ return this.connectionSource.socket?.id ?? WebSocketConnectionSource.NO_CONNECTION;
124
+ }
125
+
126
+ protected statusName(status: ConnectionStatus): string {
127
+ return status === ConnectionStatus.ONLINE ? 'online' : 'offline';
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Bind {@link ConnectionDiagnosticsContribution} as a `FrontendApplicationContribution`.
133
+ *
134
+ * Needs a {@link ChannelLogger} binding, so call it from a module that has also
135
+ * called `bindChannelLogger`. Unlike `bindConnectionResilience` this belongs in a
136
+ * normal frontend module: it only observes, so it has no reason to run during
137
+ * preload, and `MessageService` is not available that early.
138
+ */
139
+ export function bindConnectionDiagnostics(bind: interfaces.Bind): void {
140
+ bind(ConnectionDiagnosticsContribution).toSelf().inSingletonScope();
141
+ bind(FrontendApplicationContribution).toService(ConnectionDiagnosticsContribution);
142
+ }
@@ -0,0 +1,121 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, 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 { MessageService, nls, type Progress } from '@theia/core';
11
+ import { inject, injectable, type interfaces } from '@theia/core/shared/inversify';
12
+
13
+ /** What a head connects to, as the whole sentences reported about it. */
14
+ export interface ConnectionTarget {
15
+ readonly connectingMessage: string;
16
+ readonly connectedMessage: string;
17
+ }
18
+
19
+ /** One connection attempt; it reports exactly one end. */
20
+ export interface ConnectionAttempt {
21
+ connected(): void;
22
+ /** Ends the attempt with nothing to report, as when its head is disposed
23
+ * or a newer attempt takes over. */
24
+ cancelled(): void;
25
+ /**
26
+ * `message` is the whole sentence to report. `retry`, when given, starts a
27
+ * fresh attempt at once; a head without one keeps retrying on its own.
28
+ */
29
+ failed(message: string, retry?: () => void): void;
30
+ }
31
+
32
+ /**
33
+ * How a head's connection attempts reach the user. A head calls
34
+ * {@link connecting} for every attempt, a reconnect included, and keeps the
35
+ * attempt's retries and bounds itself; this decides only what is shown. Rebind
36
+ * the slot to show nothing, or something other than notifications.
37
+ */
38
+ export interface ConnectionReporter {
39
+ connecting(target: ConnectionTarget): ConnectionAttempt;
40
+ }
41
+ export const ConnectionReporter = Symbol('ConnectionReporter');
42
+
43
+ /**
44
+ * Reports through Theia notifications: an attempt still running after
45
+ * {@link connectingNoticeDelayMs} shows its progress, and an attempt ends in at
46
+ * most one notification. A failure is shown once per target until it connects
47
+ * again, so a head that keeps retrying does not raise one per attempt.
48
+ */
49
+ @injectable()
50
+ export class DefaultConnectionReporter implements ConnectionReporter {
51
+ @inject(MessageService) protected readonly messageService!: MessageService;
52
+
53
+ protected readonly connectingNoticeDelayMs: number = 3_000;
54
+ /** Targets whose last reported attempt failed. */
55
+ protected readonly failing = new Set<ConnectionTarget>();
56
+
57
+ connecting(target: ConnectionTarget): ConnectionAttempt {
58
+ let progress: Promise<Progress> | undefined;
59
+ const notice = this.failing.has(target)
60
+ ? undefined
61
+ : setTimeout(() => (progress = this.showConnecting(target)), this.connectingNoticeDelayMs);
62
+ let settled = false;
63
+ const settle = (): boolean => {
64
+ if (settled) {
65
+ return false;
66
+ }
67
+ settled = true;
68
+ clearTimeout(notice);
69
+ void progress?.then(shown => shown.cancel());
70
+ return true;
71
+ };
72
+ return {
73
+ connected: () => {
74
+ if (settle() && (this.failing.delete(target) || progress)) {
75
+ this.showConnected(target);
76
+ }
77
+ },
78
+ cancelled: () => {
79
+ settle();
80
+ },
81
+ failed: (message, retry) => {
82
+ if (settle() && !this.failing.has(target)) {
83
+ this.failing.add(target);
84
+ void this.showFailed(target, message, retry);
85
+ }
86
+ }
87
+ };
88
+ }
89
+
90
+ protected showConnecting(target: ConnectionTarget): Promise<Progress> {
91
+ return this.messageService.showProgress({ text: target.connectingMessage });
92
+ }
93
+
94
+ protected showConnected(target: ConnectionTarget): void {
95
+ this.messageService.info(target.connectedMessage);
96
+ }
97
+
98
+ protected async showFailed(target: ConnectionTarget, message: string, retry?: () => void): Promise<void> {
99
+ if (!retry) {
100
+ this.messageService.error(message);
101
+ return;
102
+ }
103
+ const label = this.retryLabel();
104
+ if ((await this.messageService.error(message, label)) === label) {
105
+ // The retried attempt reports afresh, progress included.
106
+ this.failing.delete(target);
107
+ retry();
108
+ }
109
+ }
110
+
111
+ protected retryLabel(): string {
112
+ return nls.localize('hydranium/client-theia/connection-retry', 'Retry');
113
+ }
114
+ }
115
+
116
+ /** Bind {@link DefaultConnectionReporter} unless the slot is bound already; every head calls this. */
117
+ export function bindConnectionReporter(bind: interfaces.Bind, isBound: interfaces.IsBound): void {
118
+ if (!isBound(ConnectionReporter)) {
119
+ bind(ConnectionReporter).to(DefaultConnectionReporter).inSingletonScope();
120
+ }
121
+ }