/******************************************************************************** * Copyright (c) 2026 CrossBreeze, EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { type Clock } from '@hydranium/protocol'; import { type Channel, type Event } from '@theia/core'; import { type ServiceConnectionProvider } from '@theia/core/lib/browser'; import { type Logger, type MessageConnection } from 'vscode-jsonrpc/browser'; /** * Build a `vscode-jsonrpc` {@link MessageConnection} on top of a Theia * {@link Channel}. This is the browser-side transport for the data-server * head: the frontend obtains a channel to the backend forwarder (via * `connectionProvider.listen`), wraps it here, and drives the typed * `createRpcProxy` over the returned connection — so the frontend * speaks the same vscode-jsonrpc protocol the model-server speaks, with the * backend relaying bytes between the two. * * Mirrors `@eclipse-glsp/theia-integration`'s `createChannelConnection`, but * lives here so the data-server head carries no GLSP dependency. */ export declare function createChannelConnection(channel: Channel, logger: Logger): MessageConnection; /** Options for {@link openChannelConnection}. */ export interface OpenChannelConnectionOptions { /** * Resolves when it is safe to open the channel. The handler holds back the * `connectionProvider.listen` call until this settles — e.g. pass * `whenWorkspaceOpen` so the backend forwarder isn't asked for the * model-server port before a workspace (and therefore the LSP launch that * publishes the port) exists. */ whenReady?: Promise; /** Where the connection logs its protocol faults; without one they leave no trace. */ logger: Logger; /** Times the escalation reset. Default a `SystemClock`. */ clock?: Clock; /** * Re-establish the connection whenever the live one is lost, by RE-OPENING * the channel on `path` — not merely by rebuilding over a channel Theia hands * back. * * **Default `true`, and the default is the point.** Re-opening is the only * thing that recovers a restarted language server. Theia replays a `listen` * handler solely from `ServiceConnectionProvider.handleChannelCreated`, i.e. * when the MAIN frontend-backend channel is (re)created — a page reload, a * dropped socket, a backend restart. A language-server restart closes just the * multiplexed sub-channel for this service path while the main channel stays * open, so no replacement channel ever arrives on its own and a handle that * only waits for one holds a connection to the dead process forever: its * requests neither answer nor reject. * * **Why on by default, where the older rebuild-on-a-fresh-channel behaviour * was opt-in.** That one had a working alternative to weigh against — the * existing connection was fine, and opting in only decided whether to swap to * a fresher one, so leaving a consumer's readiness gate (and the progress UI it * drives) undisturbed was worth more. A CLOSED channel offers no such choice: * the alternative to re-opening is a permanently dead handle. Re-running a * readiness gate against a live replacement is what a restarted server should * cost. * * Turn it off for a consumer that treats transport loss as terminal and tears * itself down instead. Off means build-once: every channel after the first is * ignored and {@link ChannelConnectionHandle.onDidLoseConnection} never fires. */ reconnect?: boolean; /** * Delay before the re-open following each CONSECUTIVE connection loss, in ms; * the last entry repeats once the schedule is exhausted. Defaults to * {@link DEFAULT_RECONNECT_DELAYS}. * * A delay rather than an immediate re-open, for two independent reasons. * Theia's `ChannelMultiplexer.handleClose` fires the close emitter BEFORE it * deletes the id from `openChannels`, so a re-open issued from inside the * close listener can still see the id as open — and `open()` throws * "Another channel with the id '' is already open" then, which * `ServiceConnectionProvider.listen` neither catches nor reports: the handler * is simply never invoked, and the consumer hangs rather than failing. And a * server that is flapping would otherwise be re-opened against at whatever * rate it can close a channel, which is what the escalation is for. * * The escalation resets once a connection has survived * {@link RECONNECT_ESCALATION_RESET_MS}, so an hour-long session with * occasional restarts does not converge on the longest delay. */ reconnectDelays?: readonly number[]; } /** * Default {@link OpenChannelConnectionOptions.reconnectDelays}. * * The first entry only has to outlast the synchronous close dispatch, so it is * short: the backend forwarder, not this delay, is what absorbs a server that is * still down. `AbstractSocketForwardingConnectionHandler` re-runs its port command per * connection and retries it indefinitely, so a channel re-opened while the * server is restarting parks in `findPort` and connects when the replacement * publishes its new port. The later entries exist for the case that machinery * cannot absorb — a server that accepts a socket and then dies again. */ export declare const DEFAULT_RECONNECT_DELAYS: readonly number[]; /** How long a connection must survive before the reconnect escalation resets. */ export declare const RECONNECT_ESCALATION_RESET_MS = 30000; /** * A live channel connection plus the two things a bare * `Promise` cannot express: that the connection can be * REPLACED, and that it can be released. */ export interface ChannelConnectionHandle { /** * The current connection. Read it per use rather than caching it: after a * reconnect this returns a NEW promise, and a cached one resolves to the * dead connection forever. The same reasoning makes `RpcConnection` rebuild * its proxy per generation rather than hold one. */ readonly current: Promise; /** * Fires when the live connection is gone and a replacement is on the way. * {@link current} has ALREADY been repointed at the replacement (still * unresolved) by the time listeners run, so a listener re-derives from * `current` and whatever it queues waits for the new channel instead of being * addressed at the dead one. * * **This is the event a consumer wants, not {@link onDidReconnect}.** Anything * derived from a connection — an `createRpcProxy`, a readiness gate, a * `DataConnection` generation — is dead from this moment, and a consumer that * waits for the replacement to be live instead keeps sending into the dead one * for the whole gap. A `DataPort` implementation translates this into the * port's own `onDispose`, which is what makes `DataConnection` drop its * generation. * * Never fires when {@link OpenChannelConnectionOptions.reconnect} is off. */ readonly onDidLoseConnection: Event; /** * Fires with each connection built AFTER the first, once it is live. For a * consumer that needs the connection object itself; for rebuilding what was * derived from the previous one, use {@link onDidLoseConnection} instead — * it fires at the start of the gap rather than at its end. * * Never fires when {@link OpenChannelConnectionOptions.reconnect} is off. */ readonly onDidReconnect: Event; /** * Dispose the live connection and stop tracking the channel. Idempotent. * * Without it, a frontend being torn down leaves its connection and its * inbound handler bindings attached — a leak that accumulates per cycle in * tests and in a reloading workbench. */ dispose(): void; } /** * Open a Theia channel to a backend forwarder and wrap it as the * `vscode-jsonrpc` {@link MessageConnection} the data-server proxy speaks over. * * Awaits `options.whenReady` (if given), then registers the channel handler via * `connectionProvider.listen` and keeps a live connection to `path` for as long * as the handle is undisposed — re-opening the channel whenever the current one * closes, which {@link OpenChannelConnectionOptions.reconnect} governs. Callers * build their `createRpcProxy` / `bindRpcMethods` over * {@link ChannelConnectionHandle.current}, which queues outbound calls and * inbound bindings until the channel is live. * * **The `reconnect` argument to Theia's `listen` is deliberately `false`**, in * both modes. Passing `true` there registers the handler for replay from * `handleChannelCreated`, and this handle then has two independent re-openers for * one path: Theia's replay when the main frontend-backend channel is recreated, * and the close-driven re-open below — which also fires then, because * `ChannelMultiplexer.onUnderlyingChannelClose` closes every sub-channel. Two * `open()` calls for one id means the loser throws "Another channel with the id * '' is already open" inside a promise `listen` neither awaits nor reports, * so the consumer's request is left unsettled: a hang with a clean log, not an * error. Owning the re-open here means owning it exclusively. */ export declare function openChannelConnection(connectionProvider: ServiceConnectionProvider, path: string, options: OpenChannelConnectionOptions): ChannelConnectionHandle; //# sourceMappingURL=channel-connection.d.ts.map