Files
suaveplan/packages/api-stack/api/api-ws/src/heartbeat/heartbeat.ts
2026-05-27 23:29:53 +01:00

54 lines
1.7 KiB
TypeScript

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`
);
}