import './utils/disposable'; import { MessageBus } from './MessageBus'; import type { Unsubscribe } from './types'; interface CoordinatorOptions { electionTimeout?: number; heartbeatInterval?: number; leaderTimeout?: number; leaderPingTimeout?: number; } export declare class TabCoordinator implements Disposable { private readonly bus; private readonly tabId; private _isLeader; private heartbeatTimer; private leaderCheckTimer; private lastHeartbeat; private disposed; private onBecomeLeaderFns; private onLoseLeadershipFns; private onLeaderUnhealthyFns; private cleanups; /** * Optional predicate supplied by the owner that reports whether THIS tab's * leader socket is actually alive. A backgrounded leader whose socket has * silently died still has `_isLeader === true`, so without this check an * election would keep deferring to a zombie. When set, a leader only * answers health pings (and passes self-verification) while it returns true. */ private healthCheck; /** Re-entrancy guard so rapid visibility toggles don't stack verifications. */ private verifying; /** * Set while an election is in flight. Lets a concurrent election from a * tab with a lower tabId pre-empt this one (deterministic tie-break) so two * tabs electing at the same instant can't both become leader (split-brain). * Calling it makes this tab yield and become a follower. */ private electionAbort; private readonly electionTimeout; private readonly heartbeatInterval; private readonly leaderTimeout; private readonly leaderPingTimeout; constructor(bus: MessageBus, tabId: string, options?: CoordinatorOptions); get isLeader(): boolean; elect(): Promise; abdicate(): void; onBecomeLeader(fn: () => void): Unsubscribe; onLoseLeadership(fn: () => void): Unsubscribe; /** * Fired when a self-verification finds THIS tab is leader but its socket * is unhealthy (per `setHealthCheck`). The owner should reconnect. */ onLeaderUnhealthy(fn: () => void): Unsubscribe; /** Supply a predicate reporting whether this tab's leader socket is alive. */ setHealthCheck(fn: () => boolean): void; /** * Verify the active leader is alive and re-elect if it isn't. Call this * when a tab becomes visible again after being idle: browser timer * throttling can leave a backgrounded leader with a dead socket while its * last heartbeat still looks recent enough that no follower would elect. * * - Leader tab → checks its own socket health; if unhealthy, fires * `onLeaderUnhealthy` so the owner can reconnect. * - Follower tab → pings the leader and waits up to `leaderPingTimeout`. * If no healthy leader answers, forces a step-down and runs a fresh * election so this (active) tab can take over the connection. */ verifyLeader(): Promise; /** * Follower-only: ping the current leader and take over the connection if no * healthy leader answers. Shared by `verifyLeader()` (active-tab path) and * the heartbeat-staleness check (covers the case where THIS tab stays active * the whole time while a backgrounded leader's socket silently dies — there * is no visibility change to trigger `verifyLeader`, but the leader-check * timer keeps running and routes here). */ private takeOverIfLeaderDead; /** Ping the current leader; resolve true if a healthy leader ponged in time. */ private pingLeader; private becomeLeader; private startHeartbeat; private stopHeartbeat; private startLeaderCheck; private stopLeaderCheck; [Symbol.dispose](): void; } export {};