import type { NodeStateListener } from '../orchestrator/types.js'; import type { HostedNodeStore } from './hosted-node-store.js'; import type { HostedNodeChange } from './types.js'; /** How often an unclaimed node's `/status` is read for the claim, and a joining node's for its redemption. */ export declare const HOSTED_NODE_CLAIM_POLL_MS = 2000; /** How often a joined node's `/status` is read for its connection count. */ export declare const HOSTED_NODE_CONNECTED_POLL_MS = 15000; /** * The orchestrator surface the watcher needs: the exit/start signal, when the orchestrator * has one, and the stop of a node whose invitation failed or that an owner removed. */ export interface WatchedOrchestrator { onStateChange?(listener: NodeStateListener): () => void; stopContainer(dockerId: string): Promise; } export interface HostedNodeWatcherOptions { store: HostedNodeStore; orchestrator: WatchedOrchestrator; now: () => Date; /** Where a claim or a liveness change is reported (`HostedNodeService.emit`). */ emit: (change: HostedNodeChange) => void; } /** * Follows each hosted node's `/status`: an `unclaimed` record becomes `joined` when * the node reports a finished claim; a `joining` record becomes `joined` when a member * accepted its invitation, or `error` when the redemption failed; a `joined` * record's `connected` follows the node's control-connection count; and any of them * becomes `error` when the node reports that an owner removed it from its cadre. * Best-effort: a poll that fails is retried on the next tick, and nothing here throws * out of the timer. * * One timer at the claim cadence drives all three: every tick polls every unclaimed and * joining node, and each joined node every {@link HOSTED_NODE_CONNECTED_POLL_MS}, or at once when it * was poked — the orchestrator's state change on a respawn pokes the node, and the * poke stands until a poll succeeds, so a respawned child is noticed on its first * answer rather than at the next 15 s mark. Polls are serialized on one promise * tail so a timer pass and a poked pass cannot both write one record. * * NOTE: the 2 s timer ticks while nothing is unclaimed (one cached store read, no * I/O per tick); if it ever shows up in a profile, stop it when no record is * unclaimed and restart it from `poke`. */ export declare class HostedNodeWatcher { private readonly store; private readonly orchestrator; private readonly now; private readonly emit; private tail; private timer?; private unsubscribe?; /** When each joined node's `/status` last answered (ms since epoch). */ private readonly lastPolledAt; /** Joined nodes to poll on the next pass regardless of cadence. */ private readonly due; constructor(opts: HostedNodeWatcherOptions); /** Start the timer and the state-change subscription, and poll once. Idempotent. */ start(): void; stop(): void; /** Ask for a pass now; `id` names a joined node to poll ahead of its cadence. Fire-and-forget. */ poke(id?: string): void; /** One pass over every unclaimed, joining and due joined record, serialized against the others. */ poll(): Promise; private pollOnce; /** Drop the cadence and poke bookkeeping of nodes no longer on record, so neither map grows with removals. */ private forgetRemoved; private shouldPoll; private pollOne; /** An unclaimed node that reports a finished claim is `joined`, with the party and owner the claim named. */ private applyUnclaimed; /** * A joining node whose redemption settled: `joined` with the member that admitted it and * the invitation's issuer as its owner, or `error` with the reason and whether Retry may * help. A failed node's child is stopped after the write, as a give-up's is, and kept * with its workdir for Retry; the write comes first so the exit the stop raises finds * a record the supervisor leaves alone. Still `pending`: nothing. */ private applyJoining; /** * A node that reports an owner removed it from its cadre: `error`, not retryable, its child * stopped after the write as a failed invitation's is, and its workdir kept for Remove (or a * claim node's Reset). The child had already stopped its node, and stays up only to report * the removal, so stopping it loses nothing. The party and owner come from the report, for an * `unclaimed` record whose claim this watcher never saw: its own party is the placeholder. */ private applyRemoved; /** Stop a node's child after its record went `error`; a failed stop is logged, the record stands. */ private stopChild; /** * A joined node's `connected` follows its control-connection count; published only on a * change. Its `/status` keeps reporting the invitation for as long as the child that * redeemed it runs, and whatever it says there is ignored: the node is a member, and the * record says so. */ private applyJoined; }