import { type ClusterSurfaceKey } from './surface-id.js'; import type { ClusterClock, ClusterConsumerGate, ClusterLogger, ClusterSettings, ClusterStatus, ClusterSurfaceHolding, ClusterTransport } from './types.js'; export interface ClusterCoordinatorOptions { readonly settings: ClusterSettings; readonly version: string; /** Daemon state directory; holds the persistent node id. */ readonly stateDirectory: string; readonly logger: ClusterLogger; /** Test seam: an in-memory transport instead of a real socket. */ readonly transport?: ClusterTransport | undefined; readonly clock?: ClusterClock | undefined; readonly nodeId?: string | undefined; readonly random?: (() => number) | undefined; } export declare class ClusterCoordinator { private readonly options; private readonly registry; private readonly clock; /** * Resolved on first use, never in the constructor: resolving it MINTS AND * WRITES a file, and merely composing a runtime (which every test that * builds RuntimeServices does) must not touch the state directory. */ private nodeIdValue; private election; private transport; private started; /** Only meaningful with the election off: every gate runs unconditionally. */ private ungatedRunning; /** Run once at start(), before anything is contested. See `onPrepare`. */ private readonly prepares; constructor(options: ClusterCoordinatorOptions); private get nodeId(); get enabled(): boolean; /** * True between `start()` and `stop()`. * * Read by anything that retries registering a surface in the background, a * Slack workspace whose identity would not resolve the first time, so it * stops retrying once the daemon is shutting down instead of registering * consumers into a coordinator that has already left the group. */ get running(): boolean; /** True when this node holds at least one inbound surface. */ get isMaster(): boolean; /** True when this node currently holds the given surface. */ holdsSurface(surface: ClusterSurfaceKey): boolean; /** * The surfaces this node is currently consuming, and why, the answer * `ClusterGroupRuntimeOptions.surfaceHoldings` asks for. * * The per-surface election owns this fact, so the group layer is handed a * reader for it rather than keeping a second copy that could disagree with * the elections actually running. `null`, not an empty array, when there is * no election to ask, because "this node holds nothing" and "nobody can say * what this node holds" are different answers and `cluster status` prints * them differently. * * Every `surfaceId` here is already the digest the election routes on, never * a topic or chat id; the group layer digests again on the way out, which is * a no-op for a value of this shape and a backstop for one that is not. */ surfaceHoldings(): readonly ClusterSurfaceHolding[] | null; /** * Register an inbound consumer for one surface. Returns an unregister * function. * * Registering while the node is already running starts that surface's * election immediately, so a consumer composed late, a topic added at * runtime, an account that finished authenticating, still comes up without * a restart. */ register(gate: ClusterConsumerGate): () => void; /** * Register work that decides WHICH surfaces this node can serve, to run once * at `start()` before any election begins. * * It exists because that decision is asynchronous, a surface is servable * only if its credential actually resolves, and a `goodvibes://secrets/...` * reference resolves off disk, while composition roots are not. Registering * a surface after the boot probe would make it sit out its own first * election; guessing synchronously would let a node with an unresolvable * token win one and then read nothing. * * Several callers may add their own: a host that composes inbound consumers * of its own shares this coordinator rather than making a second one, and * each contributes the surfaces it knows about. */ onPrepare(prepare: () => Promise): void; start(): Promise; stop(reason?: string): Promise; /** Wait for any in-flight transition, tests and orderly shutdown use it. */ settled(): Promise; /** * A provider reported that something else is already consuming a surface * (Telegram's 409 on getUpdates is the live case). Never contest it. * * Pass the surface when it is known: a 409 from one bot token is not a * reason for this node to give up an unrelated ntfy topic. */ reportConsumerConflict(detail: string, surface?: ClusterSurfaceKey): void; /** The `cluster` section of /status. Inspection only. */ status(): ClusterStatus; private startAllUngated; private stopAllUngated; private startGateUngated; } //# sourceMappingURL=coordinator.d.ts.map