import type { ClusterSurfaceRegistry } from './surface-registry.js'; import type { ClusterClock, ClusterLogger, ClusterRole, ClusterSettings, ClusterStatus, ClusterTransport } from './types.js'; export interface ClusterElectionOptions { readonly nodeId: string; readonly version: string; readonly settings: ClusterSettings; readonly transport: ClusterTransport; readonly clock: ClusterClock; readonly logger: ClusterLogger; /** The surfaces this node can actually serve. */ readonly registry: ClusterSurfaceRegistry; /** Test seam for the CLAIM jitter draw. Defaults to Math.random. */ readonly random?: (() => number) | undefined; } export declare class ClusterElection { private readonly options; private readonly timing; private readonly ledger; private readonly elections; private readonly peers; private readonly host; private running; private seq; /** * Monotonic reading at start(), or null before it. NOT a numeric sentinel: * a monotonic clock legitimately reads 0, and treating that as "not started" * pins this node's uptime at zero for its whole life. * * Uptime is no longer a ranking tier, the spread ranking dropped it because * it concentrated every surface on the longest-lived node, but it is still * reported, because "how long has this node been up" is the first thing * anyone reads a `/status` for. */ private startMonotonic; private cancelWatchdog; private unsubscribeRegistry; /** Monotonic reading of this node's last voluntary yield; damps rebalancing. */ private lastYieldMono; /** Wall/monotonic pair from the previous watchdog tick, for suspend detection. */ private lastTickWall; private lastTickMono; constructor(options: ClusterElectionOptions); /** * Join the group: open the transport, then run one boot probe per surface. * * A node with NOTHING to serve still opens the transport, and that is * deliberate: it takes part in nobody's election and claims nothing, but it * hears the group, so its `/status` can say who holds what. It sends no * datagram of its own until a surface is registered. */ start(): Promise; /** Leave every election cleanly, then close the socket. */ stop(reason?: string): Promise; /** Resolves once every surface's queued transitions have finished. */ settled(): Promise; /** True when this node holds at least one surface. */ get isMaster(): boolean; get heldSurfaceIds(): string[]; surfaceRole(surfaceId: string): ClusterRole; status(): ClusterStatus; private aggregateRole; /** * A provider reported that something else is already consuming. * * Routed to ONE surface when the caller knows which, a Telegram 409 is * about one bot token and says nothing about ntfy. Without a surface it * reaches every surface this node currently holds, which is the conservative * reading of an unattributed conflict. */ reportConsumerConflict(detail: string, surfaceId?: string): void; /** * Bring the running elections into line with what this node can serve. * * A surface that appears gets an election and a boot probe. A surface that * disappears, the credential was removed, the consumer was unregistered, * gets stopped through the ordered path, so if this node was holding it, it * stops consuming and says goodbye rather than going quiet and making the * rest of the network wait out the crash timeout. */ private syncSurface; private startConsumer; /** * The node's single rebalancing slot. See `SurfaceElectionHost` for why it * belongs to the node and not to each surface. * * One surface moves per cooldown, deliberately. A node three surfaces ahead * converges in successive steps rather than in one lurch, and each step is * taken against freshly observed numbers rather than against a snapshot * every surface read at the same instant. */ private tryReserveYield; private receive; /** Track the peer; returns false for a duplicate or reordered datagram. */ private recordPeer; /** * Update the holdings ledger from observed traffic. * * Only a HEARTBEAT counts as holding. A CLAIM is a node saying it WANTS the * surface, and most claims lose, counting them would briefly credit every * losing candidate in an election with a surface it never got, and the * spread ranking would rank against numbers that were never true. A holder * sends its first HEARTBEAT in the same breath as the CLAIM that took the * surface, so nothing is lost by waiting for it. RESIGN is letting go. * * Any datagram at all, a PROBE from a standby included, proves the sender * can serve that surface, which is what makes it a rebalancing candidate. */ private recordHoldings; private armWatchdog; /** * One tick does two jobs: notice that this host was asleep, and let every * surface notice a holder that stopped breathing. * * Suspend is node-level, the whole process was frozen, so every surface's * consumer was frozen with it, while holder timeout is per surface. */ private onWatchdogTick; private uptimeMs; private send; } //# sourceMappingURL=election-node.d.ts.map