@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.
- package/lib/browser/channel-data-port.d.ts +65 -0
- package/lib/browser/channel-data-port.d.ts.map +1 -0
- package/lib/browser/channel-data-port.js +115 -0
- package/lib/browser/channel-data-port.js.map +1 -0
- package/lib/browser/data-service-frontend.d.ts +39 -97
- package/lib/browser/data-service-frontend.d.ts.map +1 -1
- package/lib/browser/data-service-frontend.js +77 -98
- package/lib/browser/data-service-frontend.js.map +1 -1
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/package.json +7 -7
- package/src/browser/channel-data-port.ts +100 -0
- package/src/browser/data-service-frontend.ts +94 -125
- package/src/browser/index.ts +1 -0
|
@@ -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
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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
|
-
*
|
|
116
|
-
*
|
|
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
|
|
79
|
+
protected get server(): RpcProxy<TServer>;
|
|
119
80
|
/**
|
|
120
|
-
*
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
*
|
|
98
|
-
*
|
|
107
|
+
* The server proxy. Calls queue against the connection, so await
|
|
108
|
+
* {@link ensureConnected} first wherever the server's readiness matters.
|
|
99
109
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
122
|
-
this.
|
|
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.
|
|
132
|
+
this.lossEmitter.dispose();
|
|
136
133
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
138
|
+
return this.connection;
|
|
148
139
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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":";AAAA;;;;;;;kFAOkF;;;AAElF,
|
|
1
|
+
{"version":3,"file":"data-service-frontend.js","sourceRoot":"","sources":["../../src/browser/data-service-frontend.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAElF,kDAO6B;AAC7B,sCAAgE;AAGhE,6DAA2F;AAC3F,qDAAqD;AAErD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAsB,2BAA2B;IAU9C,yFAAyF;IACtE,cAAc,CAAkB;IAEnD;;;;;OAKG;IACgB,yBAAyB,GAAY,IAAI,CAAC;IAEnD,OAAO,CAA2B;IAClC,UAAU,CAAmC;IACpC,WAAW,GAAG,IAAI,cAAO,EAAQ,CAAC;IAErD;;;;OAIG;IACO,mBAAmB;QAC1B,OAAO,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAA,kCAAiB,EAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACvF,CAAC;IAED,uEAAuE;IAC7D,YAAY;QACnB,qBAAqB;IACxB,CAAC;IAED,gEAAgE;IACtD,OAAO;QACd,qBAAqB;IACxB,CAAC;IAED,8DAA8D;IACpD,QAAQ,CAAC,MAAe;QAC/B,qBAAqB;IACxB,CAAC;IAED;;;;OAIG;IACO,KAAK;QACZ,IAAI,CAAC,OAAO,GAAG,IAAA,0CAAqB,EAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,WAAW,EAAE;YAC7E,SAAS,EAAE,IAAI,CAAC,mBAAmB,EAAE;YACrC,SAAS,EAAE,IAAI,CAAC,yBAAyB;SAC3C,CAAC,CAAC;QACH,oEAAoE;QACpE,wEAAwE;QACxE,6DAA6D;QAC7D,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QACzE,IAAI,CAAC,UAAU,GAAG,IAAI,wBAAa,CAAmB,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE;YACpF,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,SAAS,EAAE;gBACR,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE;gBACvC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE;gBAC7B,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;aACzC;SACH,CAAC,CAAC;IACN,CAAC;IAED,yEAAyE;IAC/D,WAAW;QAClB,OAAO;YACJ,sEAAsE;YACtE,oEAAoE;YACpE,UAAU;YACV,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,OAAO;YAC5C,WAAW,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YAC7D,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK;SACnC,CAAC;IACL,CAAC;IAED,0DAA0D;IAChD,WAAW,CAAC,QAAyB;QAC5C,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,IAAA,iCAAsB,EAAC,QAAQ,EAAE,UAAG,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;IAChG,CAAC;IAED;;;OAGG;IACH,IAAc,MAAM;QACjB,OAAO,IAAI,CAAC,iBAAiB,EAAE,CAAC,MAAM,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACO,KAAK,CAAC,eAAe;QAC5B,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC,SAAS,EAAE,CAAC;IAC9C,CAAC;IAED;;;;;;OAMG;IACH,OAAO;QACJ,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QACzB,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;IAC9B,CAAC;IAES,iBAAiB;QACxB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACrE,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAC;IAC1B,CAAC;IAES,cAAc;QACrB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;QAClE,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC;IACvB,CAAC;CACH;AAvID,kEAuIC"}
|
package/lib/browser/index.d.ts
CHANGED
|
@@ -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"}
|
package/lib/browser/index.js
CHANGED
|
@@ -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);
|
package/lib/browser/index.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
74
|
-
"@hydranium/core": "1.0.0-next.
|
|
75
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
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.
|
|
87
|
-
"@hydranium/core": "1.0.0-next.
|
|
88
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
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 {
|
|
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
|
|
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
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
*
|
|
136
|
-
*
|
|
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
|
|
139
|
-
|
|
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
|
-
*
|
|
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
|
|
161
|
-
this.
|
|
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.
|
|
161
|
+
this.lossEmitter.dispose();
|
|
176
162
|
}
|
|
177
163
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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.
|
|
168
|
+
return this.connection;
|
|
189
169
|
}
|
|
190
170
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
}
|
package/src/browser/index.ts
CHANGED
|
@@ -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';
|