/** * Live channel lifecycle for dsh-connect. * * `activateChannels` starts a fixed set of adapters once, at load time. Once * the channel list is user-editable (the `dsh-connect` settings namespace) the * plugin needs the other half of that story: a channel removed from * `channels[]` has to be *stopped*, and one whose config changed has to be * restarted with the new values. * * Two properties this class exists to guarantee: * * - **Stop before start.** A reconfigured Feishu adapter must release its long * connection before the replacement opens one. Two live connections for the * same app deliver every event twice, which looks like the bot answering * itself rather than like a crash. * - **Serialised applies.** A settings edit can fire `onChange` more than once * in a burst (and the provider's file watcher debounces on its own schedule). * Applies are chained, so a stop can never interleave with the next start. * * Kept free of cordis and channel-SDK imports so it is unit-testable with fake * adapters. * @module dsh-connect/settings/channel-runtime */ import { type ChannelApply, type ChannelName, type ChannelsConfig, type LoggerLike } from "./channels.js"; /** Why a channel was stopped — used only for the log line. */ export type StopReason = "disabled" | "reconfigured" | "shutdown"; export interface ChannelRuntimeOptions { /** Context handed to each channel's `apply`. */ ctx: Ctx; /** Channel name → its plugin `apply`, as in `activateChannels`. */ channels: Record>; /** * Stop a channel that is running. Wired to `ConnectService.unregisterAdapter` * so the adapter is dropped from the routing map *and* torn down; a runtime * that only knew how to start would leak connections on every settings edit. */ teardown: (name: ChannelName) => Promise; /** * Refresh store-backed secrets before computing each channel's effective * config (same source `apply` uses at boot). Optional so tests can skip it. */ getSecrets?: (name: ChannelName) => Promise>; } /** * Owns which channels are currently running and reconciles that set against a * desired config on every change. */ export declare class ChannelRuntime { private readonly opts; /** Channel → the effective config it was started with. */ private readonly running; /** * Channel → why its last start failed, for the channels that are *not* up * because starting them threw. * * `reconcile` deliberately lets one bad channel fail without taking the rest * down, and the log line was the only trace of it — so a save that left a * channel dead still answered 「已保存」, and the user waited for a bot that * was never coming. This is the read side the settings pane reports. * * It is a statement about the *current* state, not a history: a start that * succeeds and a stop both clear the entry, so a channel that was fixed by a * later save stops being listed. */ private readonly failed; /** Tail of the apply chain; see "Serialised applies" above. */ private chain; private disposed; constructor(options: ChannelRuntimeOptions); /** Channels currently up, in activation order. */ active(): ChannelName[]; /** Channels that are down because their start threw, as `{channel: reason}`. */ failures(): Record; /** * Bring the running set in line with `config`. Channels that are new start, * channels that are gone or whose effective config changed restart. * * Never rejects: a channel that fails to start or stop is logged and the * others still get their chance. A settings change should not be able to take * the whole bridge down. */ apply(config: ChannelsConfig | null | undefined): Promise; /** Stop every running channel. Waits for an in-flight apply first. */ dispose(): Promise; private log; private reconcile; /** Channel → effective config, mirroring `activateChannels`' merge exactly. */ private desired; private stop; } //# sourceMappingURL=channel-runtime.d.ts.map