@hydranium/data-client-theia 1.0.0-next.60 → 1.0.0-next.62

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.
@@ -0,0 +1,65 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+ import { type DataPort, type ResolvedMessage } from '@hydranium/protocol';
10
+ import { Emitter, MessageService, type Event } from '@theia/core';
11
+ import { type ServiceConnectionProvider } from '@theia/core/lib/browser';
12
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
13
+ import type { MessageConnection } from 'vscode-jsonrpc';
14
+ import { type ChannelConnectionHandle } from './channel-connection';
15
+ /**
16
+ * A {@link DataPort} over a Theia frontend channel.
17
+ *
18
+ * Subclasses supply {@link servicePath}; everything above the port —
19
+ * `DataConnection`, its sessions, and whatever model a widget drives — is
20
+ * host-neutral and shared with the VS Code and browser shells.
21
+ *
22
+ * Bind one per service path and in singleton scope. Theia keys a frontend
23
+ * channel by its path and refuses a second on a path already open, and the
24
+ * throw escapes the `openChannelConnection` the loser is awaiting, leaving that
25
+ * promise unsettled rather than rejected.
26
+ */
27
+ export declare abstract class ChannelDataPort implements DataPort {
28
+ protected readonly connectionProvider: ServiceConnectionProvider;
29
+ protected readonly workspaceService: WorkspaceService;
30
+ protected readonly messageService: MessageService;
31
+ /** Frontend service path the backend forwarder for this head is registered under. */
32
+ protected abstract readonly servicePath: string;
33
+ /**
34
+ * Re-open the channel when the current connection is lost. Default `true` —
35
+ * re-opening is the only thing that recovers a restarted language server,
36
+ * which binds new ephemeral ports. Turn it off for a frontend that tears
37
+ * itself down on transport loss instead.
38
+ */
39
+ protected readonly reconnectOnConnectionLoss: boolean;
40
+ protected readonly disposeEmitter: Emitter<void>;
41
+ readonly onDispose: Event<void>;
42
+ protected handle?: ChannelConnectionHandle;
43
+ /**
44
+ * Open the workspace-gated channel and hand back its listening connection.
45
+ *
46
+ * Gated because the backend forwarder discovers the head's port by executing
47
+ * a command the language server answers, and the language server only
48
+ * launches once there is a workspace to launch it for. Asking earlier polls
49
+ * a command nobody has registered.
50
+ */
51
+ connect(): Promise<MessageConnection>;
52
+ /**
53
+ * Surface a transport or write failure as a Theia notification.
54
+ *
55
+ * Swallowing it is the failure this exists to prevent: a dead connection and
56
+ * an empty document are indistinguishable in a widget.
57
+ *
58
+ * `reported` is already a complete sentence with the detail interpolated, so
59
+ * wrapping it in a sentence of the host's own would nest one owner's clause
60
+ * inside another's and leave no translator in control of the whole.
61
+ */
62
+ reportError(_error: unknown, reported: ResolvedMessage): void;
63
+ dispose(): void;
64
+ }
65
+ //# sourceMappingURL=channel-data-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-data-port.d.ts","sourceRoot":"","sources":["../../src/browser/channel-data-port.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAA0B,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAClG,OAAO,EAAE,OAAO,EAAE,cAAc,EAAO,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,KAAK,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAGzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,KAAK,uBAAuB,EAAyB,MAAM,sBAAsB,CAAC;AAG3F;;;;;;;;;;;GAWG;AACH,8BACsB,eAAgB,YAAW,QAAQ;IACpB,SAAS,CAAC,QAAQ,CAAC,kBAAkB,EAAG,yBAAyB,CAAC;IAC1E,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAG,gBAAgB,CAAC;IACzD,SAAS,CAAC,QAAQ,CAAC,cAAc,EAAG,cAAc,CAAC;IAE3E,qFAAqF;IACrF,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAEhD;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,yBAAyB,EAAE,OAAO,CAAQ;IAE7D,SAAS,CAAC,QAAQ,CAAC,cAAc,gBAAuB;IACxD,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAA6B;IAE5D,SAAS,CAAC,MAAM,CAAC,EAAE,uBAAuB,CAAC;IAE3C;;;;;;;OAOG;IACH,OAAO,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAkBrC;;;;;;;;;OASG;IACH,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI;IAI7D,OAAO,IAAI,IAAI;CAMjB"}
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
11
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
12
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
13
+ 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;
14
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
15
+ };
16
+ var __metadata = (this && this.__metadata) || function (k, v) {
17
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.ChannelDataPort = void 0;
21
+ const protocol_1 = require("@hydranium/protocol");
22
+ const core_1 = require("@theia/core");
23
+ const service_connection_provider_1 = require("@theia/core/lib/browser/messaging/service-connection-provider");
24
+ const inversify_1 = require("@theia/core/shared/inversify");
25
+ const browser_1 = require("@theia/workspace/lib/browser");
26
+ const channel_connection_1 = require("./channel-connection");
27
+ const workspace_gate_1 = require("./workspace-gate");
28
+ /**
29
+ * A {@link DataPort} over a Theia frontend channel.
30
+ *
31
+ * Subclasses supply {@link servicePath}; everything above the port —
32
+ * `DataConnection`, its sessions, and whatever model a widget drives — is
33
+ * host-neutral and shared with the VS Code and browser shells.
34
+ *
35
+ * Bind one per service path and in singleton scope. Theia keys a frontend
36
+ * channel by its path and refuses a second on a path already open, and the
37
+ * throw escapes the `openChannelConnection` the loser is awaiting, leaving that
38
+ * promise unsettled rather than rejected.
39
+ */
40
+ let ChannelDataPort = class ChannelDataPort {
41
+ connectionProvider;
42
+ workspaceService;
43
+ messageService;
44
+ /**
45
+ * Re-open the channel when the current connection is lost. Default `true` —
46
+ * re-opening is the only thing that recovers a restarted language server,
47
+ * which binds new ephemeral ports. Turn it off for a frontend that tears
48
+ * itself down on transport loss instead.
49
+ */
50
+ reconnectOnConnectionLoss = true;
51
+ disposeEmitter = new core_1.Emitter();
52
+ onDispose = this.disposeEmitter.event;
53
+ handle;
54
+ /**
55
+ * Open the workspace-gated channel and hand back its listening connection.
56
+ *
57
+ * Gated because the backend forwarder discovers the head's port by executing
58
+ * a command the language server answers, and the language server only
59
+ * launches once there is a workspace to launch it for. Asking earlier polls
60
+ * a command nobody has registered.
61
+ */
62
+ connect() {
63
+ // Read per call rather than cached: `current` is repointed at a fresh
64
+ // promise every time the channel is re-opened, so reading through the
65
+ // handle is what makes a later generation reach the live server.
66
+ if (!this.handle) {
67
+ this.handle = (0, channel_connection_1.openChannelConnection)(this.connectionProvider, this.servicePath, {
68
+ whenReady: (0, workspace_gate_1.whenWorkspaceOpen)(this.workspaceService),
69
+ reconnect: this.reconnectOnConnectionLoss
70
+ });
71
+ // A relaunched server binds new ephemeral ports; the handle re-opens
72
+ // and the forwarder rediscovers, but `DataConnection` caches its
73
+ // generation and would go on addressing the dead one. Its signal to
74
+ // drop that generation is this event.
75
+ this.handle.onDidLoseConnection(() => this.disposeEmitter.fire(undefined));
76
+ }
77
+ return this.handle.current;
78
+ }
79
+ /**
80
+ * Surface a transport or write failure as a Theia notification.
81
+ *
82
+ * Swallowing it is the failure this exists to prevent: a dead connection and
83
+ * an empty document are indistinguishable in a widget.
84
+ *
85
+ * `reported` is already a complete sentence with the detail interpolated, so
86
+ * wrapping it in a sentence of the host's own would nest one owner's clause
87
+ * inside another's and leave no translator in control of the whole.
88
+ */
89
+ reportError(_error, reported) {
90
+ this.messageService.error((0, protocol_1.renderFrameworkMessage)(reported, core_1.nls.localization?.translations));
91
+ }
92
+ dispose() {
93
+ this.handle?.dispose();
94
+ this.handle = undefined;
95
+ this.disposeEmitter.fire(undefined);
96
+ this.disposeEmitter.dispose();
97
+ }
98
+ };
99
+ exports.ChannelDataPort = ChannelDataPort;
100
+ __decorate([
101
+ (0, inversify_1.inject)(service_connection_provider_1.RemoteConnectionProvider),
102
+ __metadata("design:type", Function)
103
+ ], ChannelDataPort.prototype, "connectionProvider", void 0);
104
+ __decorate([
105
+ (0, inversify_1.inject)(browser_1.WorkspaceService),
106
+ __metadata("design:type", browser_1.WorkspaceService)
107
+ ], ChannelDataPort.prototype, "workspaceService", void 0);
108
+ __decorate([
109
+ (0, inversify_1.inject)(core_1.MessageService),
110
+ __metadata("design:type", core_1.MessageService)
111
+ ], ChannelDataPort.prototype, "messageService", void 0);
112
+ exports.ChannelDataPort = ChannelDataPort = __decorate([
113
+ (0, inversify_1.injectable)()
114
+ ], ChannelDataPort);
115
+ //# sourceMappingURL=channel-data-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-data-port.js","sourceRoot":"","sources":["../../src/browser/channel-data-port.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;AAElF,kDAAkG;AAClG,sCAAuE;AAEvE,+GAAyG;AACzG,4DAAkE;AAClE,0DAAgE;AAEhE,6DAA2F;AAC3F,qDAAqD;AAErD;;;;;;;;;;;GAWG;AAEI,IAAe,eAAe,GAA9B,MAAe,eAAe;IACmB,kBAAkB,CAA6B;IACvD,gBAAgB,CAAoB;IACtC,cAAc,CAAkB;IAK3E;;;;;OAKG;IACgB,yBAAyB,GAAY,IAAI,CAAC;IAE1C,cAAc,GAAG,IAAI,cAAO,EAAQ,CAAC;IAC/C,SAAS,GAAgB,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;IAElD,MAAM,CAA2B;IAE3C;;;;;;;OAOG;IACH,OAAO;QACJ,sEAAsE;QACtE,sEAAsE;QACtE,iEAAiE;QACjE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,GAAG,IAAA,0CAAqB,EAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,WAAW,EAAE;gBAC5E,SAAS,EAAE,IAAA,kCAAiB,EAAC,IAAI,CAAC,gBAAgB,CAAC;gBACnD,SAAS,EAAE,IAAI,CAAC,yBAAyB;aAC3C,CAAC,CAAC;YACH,qEAAqE;YACrE,iEAAiE;YACjE,oEAAoE;YACpE,sCAAsC;YACtC,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;IAC9B,CAAC;IAED;;;;;;;;;OASG;IACH,WAAW,CAAC,MAAe,EAAE,QAAyB;QACnD,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAA,iCAAsB,EAAC,QAAQ,EAAE,UAAG,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;IAC/F,CAAC;IAED,OAAO;QACJ,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;IACjC,CAAC;CACH,CAAA;AAnEqB,0CAAe;AACmB;IAApD,IAAA,kBAAM,EAAC,sDAAwB,CAAC;;2DAAmE;AACvD;IAA5C,IAAA,kBAAM,EAAC,0BAAgB,CAAC;8BAAuC,0BAAgB;yDAAC;AACtC;IAA1C,IAAA,kBAAM,EAAC,qBAAc,CAAC;8BAAqC,qBAAc;uDAAC;0BAHxD,eAAe;IADpC,IAAA,sBAAU,GAAE;GACS,eAAe,CAmEpC"}
@@ -6,10 +6,10 @@
6
6
  *
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
+ import { type DataPort, type ReadyServer, type ResolvedMessage, RpcConnection, type RpcProxy } from '@hydranium/protocol';
10
+ import { Emitter, type MessageService } from '@theia/core';
9
11
  import type { ServiceConnectionProvider } from '@theia/core/lib/browser';
10
- import { Deferred } from '@theia/core/lib/common/promise-util';
11
12
  import type { WorkspaceService } from '@theia/workspace/lib/browser';
12
- import type { MessageConnection } from 'vscode-jsonrpc';
13
13
  import { type ChannelConnectionHandle } from './channel-connection';
14
14
  /**
15
15
  * Base for a Theia frontend that owns the data-server vscode-jsonrpc
@@ -24,109 +24,64 @@ import { type ChannelConnectionHandle } from './channel-connection';
24
24
  * the local notification target `TClient`. A subclass supplies the abstract
25
25
  * members below, calls {@link start} from its `@postConstruct`, and awaits
26
26
  * {@link ensureConnected} before its first `this.server.*` call.
27
+ *
28
+ * The lifecycle is `RpcConnection`'s — the same proxy, readiness gate and
29
+ * reconnect generation the host-neutral tier uses. What stays here is the Theia
30
+ * half: the channel, the workspace gate and the notification sink.
27
31
  */
28
- export declare abstract class AbstractDataServiceFrontend<TServer extends {
29
- waitForReady(): Promise<void>;
30
- }, TClient extends object> {
31
- /**
32
- * The workspace-gated connection to the backend forwarder. Set by
33
- * {@link start}, and REPLACED whenever the connection is lost and
34
- * {@link reconnectOnConnectionLoss} is on — so read it per use and never
35
- * cache the resolved connection.
36
- */
37
- protected connectionPromise: Promise<MessageConnection>;
38
- /**
39
- * Typed proxy over {@link connectionPromise}, addressing the server under
40
- * {@link methodNamespace}. Set by {@link start}, and replaced alongside
41
- * {@link connectionPromise} on reconnect.
42
- *
43
- * `createRpcProxy` resolves the connection promise once and binds to it for
44
- * good, so a reconnect necessarily means a new proxy. Reading
45
- * `this.server.foo()` per call — rather than hoisting `this.server` into a
46
- * local or a constructor-time field — is what keeps a subclass correct
47
- * across one.
48
- */
49
- protected server: TServer;
50
- /** The channel handle {@link start} opened; owns reconnect and disposal. */
51
- protected channel?: ChannelConnectionHandle;
52
- /** Shared init Deferred so concurrent {@link ensureConnected} callers await one initialization. */
53
- protected initialized?: Deferred<void>;
54
- /** Theia connection provider the channel is opened through. */
32
+ export declare abstract class AbstractDataServiceFrontend<TServer extends ReadyServer, TClient extends object> {
55
33
  protected abstract readonly connectionProvider: ServiceConnectionProvider;
56
- /**
57
- * Optional workspace service. When provided, the default
58
- * {@link connectionReadyGate} waits for a workspace before opening the
59
- * channel; a head that is not workspace-scoped omits it (and may override
60
- * {@link connectionReadyGate} for a different gate).
61
- */
62
34
  protected abstract readonly workspaceService?: WorkspaceService;
63
- /** Local inbound-notification target bound on the connection (the `localTarget`). */
64
35
  protected abstract readonly client: TClient;
65
- /**
66
- * Theia service path the backend forwarder is registered under.
67
- *
68
- * **Unique per frontend, not per server.** Theia keys a frontend channel by
69
- * this path and throws `Another channel with the id '<path>' is already open`
70
- * on a second opener — so a subclass sharing the framework default with any
71
- * other consumer of the same head (a host-neutral `DataPort`, a sibling
72
- * service frontend) breaks whichever opens second. The failure is remote from
73
- * its cause: the throw escapes an `openChannelConnection` the other consumer
74
- * awaited, leaving its request permanently unsettled rather than rejected,
75
- * which presents as a view stuck on its loading state with a clean server
76
- * log. Give each frontend its own path and register a forwarder per path;
77
- * they still reach one server, since the shared `portCommand` is what names
78
- * the process.
79
- */
36
+ /** Frontend service path the backend forwarder for this head is registered under. */
80
37
  protected abstract readonly servicePath: string;
81
- /** Wire namespace the server + client methods are addressed under. */
82
38
  protected abstract readonly methodNamespace: string;
83
39
  /** Allowlist of {@link client} methods to bind as inbound handlers. */
84
40
  protected abstract readonly clientMethods: readonly (keyof TClient & string)[];
41
+ /** Surfaces a transport failure. Optional: a frontend with no UI of its own omits it. */
42
+ protected readonly messageService?: MessageService;
85
43
  /**
86
- * Rebuild the connection and the proxy when the current connection is lost,
87
- * and re-run initialization against the replacement. Defaults to `true`
88
- * see `OpenChannelConnectionOptions.reconnect` for why re-opening the
89
- * channel is the only thing that recovers a restarted language server, and
90
- * why a dead connection leaves no alternative worth preserving.
91
- *
92
- * The subclass-facing cost is that {@link doInitialize} runs again per
93
- * connection, so any progress UI it drives reappears. Turn this off for a
94
- * frontend that would rather show nothing than show its warm-up twice, or
95
- * that tears itself down on transport loss.
44
+ * Rebuild the connection when the current one is lost. Defaults to `true` —
45
+ * re-opening the channel is the only thing that recovers a restarted
46
+ * language server, which binds new ephemeral ports. Turn it off for a
47
+ * frontend that would rather tear itself down than show its warm-up twice.
96
48
  */
97
49
  protected readonly reconnectOnConnectionLoss: boolean;
50
+ protected channel?: ChannelConnectionHandle;
51
+ protected connection?: RpcConnection<TServer, TClient>;
52
+ protected readonly lossEmitter: Emitter<void>;
98
53
  /**
99
54
  * Readiness gate for the connection — the channel opens only once the
100
55
  * returned promise settles. Default: waits for a workspace when
101
- * {@link workspaceService} is provided, otherwise opens immediately
102
- * (`undefined`). Override for a different gate (e.g. a fixed model store
103
- * that is always ready, or a custom warm-up).
56
+ * {@link workspaceService} is provided, otherwise opens immediately.
104
57
  */
105
58
  protected connectionReadyGate(): Promise<void> | undefined;
59
+ /** A connection generation is opening, including on each reconnect. */
60
+ protected onConnecting(): void;
61
+ /** The server's readiness gate has settled for a generation. */
62
+ protected onReady(): void;
63
+ /** A generation failed; the awaiting caller still rejects. */
64
+ protected onFailed(_error: unknown): void;
106
65
  /**
107
- * Open the connection (workspace-gated by default via
108
- * {@link connectionReadyGate}) and build the combined server proxy +
109
- * inbound client binding. Call once (typically from the adopter's
110
- * `@postConstruct`). Outbound calls + inbound notifications queue over the
111
- * connection promise until the channel is live.
66
+ * Open the channel and build the connection over it. Call once, typically
67
+ * from the adopter's `@postConstruct`. Outbound calls and inbound
68
+ * notifications queue until the channel is live.
112
69
  */
113
70
  protected start(): void;
71
+ /** The channel as a {@link DataPort} — the whole Theia-specific half. */
72
+ protected channelPort(): DataPort;
73
+ /** Surface a transport failure the way this host does. */
74
+ protected reportError(reported: ResolvedMessage): void;
114
75
  /**
115
- * Point {@link connectionPromise} and {@link server} at the channel's
116
- * current connection. Called by {@link start} and again per reconnect.
76
+ * The server proxy. Calls queue against the connection, so await
77
+ * {@link ensureConnected} first wherever the server's readiness matters.
117
78
  */
118
- protected bindConnection(): void;
79
+ protected get server(): RpcProxy<TServer>;
119
80
  /**
120
- * Rebind onto the replacement connection and arm initialization to run again.
121
- *
122
- * Clearing {@link initialized} is the load-bearing half. A restarted server
123
- * has an unwarmed workspace, so its `waitForReady` gate has to be awaited
124
- * afresh; leaving the old resolved Deferred in place would let the first
125
- * request after a restart through against a server still walking the
126
- * workspace, and be answered correctly from an empty registry — which reads
127
- * as data loss rather than as a race.
81
+ * Await the connection and the server's readiness gate, shared across
82
+ * concurrent callers and re-run once per connection generation.
128
83
  */
129
- protected handleConnectionLost(): void;
84
+ protected ensureConnected(): Promise<void>;
130
85
  /**
131
86
  * Release the connection and stop tracking the channel. Idempotent.
132
87
  *
@@ -135,20 +90,7 @@ export declare abstract class AbstractDataServiceFrontend<TServer extends {
135
90
  * lifecycle of its own.
136
91
  */
137
92
  dispose(): void;
138
- /**
139
- * Lazily drive initialization, shared across concurrent callers via one
140
- * {@link Deferred}. Request methods `await this.ensureConnected()` before
141
- * their first `this.server.*` call.
142
- */
143
- protected ensureConnected(): Promise<void>;
144
- /**
145
- * Default initialization: await the connection, await the server's readiness
146
- * gate, then resolve the passed Deferred. Initialization completion is
147
- * observable by awaiting {@link ensureConnected} (which returns this same
148
- * Deferred's promise) — there is no separate post-init hook. Override
149
- * wholesale to interleave progress UI / extra warm-up; an override owns
150
- * resolving/rejecting `initialized` (there is no `super` step to call).
151
- */
152
- protected doInitialize(initialized: Deferred<void>): Promise<void>;
93
+ protected requireConnection(): RpcConnection<TServer, TClient>;
94
+ protected requireChannel(): ChannelConnectionHandle;
153
95
  }
154
96
  //# sourceMappingURL=data-service-frontend.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"data-service-frontend.d.ts","sourceRoot":"","sources":["../../src/browser/data-service-frontend.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAGlF,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,KAAK,uBAAuB,EAAyB,MAAM,sBAAsB,CAAC;AAG3F;;;;;;;;;;;;;GAaG;AACH,8BAAsB,2BAA2B,CAAC,OAAO,SAAS;IAAE,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,EAAE,OAAO,SAAS,MAAM;IACxH;;;;;OAKG;IACH,SAAS,CAAC,iBAAiB,EAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzD;;;;;;;;;;OAUG;IACH,SAAS,CAAC,MAAM,EAAG,OAAO,CAAC;IAC3B,4EAA4E;IAC5E,SAAS,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAC5C,mGAAmG;IACnG,SAAS,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IAEvC,+DAA+D;IAC/D,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,kBAAkB,EAAE,yBAAyB,CAAC;IAC1E;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAChE,qFAAqF;IACrF,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5C;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAChD,sEAAsE;IACtE,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACpD,uEAAuE;IACvE,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,EAAE,SAAS,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC;IAE/E;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,QAAQ,CAAC,yBAAyB,EAAE,OAAO,CAAQ;IAE7D;;;;;;OAMG;IACH,SAAS,CAAC,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;IAI1D;;;;;;OAMG;IACH,SAAS,CAAC,KAAK,IAAI,IAAI;IAavB;;;OAGG;IACH,SAAS,CAAC,cAAc,IAAI,IAAI;IAYhC;;;;;;;;;OASG;IACH,SAAS,CAAC,oBAAoB,IAAI,IAAI;IAKtC;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAMf;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ1C;;;;;;;OAOG;cACa,YAAY,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAS1E"}
1
+ {"version":3,"file":"data-service-frontend.d.ts","sourceRoot":"","sources":["../../src/browser/data-service-frontend.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EACJ,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,aAAa,EACb,KAAK,QAAQ,EAEf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAO,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,KAAK,uBAAuB,EAAyB,MAAM,sBAAsB,CAAC;AAG3F;;;;;;;;;;;;;;;;;GAiBG;AACH,8BAAsB,2BAA2B,CAAC,OAAO,SAAS,WAAW,EAAE,OAAO,SAAS,MAAM;IAClG,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,kBAAkB,EAAE,yBAAyB,CAAC;IAC1E,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAChE,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5C,qFAAqF;IACrF,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAChD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACpD,uEAAuE;IACvE,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,EAAE,SAAS,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC;IAE/E,yFAAyF;IACzF,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IAEnD;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,yBAAyB,EAAE,OAAO,CAAQ;IAE7D,SAAS,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAC5C,SAAS,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACvD,SAAS,CAAC,QAAQ,CAAC,WAAW,gBAAuB;IAErD;;;;OAIG;IACH,SAAS,CAAC,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;IAI1D,uEAAuE;IACvE,SAAS,CAAC,YAAY,IAAI,IAAI;IAI9B,gEAAgE;IAChE,SAAS,CAAC,OAAO,IAAI,IAAI;IAIzB,8DAA8D;IAC9D,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAIzC;;;;OAIG;IACH,SAAS,CAAC,KAAK,IAAI,IAAI;IAoBvB,yEAAyE;IACzE,SAAS,CAAC,WAAW,IAAI,QAAQ;IAWjC,0DAA0D;IAC1D,SAAS,CAAC,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAItD;;;OAGG;IACH,SAAS,KAAK,MAAM,IAAI,QAAQ,CAAC,OAAO,CAAC,CAExC;IAED;;;OAGG;cACa,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAIhD;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAQf,SAAS,CAAC,iBAAiB,IAAI,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC;IAO9D,SAAS,CAAC,cAAc,IAAI,uBAAuB;CAMrD"}
@@ -10,7 +10,7 @@
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.AbstractDataServiceFrontend = void 0;
12
12
  const protocol_1 = require("@hydranium/protocol");
13
- const promise_util_1 = require("@theia/core/lib/common/promise-util");
13
+ const core_1 = require("@theia/core");
14
14
  const channel_connection_1 = require("./channel-connection");
15
15
  const workspace_gate_1 = require("./workspace-gate");
16
16
  /**
@@ -26,101 +26,96 @@ const workspace_gate_1 = require("./workspace-gate");
26
26
  * the local notification target `TClient`. A subclass supplies the abstract
27
27
  * members below, calls {@link start} from its `@postConstruct`, and awaits
28
28
  * {@link ensureConnected} before its first `this.server.*` call.
29
+ *
30
+ * The lifecycle is `RpcConnection`'s — the same proxy, readiness gate and
31
+ * reconnect generation the host-neutral tier uses. What stays here is the Theia
32
+ * half: the channel, the workspace gate and the notification sink.
29
33
  */
30
34
  class AbstractDataServiceFrontend {
35
+ /** Surfaces a transport failure. Optional: a frontend with no UI of its own omits it. */
36
+ messageService;
31
37
  /**
32
- * The workspace-gated connection to the backend forwarder. Set by
33
- * {@link start}, and REPLACED whenever the connection is lost and
34
- * {@link reconnectOnConnectionLoss} is on so read it per use and never
35
- * cache the resolved connection.
36
- */
37
- connectionPromise;
38
- /**
39
- * Typed proxy over {@link connectionPromise}, addressing the server under
40
- * {@link methodNamespace}. Set by {@link start}, and replaced alongside
41
- * {@link connectionPromise} on reconnect.
42
- *
43
- * `createRpcProxy` resolves the connection promise once and binds to it for
44
- * good, so a reconnect necessarily means a new proxy. Reading
45
- * `this.server.foo()` per call — rather than hoisting `this.server` into a
46
- * local or a constructor-time field — is what keeps a subclass correct
47
- * across one.
48
- */
49
- server;
50
- /** The channel handle {@link start} opened; owns reconnect and disposal. */
51
- channel;
52
- /** Shared init Deferred so concurrent {@link ensureConnected} callers await one initialization. */
53
- initialized;
54
- /**
55
- * Rebuild the connection and the proxy when the current connection is lost,
56
- * and re-run initialization against the replacement. Defaults to `true` —
57
- * see `OpenChannelConnectionOptions.reconnect` for why re-opening the
58
- * channel is the only thing that recovers a restarted language server, and
59
- * why a dead connection leaves no alternative worth preserving.
60
- *
61
- * The subclass-facing cost is that {@link doInitialize} runs again per
62
- * connection, so any progress UI it drives reappears. Turn this off for a
63
- * frontend that would rather show nothing than show its warm-up twice, or
64
- * that tears itself down on transport loss.
38
+ * Rebuild the connection when the current one is lost. Defaults to `true` —
39
+ * re-opening the channel is the only thing that recovers a restarted
40
+ * language server, which binds new ephemeral ports. Turn it off for a
41
+ * frontend that would rather tear itself down than show its warm-up twice.
65
42
  */
66
43
  reconnectOnConnectionLoss = true;
44
+ channel;
45
+ connection;
46
+ lossEmitter = new core_1.Emitter();
67
47
  /**
68
48
  * Readiness gate for the connection — the channel opens only once the
69
49
  * returned promise settles. Default: waits for a workspace when
70
- * {@link workspaceService} is provided, otherwise opens immediately
71
- * (`undefined`). Override for a different gate (e.g. a fixed model store
72
- * that is always ready, or a custom warm-up).
50
+ * {@link workspaceService} is provided, otherwise opens immediately.
73
51
  */
74
52
  connectionReadyGate() {
75
53
  return this.workspaceService ? (0, workspace_gate_1.whenWorkspaceOpen)(this.workspaceService) : undefined;
76
54
  }
55
+ /** A connection generation is opening, including on each reconnect. */
56
+ onConnecting() {
57
+ // nothing by default
58
+ }
59
+ /** The server's readiness gate has settled for a generation. */
60
+ onReady() {
61
+ // nothing by default
62
+ }
63
+ /** A generation failed; the awaiting caller still rejects. */
64
+ onFailed(_error) {
65
+ // nothing by default
66
+ }
77
67
  /**
78
- * Open the connection (workspace-gated by default via
79
- * {@link connectionReadyGate}) and build the combined server proxy +
80
- * inbound client binding. Call once (typically from the adopter's
81
- * `@postConstruct`). Outbound calls + inbound notifications queue over the
82
- * connection promise until the channel is live.
68
+ * Open the channel and build the connection over it. Call once, typically
69
+ * from the adopter's `@postConstruct`. Outbound calls and inbound
70
+ * notifications queue until the channel is live.
83
71
  */
84
72
  start() {
85
73
  this.channel = (0, channel_connection_1.openChannelConnection)(this.connectionProvider, this.servicePath, {
86
74
  whenReady: this.connectionReadyGate(),
87
75
  reconnect: this.reconnectOnConnectionLoss
88
76
  });
89
- this.bindConnection();
90
- // The LOSS, not the replacement's arrival: rebinding when the channel
91
- // closes points `server` at the queueing replacement promise, so a request
92
- // made during the gap waits for the new server instead of being addressed
93
- // at the dead one and never settling.
94
- this.channel.onDidLoseConnection(() => this.handleConnectionLost());
77
+ // The LOSS, not the replacement's arrival: the connection drops its
78
+ // generation here, so a request made during the gap waits for the fresh
79
+ // one instead of addressing the dead one and never settling.
80
+ this.channel.onDidLoseConnection(() => this.lossEmitter.fire(undefined));
81
+ this.connection = new protocol_1.RpcConnection(this.channelPort(), this.client, {
82
+ methodNamespace: this.methodNamespace,
83
+ clientMethods: this.clientMethods,
84
+ lifecycle: {
85
+ onConnecting: () => this.onConnecting(),
86
+ onReady: () => this.onReady(),
87
+ onFailed: error => this.onFailed(error)
88
+ }
89
+ });
90
+ }
91
+ /** The channel as a {@link DataPort} — the whole Theia-specific half. */
92
+ channelPort() {
93
+ return {
94
+ // Read per call: `current` is repointed on every re-open, so reaching
95
+ // through the handle is what makes a later generation find the live
96
+ // server.
97
+ connect: () => this.requireChannel().current,
98
+ reportError: (_error, reported) => this.reportError(reported),
99
+ onDispose: this.lossEmitter.event
100
+ };
101
+ }
102
+ /** Surface a transport failure the way this host does. */
103
+ reportError(reported) {
104
+ this.messageService?.error((0, protocol_1.renderFrameworkMessage)(reported, core_1.nls.localization?.translations));
95
105
  }
96
106
  /**
97
- * Point {@link connectionPromise} and {@link server} at the channel's
98
- * current connection. Called by {@link start} and again per reconnect.
107
+ * The server proxy. Calls queue against the connection, so await
108
+ * {@link ensureConnected} first wherever the server's readiness matters.
99
109
  */
100
- bindConnection() {
101
- if (!this.channel) {
102
- throw new Error('bindConnection called before start');
103
- }
104
- this.connectionPromise = this.channel.current;
105
- this.server = (0, protocol_1.createRpcProxy)(this.connectionPromise, {
106
- methodNamespace: this.methodNamespace,
107
- localTarget: this.client,
108
- localMethods: this.clientMethods
109
- });
110
+ get server() {
111
+ return this.requireConnection().server;
110
112
  }
111
113
  /**
112
- * Rebind onto the replacement connection and arm initialization to run again.
113
- *
114
- * Clearing {@link initialized} is the load-bearing half. A restarted server
115
- * has an unwarmed workspace, so its `waitForReady` gate has to be awaited
116
- * afresh; leaving the old resolved Deferred in place would let the first
117
- * request after a restart through against a server still walking the
118
- * workspace, and be answered correctly from an empty registry — which reads
119
- * as data loss rather than as a race.
114
+ * Await the connection and the server's readiness gate, shared across
115
+ * concurrent callers and re-run once per connection generation.
120
116
  */
121
- handleConnectionLost() {
122
- this.initialized = undefined;
123
- this.bindConnection();
117
+ async ensureConnected() {
118
+ await this.requireConnection().connected();
124
119
  }
125
120
  /**
126
121
  * Release the connection and stop tracking the channel. Idempotent.
@@ -130,39 +125,23 @@ class AbstractDataServiceFrontend {
130
125
  * lifecycle of its own.
131
126
  */
132
127
  dispose() {
128
+ this.connection?.dispose();
129
+ this.connection = undefined;
133
130
  this.channel?.dispose();
134
131
  this.channel = undefined;
135
- this.initialized = undefined;
132
+ this.lossEmitter.dispose();
136
133
  }
137
- /**
138
- * Lazily drive initialization, shared across concurrent callers via one
139
- * {@link Deferred}. Request methods `await this.ensureConnected()` before
140
- * their first `this.server.*` call.
141
- */
142
- ensureConnected() {
143
- if (!this.initialized) {
144
- this.initialized = new promise_util_1.Deferred();
145
- void this.doInitialize(this.initialized);
134
+ requireConnection() {
135
+ if (!this.connection) {
136
+ throw new Error('the connection is not open: call start() first');
146
137
  }
147
- return this.initialized.promise;
138
+ return this.connection;
148
139
  }
149
- /**
150
- * Default initialization: await the connection, await the server's readiness
151
- * gate, then resolve the passed Deferred. Initialization completion is
152
- * observable by awaiting {@link ensureConnected} (which returns this same
153
- * Deferred's promise) — there is no separate post-init hook. Override
154
- * wholesale to interleave progress UI / extra warm-up; an override owns
155
- * resolving/rejecting `initialized` (there is no `super` step to call).
156
- */
157
- async doInitialize(initialized) {
158
- try {
159
- await this.connectionPromise;
160
- await this.server.waitForReady();
161
- initialized.resolve();
162
- }
163
- catch (error) {
164
- initialized.reject(error instanceof Error ? error : new Error(String(error)));
140
+ requireChannel() {
141
+ if (!this.channel) {
142
+ throw new Error('the channel is not open: call start() first');
165
143
  }
144
+ return this.channel;
166
145
  }
167
146
  }
168
147
  exports.AbstractDataServiceFrontend = AbstractDataServiceFrontend;
@@ -1 +1 @@
1
- {"version":3,"file":"data-service-frontend.js","sourceRoot":"","sources":["../../src/browser/data-service-frontend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"data-service-frontend.js","sourceRoot":"","sources":["../../src/browser/data-service-frontend.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
  export * from './channel-connection';
10
+ export * from './channel-data-port';
10
11
  export * from './data-service-frontend';
11
12
  export * from './diagnostics-data-service-frontend';
12
13
  export * from './host-diagnostics-frontend';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qCAAqC,CAAC;AACpD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qCAAqC,CAAC;AACpD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kBAAkB,CAAC"}
@@ -23,6 +23,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  __exportStar(require("./channel-connection"), exports);
26
+ __exportStar(require("./channel-data-port"), exports);
26
27
  __exportStar(require("./data-service-frontend"), exports);
27
28
  __exportStar(require("./diagnostics-data-service-frontend"), exports);
28
29
  __exportStar(require("./host-diagnostics-frontend"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,uDAAqC;AACrC,0DAAwC;AACxC,sEAAoD;AACpD,8DAA4C;AAC5C,qEAAmD;AACnD,mDAAiC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,uDAAqC;AACrC,sDAAoC;AACpC,0DAAwC;AACxC,sEAAoD;AACpD,8DAA4C;AAC5C,qEAAmD;AACnD,mDAAiC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydranium/data-client-theia",
3
- "version": "1.0.0-next.60",
3
+ "version": "1.0.0-next.62",
4
4
  "description": "Theia-coupled client primitives for the hydranium data-server protocol head; companion of @hydranium/data-server. Bridges framework typed errors (e.g. ConflictError) across Theia's RPC boundary so their code + data survive the wire.",
5
5
  "keywords": [
6
6
  "hydranium",
@@ -70,9 +70,9 @@
70
70
  "watch": "tsc -b -w --preserveWatchOutput"
71
71
  },
72
72
  "devDependencies": {
73
- "@hydranium/client-theia": "1.0.0-next.60",
74
- "@hydranium/core": "1.0.0-next.60",
75
- "@hydranium/protocol": "1.0.0-next.60",
73
+ "@hydranium/client-theia": "1.0.0-next.62",
74
+ "@hydranium/core": "1.0.0-next.62",
75
+ "@hydranium/protocol": "1.0.0-next.62",
76
76
  "@theia/core": "^1.70.0",
77
77
  "@theia/workspace": "^1.70.0",
78
78
  "@types/node": "^22.0.0",
@@ -83,9 +83,9 @@
83
83
  "vscode-jsonrpc": "9.0.1"
84
84
  },
85
85
  "peerDependencies": {
86
- "@hydranium/client-theia": "1.0.0-next.60",
87
- "@hydranium/core": "1.0.0-next.60",
88
- "@hydranium/protocol": "1.0.0-next.60",
86
+ "@hydranium/client-theia": "1.0.0-next.62",
87
+ "@hydranium/core": "1.0.0-next.62",
88
+ "@hydranium/protocol": "1.0.0-next.62",
89
89
  "@theia/core": "^1.70.0",
90
90
  "@theia/workspace": "^1.70.0",
91
91
  "inversify": "^6.0.0",
@@ -0,0 +1,100 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import { renderFrameworkMessage, type DataPort, type ResolvedMessage } from '@hydranium/protocol';
11
+ import { Emitter, MessageService, nls, type Event } from '@theia/core';
12
+ import { type ServiceConnectionProvider } from '@theia/core/lib/browser';
13
+ import { RemoteConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
14
+ import { inject, injectable } from '@theia/core/shared/inversify';
15
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
16
+ import type { MessageConnection } from 'vscode-jsonrpc';
17
+ import { type ChannelConnectionHandle, openChannelConnection } from './channel-connection';
18
+ import { whenWorkspaceOpen } from './workspace-gate';
19
+
20
+ /**
21
+ * A {@link DataPort} over a Theia frontend channel.
22
+ *
23
+ * Subclasses supply {@link servicePath}; everything above the port —
24
+ * `DataConnection`, its sessions, and whatever model a widget drives — is
25
+ * host-neutral and shared with the VS Code and browser shells.
26
+ *
27
+ * Bind one per service path and in singleton scope. Theia keys a frontend
28
+ * channel by its path and refuses a second on a path already open, and the
29
+ * throw escapes the `openChannelConnection` the loser is awaiting, leaving that
30
+ * promise unsettled rather than rejected.
31
+ */
32
+ @injectable()
33
+ export abstract class ChannelDataPort implements DataPort {
34
+ @inject(RemoteConnectionProvider) protected readonly connectionProvider!: ServiceConnectionProvider;
35
+ @inject(WorkspaceService) protected readonly workspaceService!: WorkspaceService;
36
+ @inject(MessageService) protected readonly messageService!: MessageService;
37
+
38
+ /** Frontend service path the backend forwarder for this head is registered under. */
39
+ protected abstract readonly servicePath: string;
40
+
41
+ /**
42
+ * Re-open the channel when the current connection is lost. Default `true` —
43
+ * re-opening is the only thing that recovers a restarted language server,
44
+ * which binds new ephemeral ports. Turn it off for a frontend that tears
45
+ * itself down on transport loss instead.
46
+ */
47
+ protected readonly reconnectOnConnectionLoss: boolean = true;
48
+
49
+ protected readonly disposeEmitter = new Emitter<void>();
50
+ readonly onDispose: Event<void> = this.disposeEmitter.event;
51
+
52
+ protected handle?: ChannelConnectionHandle;
53
+
54
+ /**
55
+ * Open the workspace-gated channel and hand back its listening connection.
56
+ *
57
+ * Gated because the backend forwarder discovers the head's port by executing
58
+ * a command the language server answers, and the language server only
59
+ * launches once there is a workspace to launch it for. Asking earlier polls
60
+ * a command nobody has registered.
61
+ */
62
+ connect(): Promise<MessageConnection> {
63
+ // Read per call rather than cached: `current` is repointed at a fresh
64
+ // promise every time the channel is re-opened, so reading through the
65
+ // handle is what makes a later generation reach the live server.
66
+ if (!this.handle) {
67
+ this.handle = openChannelConnection(this.connectionProvider, this.servicePath, {
68
+ whenReady: whenWorkspaceOpen(this.workspaceService),
69
+ reconnect: this.reconnectOnConnectionLoss
70
+ });
71
+ // A relaunched server binds new ephemeral ports; the handle re-opens
72
+ // and the forwarder rediscovers, but `DataConnection` caches its
73
+ // generation and would go on addressing the dead one. Its signal to
74
+ // drop that generation is this event.
75
+ this.handle.onDidLoseConnection(() => this.disposeEmitter.fire(undefined));
76
+ }
77
+ return this.handle.current;
78
+ }
79
+
80
+ /**
81
+ * Surface a transport or write failure as a Theia notification.
82
+ *
83
+ * Swallowing it is the failure this exists to prevent: a dead connection and
84
+ * an empty document are indistinguishable in a widget.
85
+ *
86
+ * `reported` is already a complete sentence with the detail interpolated, so
87
+ * wrapping it in a sentence of the host's own would nest one owner's clause
88
+ * inside another's and leave no translator in control of the whole.
89
+ */
90
+ reportError(_error: unknown, reported: ResolvedMessage): void {
91
+ this.messageService.error(renderFrameworkMessage(reported, nls.localization?.translations));
92
+ }
93
+
94
+ dispose(): void {
95
+ this.handle?.dispose();
96
+ this.handle = undefined;
97
+ this.disposeEmitter.fire(undefined);
98
+ this.disposeEmitter.dispose();
99
+ }
100
+ }
@@ -7,11 +7,17 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { createRpcProxy } from '@hydranium/protocol';
10
+ import {
11
+ type DataPort,
12
+ type ReadyServer,
13
+ type ResolvedMessage,
14
+ RpcConnection,
15
+ type RpcProxy,
16
+ renderFrameworkMessage
17
+ } from '@hydranium/protocol';
18
+ import { Emitter, nls, type MessageService } from '@theia/core';
11
19
  import type { ServiceConnectionProvider } from '@theia/core/lib/browser';
12
- import { Deferred } from '@theia/core/lib/common/promise-util';
13
20
  import type { WorkspaceService } from '@theia/workspace/lib/browser';
14
- import type { MessageConnection } from 'vscode-jsonrpc';
15
21
  import { type ChannelConnectionHandle, openChannelConnection } from './channel-connection';
16
22
  import { whenWorkspaceOpen } from './workspace-gate';
17
23
 
@@ -28,138 +34,116 @@ import { whenWorkspaceOpen } from './workspace-gate';
28
34
  * the local notification target `TClient`. A subclass supplies the abstract
29
35
  * members below, calls {@link start} from its `@postConstruct`, and awaits
30
36
  * {@link ensureConnected} before its first `this.server.*` call.
37
+ *
38
+ * The lifecycle is `RpcConnection`'s — the same proxy, readiness gate and
39
+ * reconnect generation the host-neutral tier uses. What stays here is the Theia
40
+ * half: the channel, the workspace gate and the notification sink.
31
41
  */
32
- export abstract class AbstractDataServiceFrontend<TServer extends { waitForReady(): Promise<void> }, TClient extends object> {
33
- /**
34
- * The workspace-gated connection to the backend forwarder. Set by
35
- * {@link start}, and REPLACED whenever the connection is lost and
36
- * {@link reconnectOnConnectionLoss} is on — so read it per use and never
37
- * cache the resolved connection.
38
- */
39
- protected connectionPromise!: Promise<MessageConnection>;
40
- /**
41
- * Typed proxy over {@link connectionPromise}, addressing the server under
42
- * {@link methodNamespace}. Set by {@link start}, and replaced alongside
43
- * {@link connectionPromise} on reconnect.
44
- *
45
- * `createRpcProxy` resolves the connection promise once and binds to it for
46
- * good, so a reconnect necessarily means a new proxy. Reading
47
- * `this.server.foo()` per call — rather than hoisting `this.server` into a
48
- * local or a constructor-time field — is what keeps a subclass correct
49
- * across one.
50
- */
51
- protected server!: TServer;
52
- /** The channel handle {@link start} opened; owns reconnect and disposal. */
53
- protected channel?: ChannelConnectionHandle;
54
- /** Shared init Deferred so concurrent {@link ensureConnected} callers await one initialization. */
55
- protected initialized?: Deferred<void>;
56
-
57
- /** Theia connection provider the channel is opened through. */
42
+ export abstract class AbstractDataServiceFrontend<TServer extends ReadyServer, TClient extends object> {
58
43
  protected abstract readonly connectionProvider: ServiceConnectionProvider;
59
- /**
60
- * Optional workspace service. When provided, the default
61
- * {@link connectionReadyGate} waits for a workspace before opening the
62
- * channel; a head that is not workspace-scoped omits it (and may override
63
- * {@link connectionReadyGate} for a different gate).
64
- */
65
44
  protected abstract readonly workspaceService?: WorkspaceService;
66
- /** Local inbound-notification target bound on the connection (the `localTarget`). */
67
45
  protected abstract readonly client: TClient;
68
- /**
69
- * Theia service path the backend forwarder is registered under.
70
- *
71
- * **Unique per frontend, not per server.** Theia keys a frontend channel by
72
- * this path and throws `Another channel with the id '<path>' is already open`
73
- * on a second opener — so a subclass sharing the framework default with any
74
- * other consumer of the same head (a host-neutral `DataPort`, a sibling
75
- * service frontend) breaks whichever opens second. The failure is remote from
76
- * its cause: the throw escapes an `openChannelConnection` the other consumer
77
- * awaited, leaving its request permanently unsettled rather than rejected,
78
- * which presents as a view stuck on its loading state with a clean server
79
- * log. Give each frontend its own path and register a forwarder per path;
80
- * they still reach one server, since the shared `portCommand` is what names
81
- * the process.
82
- */
46
+ /** Frontend service path the backend forwarder for this head is registered under. */
83
47
  protected abstract readonly servicePath: string;
84
- /** Wire namespace the server + client methods are addressed under. */
85
48
  protected abstract readonly methodNamespace: string;
86
49
  /** Allowlist of {@link client} methods to bind as inbound handlers. */
87
50
  protected abstract readonly clientMethods: readonly (keyof TClient & string)[];
88
51
 
52
+ /** Surfaces a transport failure. Optional: a frontend with no UI of its own omits it. */
53
+ protected readonly messageService?: MessageService;
54
+
89
55
  /**
90
- * Rebuild the connection and the proxy when the current connection is lost,
91
- * and re-run initialization against the replacement. Defaults to `true`
92
- * see `OpenChannelConnectionOptions.reconnect` for why re-opening the
93
- * channel is the only thing that recovers a restarted language server, and
94
- * why a dead connection leaves no alternative worth preserving.
95
- *
96
- * The subclass-facing cost is that {@link doInitialize} runs again per
97
- * connection, so any progress UI it drives reappears. Turn this off for a
98
- * frontend that would rather show nothing than show its warm-up twice, or
99
- * that tears itself down on transport loss.
56
+ * Rebuild the connection when the current one is lost. Defaults to `true` —
57
+ * re-opening the channel is the only thing that recovers a restarted
58
+ * language server, which binds new ephemeral ports. Turn it off for a
59
+ * frontend that would rather tear itself down than show its warm-up twice.
100
60
  */
101
61
  protected readonly reconnectOnConnectionLoss: boolean = true;
102
62
 
63
+ protected channel?: ChannelConnectionHandle;
64
+ protected connection?: RpcConnection<TServer, TClient>;
65
+ protected readonly lossEmitter = new Emitter<void>();
66
+
103
67
  /**
104
68
  * Readiness gate for the connection — the channel opens only once the
105
69
  * returned promise settles. Default: waits for a workspace when
106
- * {@link workspaceService} is provided, otherwise opens immediately
107
- * (`undefined`). Override for a different gate (e.g. a fixed model store
108
- * that is always ready, or a custom warm-up).
70
+ * {@link workspaceService} is provided, otherwise opens immediately.
109
71
  */
110
72
  protected connectionReadyGate(): Promise<void> | undefined {
111
73
  return this.workspaceService ? whenWorkspaceOpen(this.workspaceService) : undefined;
112
74
  }
113
75
 
76
+ /** A connection generation is opening, including on each reconnect. */
77
+ protected onConnecting(): void {
78
+ // nothing by default
79
+ }
80
+
81
+ /** The server's readiness gate has settled for a generation. */
82
+ protected onReady(): void {
83
+ // nothing by default
84
+ }
85
+
86
+ /** A generation failed; the awaiting caller still rejects. */
87
+ protected onFailed(_error: unknown): void {
88
+ // nothing by default
89
+ }
90
+
114
91
  /**
115
- * Open the connection (workspace-gated by default via
116
- * {@link connectionReadyGate}) and build the combined server proxy +
117
- * inbound client binding. Call once (typically from the adopter's
118
- * `@postConstruct`). Outbound calls + inbound notifications queue over the
119
- * connection promise until the channel is live.
92
+ * Open the channel and build the connection over it. Call once, typically
93
+ * from the adopter's `@postConstruct`. Outbound calls and inbound
94
+ * notifications queue until the channel is live.
120
95
  */
121
96
  protected start(): void {
122
97
  this.channel = openChannelConnection(this.connectionProvider, this.servicePath, {
123
98
  whenReady: this.connectionReadyGate(),
124
99
  reconnect: this.reconnectOnConnectionLoss
125
100
  });
126
- this.bindConnection();
127
- // The LOSS, not the replacement's arrival: rebinding when the channel
128
- // closes points `server` at the queueing replacement promise, so a request
129
- // made during the gap waits for the new server instead of being addressed
130
- // at the dead one and never settling.
131
- this.channel.onDidLoseConnection(() => this.handleConnectionLost());
101
+ // The LOSS, not the replacement's arrival: the connection drops its
102
+ // generation here, so a request made during the gap waits for the fresh
103
+ // one instead of addressing the dead one and never settling.
104
+ this.channel.onDidLoseConnection(() => this.lossEmitter.fire(undefined));
105
+ this.connection = new RpcConnection<TServer, TClient>(this.channelPort(), this.client, {
106
+ methodNamespace: this.methodNamespace,
107
+ clientMethods: this.clientMethods,
108
+ lifecycle: {
109
+ onConnecting: () => this.onConnecting(),
110
+ onReady: () => this.onReady(),
111
+ onFailed: error => this.onFailed(error)
112
+ }
113
+ });
114
+ }
115
+
116
+ /** The channel as a {@link DataPort} — the whole Theia-specific half. */
117
+ protected channelPort(): DataPort {
118
+ return {
119
+ // Read per call: `current` is repointed on every re-open, so reaching
120
+ // through the handle is what makes a later generation find the live
121
+ // server.
122
+ connect: () => this.requireChannel().current,
123
+ reportError: (_error, reported) => this.reportError(reported),
124
+ onDispose: this.lossEmitter.event
125
+ };
126
+ }
127
+
128
+ /** Surface a transport failure the way this host does. */
129
+ protected reportError(reported: ResolvedMessage): void {
130
+ this.messageService?.error(renderFrameworkMessage(reported, nls.localization?.translations));
132
131
  }
133
132
 
134
133
  /**
135
- * Point {@link connectionPromise} and {@link server} at the channel's
136
- * current connection. Called by {@link start} and again per reconnect.
134
+ * The server proxy. Calls queue against the connection, so await
135
+ * {@link ensureConnected} first wherever the server's readiness matters.
137
136
  */
138
- protected bindConnection(): void {
139
- if (!this.channel) {
140
- throw new Error('bindConnection called before start');
141
- }
142
- this.connectionPromise = this.channel.current;
143
- this.server = createRpcProxy<TServer, TClient>(this.connectionPromise, {
144
- methodNamespace: this.methodNamespace,
145
- localTarget: this.client,
146
- localMethods: this.clientMethods
147
- });
137
+ protected get server(): RpcProxy<TServer> {
138
+ return this.requireConnection().server;
148
139
  }
149
140
 
150
141
  /**
151
- * Rebind onto the replacement connection and arm initialization to run again.
152
- *
153
- * Clearing {@link initialized} is the load-bearing half. A restarted server
154
- * has an unwarmed workspace, so its `waitForReady` gate has to be awaited
155
- * afresh; leaving the old resolved Deferred in place would let the first
156
- * request after a restart through against a server still walking the
157
- * workspace, and be answered correctly from an empty registry — which reads
158
- * as data loss rather than as a race.
142
+ * Await the connection and the server's readiness gate, shared across
143
+ * concurrent callers and re-run once per connection generation.
159
144
  */
160
- protected handleConnectionLost(): void {
161
- this.initialized = undefined;
162
- this.bindConnection();
145
+ protected async ensureConnected(): Promise<void> {
146
+ await this.requireConnection().connected();
163
147
  }
164
148
 
165
149
  /**
@@ -170,39 +154,24 @@ export abstract class AbstractDataServiceFrontend<TServer extends { waitForReady
170
154
  * lifecycle of its own.
171
155
  */
172
156
  dispose(): void {
157
+ this.connection?.dispose();
158
+ this.connection = undefined;
173
159
  this.channel?.dispose();
174
160
  this.channel = undefined;
175
- this.initialized = undefined;
161
+ this.lossEmitter.dispose();
176
162
  }
177
163
 
178
- /**
179
- * Lazily drive initialization, shared across concurrent callers via one
180
- * {@link Deferred}. Request methods `await this.ensureConnected()` before
181
- * their first `this.server.*` call.
182
- */
183
- protected ensureConnected(): Promise<void> {
184
- if (!this.initialized) {
185
- this.initialized = new Deferred<void>();
186
- void this.doInitialize(this.initialized);
164
+ protected requireConnection(): RpcConnection<TServer, TClient> {
165
+ if (!this.connection) {
166
+ throw new Error('the connection is not open: call start() first');
187
167
  }
188
- return this.initialized.promise;
168
+ return this.connection;
189
169
  }
190
170
 
191
- /**
192
- * Default initialization: await the connection, await the server's readiness
193
- * gate, then resolve the passed Deferred. Initialization completion is
194
- * observable by awaiting {@link ensureConnected} (which returns this same
195
- * Deferred's promise) — there is no separate post-init hook. Override
196
- * wholesale to interleave progress UI / extra warm-up; an override owns
197
- * resolving/rejecting `initialized` (there is no `super` step to call).
198
- */
199
- protected async doInitialize(initialized: Deferred<void>): Promise<void> {
200
- try {
201
- await this.connectionPromise;
202
- await this.server.waitForReady();
203
- initialized.resolve();
204
- } catch (error) {
205
- initialized.reject(error instanceof Error ? error : new Error(String(error)));
171
+ protected requireChannel(): ChannelConnectionHandle {
172
+ if (!this.channel) {
173
+ throw new Error('the channel is not open: call start() first');
206
174
  }
175
+ return this.channel;
207
176
  }
208
177
  }
@@ -8,6 +8,7 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  export * from './channel-connection';
11
+ export * from './channel-data-port';
11
12
  export * from './data-service-frontend';
12
13
  export * from './diagnostics-data-service-frontend';
13
14
  export * from './host-diagnostics-frontend';