@hydranium/client-theia 1.0.0-next.10

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/lib/browser/browser-capture.d.ts +21 -0
  4. package/lib/browser/browser-capture.d.ts.map +1 -0
  5. package/lib/browser/browser-capture.js +61 -0
  6. package/lib/browser/browser-capture.js.map +1 -0
  7. package/lib/browser/channel-logger.d.ts +87 -0
  8. package/lib/browser/channel-logger.d.ts.map +1 -0
  9. package/lib/browser/channel-logger.js +149 -0
  10. package/lib/browser/channel-logger.js.map +1 -0
  11. package/lib/browser/index.d.ts +13 -0
  12. package/lib/browser/index.d.ts.map +1 -0
  13. package/lib/browser/index.js +30 -0
  14. package/lib/browser/index.js.map +1 -0
  15. package/lib/browser/log-level-preference.d.ts +82 -0
  16. package/lib/browser/log-level-preference.d.ts.map +1 -0
  17. package/lib/browser/log-level-preference.js +146 -0
  18. package/lib/browser/log-level-preference.js.map +1 -0
  19. package/lib/browser/memory-diagnostics-contribution.d.ts +105 -0
  20. package/lib/browser/memory-diagnostics-contribution.d.ts.map +1 -0
  21. package/lib/browser/memory-diagnostics-contribution.js +229 -0
  22. package/lib/browser/memory-diagnostics-contribution.js.map +1 -0
  23. package/lib/index.d.ts +10 -0
  24. package/lib/index.d.ts.map +1 -0
  25. package/lib/index.js +11 -0
  26. package/lib/index.js.map +1 -0
  27. package/lib/node/abstract-socket-forwarding-connection-handler.d.ts +96 -0
  28. package/lib/node/abstract-socket-forwarding-connection-handler.d.ts.map +1 -0
  29. package/lib/node/abstract-socket-forwarding-connection-handler.js +224 -0
  30. package/lib/node/abstract-socket-forwarding-connection-handler.js.map +1 -0
  31. package/lib/node/index.d.ts +10 -0
  32. package/lib/node/index.d.ts.map +1 -0
  33. package/lib/node/index.js +26 -0
  34. package/lib/node/index.js.map +1 -0
  35. package/lib/testing/index.d.ts +11 -0
  36. package/lib/testing/index.d.ts.map +1 -0
  37. package/lib/testing/index.js +29 -0
  38. package/lib/testing/index.js.map +1 -0
  39. package/lib/testing/stub-inversify-context.d.ts +15 -0
  40. package/lib/testing/stub-inversify-context.d.ts.map +1 -0
  41. package/lib/testing/stub-inversify-context.js +32 -0
  42. package/lib/testing/stub-inversify-context.js.map +1 -0
  43. package/lib/testing/stub-output-channel.d.ts +25 -0
  44. package/lib/testing/stub-output-channel.d.ts.map +1 -0
  45. package/lib/testing/stub-output-channel.js +33 -0
  46. package/lib/testing/stub-output-channel.js.map +1 -0
  47. package/package.json +95 -0
  48. package/src/browser/browser-capture.ts +75 -0
  49. package/src/browser/channel-logger.ts +160 -0
  50. package/src/browser/index.ts +14 -0
  51. package/src/browser/log-level-preference.ts +127 -0
  52. package/src/browser/memory-diagnostics-contribution.ts +248 -0
  53. package/src/index.ts +19 -0
  54. package/src/node/abstract-socket-forwarding-connection-handler.ts +218 -0
  55. package/src/node/index.ts +10 -0
  56. package/src/testing/index.ts +14 -0
  57. package/src/testing/stub-inversify-context.ts +32 -0
  58. package/src/testing/stub-output-channel.ts +47 -0
