import { WS_CODES, WsError } from "../error-codes/error-codes.js"; /** * Policy describing the engine's ping/pong cadence and failure threshold. * * Defaults match design doc ยง11.4: ping every 30000ms, declare the * connection dead after two consecutive missed pongs. */ export interface HeartbeatPolicy { readonly heartbeatMs: number; readonly maxMissedPongs: number; } /** Default policy applied when no override is supplied. */ export const DEFAULT_HEARTBEAT_POLICY: HeartbeatPolicy = Object.freeze({ heartbeatMs: 30000, maxMissedPongs: 2, }); /** Action returned by {@link evaluateHeartbeat}. */ export type HeartbeatAction = "ok" | "ping" | "abort"; /** * Stateless evaluator: given the engine's current view of "ms since last * pong" + "consecutive missed pongs", returns the action the engine must * take next. * * - `ok` โ€” within tolerance, nothing to do. * - `ping` โ€” heartbeat interval elapsed; engine should send a ping. * - `abort` โ€” `maxMissedPongs` reached; engine MUST close the connection * with `ABNORMAL` and emit `WS.HEARTBEAT_FAILED`. */ export function evaluateHeartbeat( msSinceLastPong: number, consecutiveMissedPongs: number, policy: HeartbeatPolicy = DEFAULT_HEARTBEAT_POLICY ): HeartbeatAction { if (consecutiveMissedPongs >= policy.maxMissedPongs) { return "abort"; } if (msSinceLastPong >= policy.heartbeatMs) { return "ping"; } return "ok"; } /** Helper that throws the canonical `WsError`. */ export function raiseHeartbeatFailed(connectionId: string): never { throw new WsError( WS_CODES.HEARTBEAT_FAILED, `connection ${connectionId} failed heartbeat: two missed pongs` ); }