/** * Reconnection manager — exponential backoff with jitter, * matching Rust reconnect patterns (1s base, 30s cap, 2^n multiplier). */ import { RECONNECT_BASE_DELAY_MS, RECONNECT_MAX_DELAY_MS, RECONNECT_MAX_ATTEMPTS, } from './constants'; export interface ReconnectState { attempt: number; nextDelay: number; isReconnecting: boolean; lastAttemptAt: number | null; } export class ReconnectManager { private attempt = 0; private isReconnecting = false; private timer: ReturnType | null = null; private onReconnect: (() => void) | null = null; /** Register the callback that fires when reconnection should be attempted. */ setCallback(onReconnect: () => void): void { this.onReconnect = onReconnect; } /** Start reconnection cycle — exponential backoff with jitter. */ start(): void { if (this.isReconnecting) return; this.isReconnecting = true; this.attempt = 0; this.scheduleNext(); } /** Stop reconnection cycle. */ stop(): void { this.isReconnecting = false; if (this.timer) { clearTimeout(this.timer); this.timer = null; } } /** Notify that reconnection succeeded — resets state. */ succeed(): void { this.attempt = 0; this.isReconnecting = false; if (this.timer) { clearTimeout(this.timer); this.timer = null; } } /** Reset attempt counter and start a fresh reconnect cycle (used after visibility/network recovery). */ resetAndStart(): void { this.attempt = 0; this.isReconnecting = false; if (this.timer) { clearTimeout(this.timer); this.timer = null; } this.start(); } /** Notify that reconnection attempt failed — schedules next. */ fail(): void { this.attempt++; if (this.attempt >= RECONNECT_MAX_ATTEMPTS) { this.stop(); return; } this.scheduleNext(); } /** Calculate delay: base * 2^attempt, capped at max, with jitter. */ private getDelay(): number { const exponential = RECONNECT_BASE_DELAY_MS * Math.pow(2, this.attempt); const capped = Math.min(exponential, RECONNECT_MAX_DELAY_MS); // Full jitter: random between 0 and capped (matches Rust LCG PRNG pattern) const jitter = Math.random() * capped; return jitter; } private scheduleNext(): void { const delay = this.getDelay(); this.timer = setTimeout(() => { this.onReconnect?.(); }, delay); } /** Get current reconnection state. */ getState(): ReconnectState { return { attempt: this.attempt, nextDelay: this.isReconnecting ? this.getDelay() : 0, isReconnecting: this.isReconnecting, lastAttemptAt: null, }; } }