@@ -0,0 +1,224 @@
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
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
27
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
28
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
29
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
30
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
31
+ };
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ var __metadata = (this && this.__metadata) || function (k, v) {
50
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
51
+ };
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.AbstractSocketForwardingConnectionHandler = void 0;
54
+ const core_1 = require("@theia/core");
55
+ const channel_1 = require("@theia/core/lib/common/message-rpc/channel");
56
+ const promise_util_1 = require("@theia/core/lib/common/promise-util");
57
+ const inversify_1 = require("@theia/core/shared/inversify");
58
+ const net = __importStar(require("node:net"));
59
+ /**
60
+ * Cross-head base for the Theia backend half of a protocol head's transport:
61
+ * it bridges a Theia browser-frontend {@link Channel} to a server's TCP socket
62
+ * by relaying bytes between the two.
63
+ *
64
+ * Theia delivers each frontend connection to a registered `ConnectionHandler`
65
+ * keyed by its `path`. The handler asks the language server (via a registered
66
+ * command) for the target server's listening port, opens a `net.Socket` to that
67
+ * port, and forwards the Theia channel onto the socket. The frontend terminates
68
+ * the same wire protocol the server speaks, so the backend performs no semantic
69
+ * re-proxy — it relays bytes only and stays oblivious to the server's method set.
70
+ *
71
+ * The single per-head variation — *which* byte forwarder bridges the channel and
72
+ * the socket — is the abstract {@link forwardToSocketConnection} hook: the GLSP
73
+ * head plugs in `@eclipse-glsp/theia-integration`'s `SocketConnectionForwarder`,
74
+ * the data-server head its own `SocketChannelForwarder`. Keeping the forwarder
75
+ * behind the hook is what lets the data head stay GLSP-free while sharing the
76
+ * port-discovery + buffer-and-replay race fix + connect orchestration here.
77
+ */
78
+ let AbstractSocketForwardingConnectionHandler = class AbstractSocketForwardingConnectionHandler {
79
+ messageService;
80
+ commandService;
81
+ // Theia backend `ILogger` — this handler runs in the Theia backend process,
82
+ // not the spawned language-server process, so the framework `LspLogger`
83
+ // (which needs the Langium connection/services) is out of reach here.
84
+ logger;
85
+ path;
86
+ portCommand;
87
+ logComponent;
88
+ serverName;
89
+ findPortTimeout;
90
+ findPortAttempts;
91
+ connectTimeoutMs;
92
+ onSocketCreated;
93
+ constructor(options) {
94
+ this.path = options.path;
95
+ this.portCommand = options.portCommand;
96
+ this.logComponent = options.logComponent;
97
+ this.serverName = options.serverName;
98
+ this.findPortTimeout = options.findPortTimeout ?? 500;
99
+ this.findPortAttempts = options.findPortAttempts ?? -1;
100
+ this.connectTimeoutMs = options.connectTimeoutMs ?? 10000;
101
+ this.onSocketCreated = options.onSocketCreated;
102
+ }
103
+ onConnection(connection) {
104
+ this.initializeServerConnection(connection);
105
+ }
106
+ async initializeServerConnection(channel) {
107
+ // RACE FIX: subscribe to `channel.onMessage` synchronously and buffer
108
+ // every `MessageProvider` until the forwarder is wired. Theia's
109
+ // `ForwardingChannel.onMessage` is a plain `Emitter` — it does NOT replay
110
+ // to listeners that subscribe later. Without this buffer, frontend writes
111
+ // arriving between `onConnection` and the forwarder subscribing (worst
112
+ // case: the whole `findPort` delay plus the socket connect time) are
113
+ // dropped silently. A frontend builds its proxy synchronously on the
114
+ // channel and may send requests immediately, so it reliably hits this
115
+ // window.
116
+ const buffered = [];
117
+ const bufferSub = channel.onMessage(provider => buffered.push(provider));
118
+ try {
119
+ const port = await this.findPort();
120
+ this.logger.info(`[${this.logComponent}] Connecting to ${this.serverName} on port ${port}...`);
121
+ await this.connectToServer(channel, port, { bufferSub, buffered });
122
+ this.logger.info(`[${this.logComponent}] Connected to ${this.serverName} on port ${port}.`);
123
+ }
124
+ catch (error) {
125
+ bufferSub.dispose();
126
+ const message = error && typeof error === 'object' && 'message' in error ? String(error.message) : String(error);
127
+ this.logger.error(`[${this.logComponent}] Could not connect to ${this.serverName}: ${message}`);
128
+ this.messageService.error(`Could not connect to ${this.serverName}: ` + message);
129
+ }
130
+ }
131
+ async findPort() {
132
+ const pendingContent = new promise_util_1.Deferred();
133
+ let counter = 0;
134
+ const tryQueryingPort = () => {
135
+ setTimeout(async () => {
136
+ try {
137
+ const port = await this.commandService.executeCommand(this.portCommand);
138
+ if (port) {
139
+ pendingContent.resolve(port);
140
+ }
141
+ }
142
+ catch (error) {
143
+ counter++;
144
+ if (this.findPortAttempts >= 0 && counter > this.findPortAttempts) {
145
+ pendingContent.reject(error);
146
+ }
147
+ else {
148
+ tryQueryingPort();
149
+ }
150
+ }
151
+ }, this.findPortTimeout);
152
+ };
153
+ tryQueryingPort();
154
+ return pendingContent.promise;
155
+ }
156
+ async connectToServer(channel, port, preForwardBuffer) {
157
+ const connected = new promise_util_1.Deferred();
158
+ const socket = new net.Socket();
159
+ this.onSocketCreated?.(socket);
160
+ socket.on('ready', () => connected.resolve());
161
+ socket.on('close', () => connected.reject(`Socket to ${this.serverName} was closed.`));
162
+ socket.on('error', error => this.logger.error(`Error occurred with the ${this.serverName} socket: ${error.name}; ${error.message}`));
163
+ // Synchronous hand-off: dispose the pre-forward buffer FIRST, then wire the
164
+ // forwarder, then replay buffered messages. The event loop cannot
165
+ // interleave between these synchronous statements, so no message is both
166
+ // buffered AND forwarded (no double delivery), and none arriving this tick
167
+ // slips through without a subscriber.
168
+ preForwardBuffer?.bufferSub.dispose();
169
+ this.forwardToSocketConnection(channel, socket);
170
+ if (preForwardBuffer && preForwardBuffer.buffered.length > 0) {
171
+ this.replayBufferedMessages(channel, preForwardBuffer.buffered);
172
+ }
173
+ if (channel instanceof channel_1.ForwardingChannel) {
174
+ socket.on('error', error => channel.onErrorEmitter.fire(error));
175
+ }
176
+ socket.connect({ port });
177
+ setTimeout(() => connected.reject('Timeout reached.'), this.connectTimeoutMs);
178
+ return connected.promise;
179
+ }
180
+ /**
181
+ * Re-fire pre-forward buffered `MessageProvider`s on the channel's internal
182
+ * `onMessageEmitter` so the now-subscribed forwarder picks them up in arrival
183
+ * order. `MessageProvider` is a thunk (`() => ReadBuffer`) — buffering does
184
+ * not consume the read position, so replay produces the same bytes the
185
+ * forwarder would have seen if wired earlier.
186
+ *
187
+ * `AbstractChannel.onMessageEmitter` is `protected` in `@theia/core` but
188
+ * reachable at runtime; the cast is the workaround. A future Theia rename
189
+ * surfaces as a clear warning here.
190
+ */
191
+ replayBufferedMessages(channel, buffered) {
192
+ if (!(channel instanceof channel_1.ForwardingChannel)) {
193
+ this.logger.warn(`[${this.logComponent}] dropping ${buffered.length} pre-forward message(s) — channel is not a ForwardingChannel`);
194
+ return;
195
+ }
196
+ const internals = channel;
197
+ if (!internals.onMessageEmitter) {
198
+ this.logger.warn(`[${this.logComponent}] dropping ${buffered.length} pre-forward message(s) — ForwardingChannel.onMessageEmitter not accessible`);
199
+ return;
200
+ }
201
+ this.logger.info(`[${this.logComponent}] replaying ${buffered.length} pre-forward message(s) onto the wired socket forwarder`);
202
+ for (const provider of buffered) {
203
+ internals.onMessageEmitter.fire(provider);
204
+ }
205
+ }
206
+ };
207
+ exports.AbstractSocketForwardingConnectionHandler = AbstractSocketForwardingConnectionHandler;
208
+ __decorate([
209
+ (0, inversify_1.inject)(core_1.MessageService),
210
+ __metadata("design:type", core_1.MessageService)
211
+ ], AbstractSocketForwardingConnectionHandler.prototype, "messageService", void 0);
212
+ __decorate([
213
+ (0, inversify_1.inject)(core_1.CommandService),
214
+ __metadata("design:type", Object)
215
+ ], AbstractSocketForwardingConnectionHandler.prototype, "commandService", void 0);
216
+ __decorate([
217
+ (0, inversify_1.inject)(core_1.ILogger),
218
+ __metadata("design:type", Object)
219
+ ], AbstractSocketForwardingConnectionHandler.prototype, "logger", void 0);
220
+ exports.AbstractSocketForwardingConnectionHandler = AbstractSocketForwardingConnectionHandler = __decorate([
221
+ (0, inversify_1.injectable)(),
222
+ __metadata("design:paramtypes", [Object])
223
+ ], AbstractSocketForwardingConnectionHandler);
224
+ //# sourceMappingURL=abstract-socket-forwarding-connection-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abstract-socket-forwarding-connection-handler.js","sourceRoot":"","sources":["../../src/node/abstract-socket-forwarding-connection-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
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
+ export * from './abstract-socket-forwarding-connection-handler';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,iDAAiD,CAAC"}
@@ -0,0 +1,26 @@
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
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ __exportStar(require("./abstract-socket-forwarding-connection-handler"), exports);
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,kFAAgE"}
@@ -0,0 +1,11 @@
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
+ export * from './stub-output-channel';
10
+ export * from './stub-inversify-context';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAKlF,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC"}
@@ -0,0 +1,29 @@
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
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ // Subpath barrel for `@hydranium/client-theia/testing` — cross-head Theia
26
+ // test doubles (`makeStubOutputChannelManager`, `makeStubInversifyContext`).
27
+ __exportStar(require("./stub-output-channel"), exports);
28
+ __exportStar(require("./stub-inversify-context"), exports);
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,0EAA0E;AAC1E,6EAA6E;AAE7E,wDAAsC;AACtC,2DAAyC"}
@@ -0,0 +1,15 @@
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
+ import { type interfaces } from '@theia/core/shared/inversify';
10
+ /** Builds the minimal slice of an Inversify Context needed by
11
+ * `getRequestParentName`: just the `currentRequest.parentRequest.bindings[0].implementationType.name`
12
+ * shape. Tests pass the desired parent class name (or undefined) and receive a
13
+ * cast context for the helper to walk. */
14
+ export declare function makeStubInversifyContext(parentClassName?: string): interfaces.Context;
15
+ //# sourceMappingURL=stub-inversify-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub-inversify-context.d.ts","sourceRoot":"","sources":["../../src/testing/stub-inversify-context.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D;;;2CAG2C;AAC3C,wBAAgB,wBAAwB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC,OAAO,CAgBrF"}
@@ -0,0 +1,32 @@
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.makeStubInversifyContext = makeStubInversifyContext;
12
+ /** Builds the minimal slice of an Inversify Context needed by
13
+ * `getRequestParentName`: just the `currentRequest.parentRequest.bindings[0].implementationType.name`
14
+ * shape. Tests pass the desired parent class name (or undefined) and receive a
15
+ * cast context for the helper to walk. */
16
+ function makeStubInversifyContext(parentClassName) {
17
+ const parentRequest = parentClassName === undefined
18
+ ? null
19
+ : {
20
+ bindings: [
21
+ {
22
+ implementationType: parentClassName === '' ? {} : { name: parentClassName }
23
+ }
24
+ ]
25
+ };
26
+ return {
27
+ currentRequest: {
28
+ parentRequest
29
+ }
30
+ };
31
+ }
32
+ //# sourceMappingURL=stub-inversify-context.js.map
@@ -0,0 +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"}
@@ -0,0 +1,25 @@
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
+ /** Minimal fake `OutputChannel` that captures appended lines into an array. */
10
+ export interface StubOutputChannel {
11
+ readonly name: string;
12
+ readonly lines: string[];
13
+ appendLine(line: string): void;
14
+ }
15
+ /**
16
+ * Minimal fake `OutputChannelManager` handing out {@link StubOutputChannel}s.
17
+ * Use in unit tests of `ChannelLogger` (and its subclasses) and of any consumer
18
+ * that injects an output channel.
19
+ */
20
+ export interface StubOutputChannelManager {
21
+ readonly channels: ReadonlyMap<string, StubOutputChannel>;
22
+ getChannel(name: string): StubOutputChannel;
23
+ }
24
+ export declare function makeStubOutputChannelManager(): StubOutputChannelManager;
25
+ //# sourceMappingURL=stub-output-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub-output-channel.d.ts","sourceRoot":"","sources":["../../src/testing/stub-output-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACtC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC1D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAAC;CAC9C;AAED,wBAAgB,4BAA4B,IAAI,wBAAwB,CAoBvE"}
@@ -0,0 +1,33 @@
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.makeStubOutputChannelManager = makeStubOutputChannelManager;
12
+ function makeStubOutputChannelManager() {
13
+ const channels = new Map();
14
+ return {
15
+ channels,
16
+ getChannel(name) {
17
+ let channel = channels.get(name);
18
+ if (!channel) {
19
+ const lines = [];
20
+ channel = {
21
+ name,
22
+ lines,
23
+ appendLine(line) {
24
+ lines.push(line);
25
+ }
26
+ };
27
+ channels.set(name, channel);
28
+ }
29
+ return channel;
30
+ }
31
+ };
32
+ }
33
+ //# sourceMappingURL=stub-output-channel.js.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@hydranium/client-theia",
3
+ "version": "1.0.0-next.10",
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
+ "keywords": [
6
+ "hydranium",
7
+ "langium",
8
+ "language-server",
9
+ "lsp",
10
+ "theia"
11
+ ],
12
+ "homepage": "https://github.com/eclipse-emfcloud/hydranium/tree/main/packages/client-theia",
13
+ "bugs": {
14
+ "url": "https://github.com/eclipse-emfcloud/hydranium/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/eclipse-emfcloud/hydranium.git",
19
+ "directory": "packages/client-theia"
20
+ },
21
+ "license": "MIT",
22
+ "author": {
23
+ "name": "Hydranium Team"
24
+ },
25
+ "sideEffects": false,
26
+ "exports": {
27
+ ".": {
28
+ "types": "./lib/index.d.ts",
29
+ "default": "./lib/index.js"
30
+ },
31
+ "./browser": {
32
+ "types": "./lib/browser/index.d.ts",
33
+ "default": "./lib/browser/index.js"
34
+ },
35
+ "./lib/browser": {
36
+ "types": "./lib/browser/index.d.ts",
37
+ "default": "./lib/browser/index.js"
38
+ },
39
+ "./node": {
40
+ "types": "./lib/node/index.d.ts",
41
+ "default": "./lib/node/index.js"
42
+ },
43
+ "./lib/node": {
44
+ "types": "./lib/node/index.d.ts",
45
+ "default": "./lib/node/index.js"
46
+ },
47
+ "./testing": {
48
+ "types": "./lib/testing/index.d.ts",
49
+ "default": "./lib/testing/index.js"
50
+ },
51
+ "./lib/testing": {
52
+ "types": "./lib/testing/index.d.ts",
53
+ "default": "./lib/testing/index.js"
54
+ }
55
+ },
56
+ "main": "lib/index.js",
57
+ "types": "lib/index.d.ts",
58
+ "files": [
59
+ "lib",
60
+ "src",
61
+ "!lib/**/*.tsbuildinfo"
62
+ ],
63
+ "scripts": {
64
+ "build": "tsc -b",
65
+ "clean": "rimraf lib tsconfig.tsbuildinfo",
66
+ "lint": "eslint src test --max-warnings 0",
67
+ "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
+ "test": "npm run typecheck:test && vitest run",
69
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
70
+ "watch": "tsc -b -w --preserveWatchOutput"
71
+ },
72
+ "devDependencies": {
73
+ "@hydranium/protocol": "1.0.0-next.10",
74
+ "@theia/core": "^1.71.0",
75
+ "@theia/output": "^1.71.0",
76
+ "@types/node": "^22.0.0",
77
+ "inversify": "6.2.2",
78
+ "reflect-metadata": "0.2.2",
79
+ "rimraf": "^5.0.0",
80
+ "typescript": "^5.8.0"
81
+ },
82
+ "peerDependencies": {
83
+ "@hydranium/protocol": "1.0.0-next.10",
84
+ "@theia/core": "^1.71.0",
85
+ "@theia/output": "^1.71.0",
86
+ "inversify": "^6.0.0"
87
+ },
88
+ "engines": {
89
+ "node": ">=22.13"
90
+ },
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."
95
+ }
@@ -0,0 +1,75 @@
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
+ /*
11
+ * Browser-origin capture — what page JS can reach without DevTools attached (it
12
+ * cannot start a V8 CPU/heap profile). Produces a single-point renderer-runtime
13
+ * report (memory, source-graded). The shared report shape lives in
14
+ * `@hydranium/protocol` so this live capture and the Playwright CDP bridge produce
15
+ * the SAME `browser-runtime.json`. The main-thread timeline is an e2e-only signal,
16
+ * captured by the CDP bridge; deep renderer CPU/heap profiling stays a manual
17
+ * DevTools activity.
18
+ */
19
+
20
+ import type { BrowserRuntimeReport } from '@hydranium/protocol';
21
+
22
+ interface MemoryCapablePerformance {
23
+ measureUserAgentSpecificMemory?: () => Promise<{ bytes: number; breakdown: unknown }>;
24
+ memory?: { usedJSHeapSize: number; totalJSHeapSize: number; jsHeapSizeLimit: number };
25
+ }
26
+
27
+ /**
28
+ * Capture the renderer runtime as a single-point {@link BrowserRuntimeReport}
29
+ * (`after`-only — the live gauge read). Prefers the standardized
30
+ * `performance.measureUserAgentSpecificMemory()` (precise, by type + container, but
31
+ * requires cross-origin isolation and is rate-limited), falling back to Chrome's
32
+ * coarse `performance.memory` (JS-heap gauge), then to `unavailable`. The two-point
33
+ * CDP source (`nodes`/`jsEventListeners`/… + delta) is the e2e bridge's job.
34
+ */
35
+ export async function captureBrowserRuntime(): Promise<BrowserRuntimeReport> {
36
+ const perf = globalThis.performance as (Performance & MemoryCapablePerformance) | undefined;
37
+ if (perf?.measureUserAgentSpecificMemory) {
38
+ try {
39
+ const result = await perf.measureUserAgentSpecificMemory();
40
+ return { source: 'measureUserAgentSpecificMemory', after: { totalBytes: result.bytes, breakdown: result.breakdown } };
41
+ } catch {
42
+ // COI missing or rate-limited — fall through to the coarse fallback.
43
+ }
44
+ }
45
+ if (perf?.memory) {
46
+ return {
47
+ source: 'performance.memory',
48
+ after: {
49
+ jsHeapUsedBytes: perf.memory.usedJSHeapSize,
50
+ jsHeapTotalBytes: perf.memory.totalJSHeapSize,
51
+ jsHeapLimitBytes: perf.memory.jsHeapSizeLimit
52
+ }
53
+ };
54
+ }
55
+ return { source: 'unavailable', after: {} };
56
+ }
57
+
58
+ function mb(bytes: number): string {
59
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
60
+ }
61
+
62
+ /** Render a {@link BrowserRuntimeReport} as a one-line summary for a channel / toast. */
63
+ export function formatBrowserRuntime(report: BrowserRuntimeReport): string {
64
+ const { after } = report;
65
+ if (report.source === 'measureUserAgentSpecificMemory' && after.totalBytes !== undefined) {
66
+ return `Browser renderer memory (precise): ${mb(after.totalBytes)} total across JS/DOM/workers.`;
67
+ }
68
+ if (report.source === 'performance.memory' && after.jsHeapUsedBytes !== undefined) {
69
+ const used = after.jsHeapUsedBytes;
70
+ const total = after.jsHeapTotalBytes ?? 0;
71
+ const limit = after.jsHeapLimitBytes ?? 0;
72
+ return `Browser JS heap: ${mb(used)} used / ${mb(total)} total / ${mb(limit)} limit (JS heap only, not the full renderer).`;
73
+ }
74
+ return 'Browser memory info unavailable (non-Chromium, or performance.memory disabled / cross-origin isolation off).';
75
+ }