/** * Remote Substrate, Reconnect Engine * * Implements reconnect with handshake tokens, epoch tracking, * replay from last acknowledged offset, and idempotent command submission. * * The reconnect engine drives the transport state machine: * disconnected → initializing → authenticating → connected → syncing → degraded * → reconnecting → ... (retry loop) * → terminal_failure (max attempts exceeded or non-retryable error) */ import type { HandshakeToken, ReplayConfig, DurableIdentity, RetryPolicy, TransportErrorCategory, NegotiatedProtocol } from './types.js'; /** Outcome of a connect/reconnect attempt. */ export type ConnectOutcome = { readonly success: true; readonly token: HandshakeToken; readonly epoch: number; readonly replayFromOffset: number; /** Negotiated protocol agreed during handshake. */ readonly negotiatedProtocol: NegotiatedProtocol; } | { readonly success: false; readonly category: TransportErrorCategory; readonly error: string; readonly retryable: boolean; /** * Unsupported code when the failure is due to version mismatch. * Absent for auth/network failures. */ readonly unsupportedCode?: 'major_version_mismatch' | 'peer_version_too_old' | 'peer_version_unsupported'; }; /** * Adapter interface, callers implement the actual transport operations. * * The reconnect engine delegates real I/O to this adapter, keeping itself * transport-agnostic (WebSocket, HTTP, stdio all implement the same adapter). */ export interface TransportAdapter { /** * Attempt to establish a connection and perform the handshake. * * Implementations must: * 1. Open the transport channel * 2. Send HANDSHAKE_INIT with identity, epoch, lastAckedOffset, authToken, * and the local `protocolVersion` from CURRENT_PROTOCOL_VERSION * 3. Wait for HANDSHAKE_ACCEPT or HANDSHAKE_REJECT * 4. On HANDSHAKE_ACCEPT, extract `negotiatedProtocol` and return it in the outcome * 5. On HANDSHAKE_REJECT with an `unsupportedCode`, return success=false with * that code, the engine will treat it as a terminal (non-retryable) failure * * @param identity - Stable durable identity to present. * @param lastAckedOffset - Offset to replay from. * @param authToken - Bearer token from the AuthProvider. * @returns ConnectOutcome, success with token+negotiatedProtocol or failure with category. */ connect(identity: DurableIdentity, lastAckedOffset: number, authToken: string): Promise; /** Disconnect the transport cleanly. */ disconnect(): Promise; /** * Called after a successful handshake to replay messages since lastAckedOffset. * The adapter fetches and delivers replayed messages via its normal receive path. */ requestReplay(fromOffset: number, maxCount: number): Promise; } /** Lifecycle event callbacks fired by the reconnect engine. */ export interface ReconnectEngineCallbacks { /** Called when version negotiation produces a downgrade. Unsupported = terminal failure. */ onVersionNegotiated?(protocol: NegotiatedProtocol): void; /** Called when the transport enters 'initializing'. */ onInitializing?(attempt: number): void; /** Called when authentication is in progress. */ onAuthenticating?(): void; /** Called when the connection is successfully established. */ onConnected(token: HandshakeToken, epoch: number): void; /** Called when state sync is in progress after connect. */ onSyncing?(): void; /** Called when a reconnect attempt is scheduled. */ onReconnecting?(attempt: number, maxAttempts: number, delayMs: number): void; /** Called when the transport is disconnected (willRetry indicates intent). */ onDisconnected?(reason?: string, willRetry?: boolean): void; /** Called when all retries are exhausted, terminal failure. */ onTerminalFailure(error: string): void; } /** * ReconnectEngine, manages the full reconnect lifecycle with backoff and replay. * * Usage: * ```ts * const engine = new ReconnectEngine(adapter, identity, replayConfig, callbacks); * await engine.connect(); // blocks until connected or terminal failure * // After a failure detected externally: * await engine.reconnect(); // drives the retry loop * engine.dispose(); // cancel any pending reconnect * ``` */ export declare class ReconnectEngine { private readonly adapter; private readonly identity; private readonly callbacks?; private readonly _policy; private readonly _replay; private _handshakeToken; private _epoch; private _lastAckedOffset; private _attempts; private _disposed; private _pendingTimer; /** Negotiated protocol from the last successful handshake. */ private _negotiatedProtocol; constructor(adapter: TransportAdapter, identity: DurableIdentity, replayConfig?: Partial, callbacks?: ReconnectEngineCallbacks | undefined, reconnectPolicy?: Partial); /** Current handshake token (undefined until first successful connect). */ get handshakeToken(): HandshakeToken | undefined; /** Current server epoch. */ get epoch(): number; /** Offset of the last acknowledged message. */ get lastAckedOffset(): number; /** Number of reconnect attempts since last successful connect. */ get attempts(): number; /** * The negotiated protocol from the last successful handshake. * Undefined until the first successful connect. */ get negotiatedProtocol(): NegotiatedProtocol | undefined; /** * Update the last acknowledged offset. * Call this whenever an ack is sent or a message is durably processed. * * @param offset - The offset to record as acknowledged. */ ackOffset(offset: number): void; /** * Perform the initial connect attempt. * * @param authToken - Bearer token from the AuthProvider. * @returns True on success, false on terminal failure. */ connect(authToken: string): Promise; /** * Drive the reconnect retry loop. * * Attempts reconnection up to `policy.maxAttempts` times using * exponential backoff with jitter. Calls callbacks at each state transition. * * @param getAuthToken - Called before each attempt to get a fresh token. * @returns True if eventually connected, false on terminal failure. */ reconnect(getAuthToken: () => Promise): Promise; /** Cancel any pending reconnect timer and mark engine as disposed. */ dispose(): void; private _attemptConnect; private _sleep; } /** * Generate an idempotent submission key for a command. * * The key combines the session ID with a random UUID so that retried * submissions with the same key can be deduplicated by the server. * * @param sessionId - Durable session ID. * @returns A stable key string for use as `idempotencyKey`. */ export declare function generateIdempotencyKey(sessionId: string): string; //# sourceMappingURL=reconnect.d.ts.map