type RealtimeEventHandler = { handle(event: Event): void; }["handle"]; export interface RealtimeSocket { onclose: RealtimeEventHandler<{ type: string }> | null; onerror: RealtimeEventHandler<{ type: string }> | null; onmessage: RealtimeEventHandler<{ data: unknown }> | null; onopen: RealtimeEventHandler<{ type: string }> | null; close: () => void; send: (data: string) => void; } export type RealtimeSocketConstructor = new (url: string) => RealtimeSocket; export interface RealtimeConnectionController { start: () => void; stop: () => void; } export interface RealtimeConnectionAttempt { registrationMessage: string; url: string; } export interface RealtimeConnectionOptions { clearTimeout?: typeof clearTimeout; /** * Observes an asynchronous transport failure after `start()` returns, * including automatic reconnect setup and registration send failures. * Recovery remains owned by the connection controller. */ onAsyncTransportError?: (error: unknown) => void; /** * Reports an automatic reconnect setup failure that stops reconnection. * Synchronous failures during direct `start()` still throw to the caller. */ onReconnectSetupError?: (error: unknown) => void; /** * Called after a registration frame is sent successfully and the transport * has established its connected state and next refresh schedule. */ onRegistrationSent?: (registrationMessage: string) => void; onMessage: (data: unknown) => void; onSocketError?: (event: { type: string }) => void; reconnectDelayMs?: number; registrationRefreshMs?: number | null; resolveConnectionAttempt: () => RealtimeConnectionAttempt | null; setTimeout?: typeof setTimeout; shouldAttemptConnection?: () => boolean; socketConstructor: RealtimeSocketConstructor; } type Timer = ReturnType; type ConnectionState = | { status: "stopped" } | { registrationMessage: string; socket: RealtimeSocket; status: "connecting"; } | { registrationRefreshTimer: Timer | null; registrationRefreshToken: object | null; socket: RealtimeSocket; status: "connected"; } | { reconnectTimer: Timer; reconnectToken: object; status: "reconnecting"; }; const DEFAULT_RECONNECT_DELAY_MS = 1000; export const createRealtimeConnectionController = ({ clearTimeout: clearTimeoutRef = clearTimeout, onMessage, onAsyncTransportError, onReconnectSetupError, onRegistrationSent, onSocketError, reconnectDelayMs = DEFAULT_RECONNECT_DELAY_MS, registrationRefreshMs = null, resolveConnectionAttempt, setTimeout: setTimeoutRef = setTimeout, shouldAttemptConnection = () => true, socketConstructor, }: RealtimeConnectionOptions): RealtimeConnectionController => { assertReconnectDelay(reconnectDelayMs); assertRegistrationRefreshMs(registrationRefreshMs); let state: ConnectionState = { status: "stopped" }; const clearStateTimer = (currentState: ConnectionState): void => { if ( currentState.status === "connected" && currentState.registrationRefreshTimer !== null ) { clearTimeoutRef(currentState.registrationRefreshTimer); } if (currentState.status === "reconnecting") { clearTimeoutRef(currentState.reconnectTimer); } }; const tryCloseRecordedSocket = (recordedState: ConnectionState): void => { if ( recordedState.status === "connecting" || recordedState.status === "connected" ) { try { recordedState.socket.close(); } catch { // Cleanup cannot replace the transport failure that made the socket unusable. } } }; const notifyAsyncTransportError = (error: unknown): void => { try { onAsyncTransportError?.(error); } catch { // Error observers cannot take ownership of the transport callback. } }; const stopAfterReconnectSetupError = (error: unknown): void => { const previousState = state; state = { status: "stopped" }; tryCloseRecordedSocket(previousState); notifyAsyncTransportError(error); try { onReconnectSetupError?.(error); } catch { // Error observers cannot take ownership of the transport callback. } }; const notifyRegistrationSent = (registrationMessage: string): void => { try { onRegistrationSent?.(registrationMessage); } catch { // Policy observers cannot take ownership of the transport callback. } }; const scheduleReconnect = (): void => { try { clearStateTimer(state); if (!shouldAttemptConnection()) { state = { status: "stopped" }; return; } const reconnectToken = {}; const reconnectTimer = setTimeoutRef(() => { if ( state.status !== "reconnecting" || state.reconnectToken !== reconnectToken ) { return; } try { connect(); } catch (error) { stopAfterReconnectSetupError(error); } }, reconnectDelayMs); state = { reconnectTimer, reconnectToken, status: "reconnecting" }; } catch (error) { stopAfterReconnectSetupError(error); } }; const recoverAfterRegistrationSendError = (error: unknown): void => { const previousState = state; state = { status: "stopped" }; clearStateTimer(previousState); tryCloseRecordedSocket(previousState); scheduleReconnect(); notifyAsyncTransportError(error); }; const scheduleRegistrationRefresh = ( socket: RealtimeSocket, registrationMessage: string, ): void => { const registrationRefreshToken = {}; const registrationRefreshTimer = registrationRefreshMs === null ? null : setTimeoutRef(() => { if ( state.status !== "connected" || state.socket !== socket || state.registrationRefreshToken !== registrationRefreshToken ) { return; } state = { registrationRefreshTimer: null, registrationRefreshToken: null, socket, status: "connected", }; try { socket.send(registrationMessage); scheduleRegistrationRefresh(socket, registrationMessage); } catch (error) { recoverAfterRegistrationSendError(error); return; } notifyRegistrationSent(registrationMessage); }, registrationRefreshMs); clearStateTimer(state); state = { registrationRefreshTimer, registrationRefreshToken: registrationRefreshTimer === null ? null : registrationRefreshToken, socket, status: "connected", }; }; const connect = (): void => { clearStateTimer(state); if (!shouldAttemptConnection()) { state = { status: "stopped" }; return; } const attempt = resolveConnectionAttempt(); if (attempt === null) { state = { status: "stopped" }; return; } const socket = new socketConstructor(attempt.url); state = { registrationMessage: attempt.registrationMessage, socket, status: "connecting", }; try { socket.onopen = () => { if (state.status !== "connecting" || state.socket !== socket) { return; } const { registrationMessage } = state; try { socket.send(registrationMessage); scheduleRegistrationRefresh(socket, registrationMessage); } catch (error) { recoverAfterRegistrationSendError(error); return; } notifyRegistrationSent(registrationMessage); }; socket.onmessage = (event) => { if (state.status === "connected" && state.socket === socket) { onMessage(event.data); } }; socket.onclose = () => { if ( (state.status === "connecting" || state.status === "connected") && state.socket === socket ) { scheduleReconnect(); } }; socket.onerror = (event) => { if ( (state.status === "connecting" || state.status === "connected") && state.socket === socket ) { onSocketError?.(event); } }; } catch (error) { if (state.status === "connecting" && state.socket === socket) { const failedState = state; state = { status: "stopped" }; tryCloseRecordedSocket(failedState); } throw error; } }; const start = (): void => { if (state.status === "stopped" || state.status === "reconnecting") { connect(); } }; const stop = (): void => { const previousState = state; state = { status: "stopped" }; clearStateTimer(previousState); if ( previousState.status === "connecting" || previousState.status === "connected" ) { previousState.socket.close(); } }; return { start, stop }; }; const assertReconnectDelay = (reconnectDelayMs: number): void => { if (!Number.isFinite(reconnectDelayMs) || reconnectDelayMs < 0) { throw new TypeError("Realtime reconnect delay is invalid."); } }; const assertRegistrationRefreshMs = ( registrationRefreshMs: number | null, ): void => { if ( registrationRefreshMs !== null && (!Number.isFinite(registrationRefreshMs) || registrationRefreshMs <= 0) ) { throw new TypeError("Realtime registration refresh interval is invalid."); } };