import { randomUUID } from "node:crypto"; import type { ClawdbotConfig } from "../config/config.js"; export type ReconnectPolicy = { initialMs: number; maxMs: number; factor: number; jitter: number; maxAttempts: number; }; export const DEFAULT_HEARTBEAT_SECONDS = 60; export const DEFAULT_RECONNECT_POLICY: ReconnectPolicy = { initialMs: 2_000, maxMs: 30_000, factor: 1.8, jitter: 0.25, maxAttempts: 12, }; const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val)); export function resolveHeartbeatSeconds( cfg: ClawdbotConfig, overrideSeconds?: number, ): number { const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds; if (typeof candidate === "number" && candidate > 0) return candidate; return DEFAULT_HEARTBEAT_SECONDS; } export function resolveReconnectPolicy( cfg: ClawdbotConfig, overrides?: Partial, ): ReconnectPolicy { const reconnectOverrides = cfg.web?.reconnect ?? {}; const overrideConfig = overrides ?? {}; const merged = { ...DEFAULT_RECONNECT_POLICY, ...reconnectOverrides, ...overrideConfig, } as ReconnectPolicy; merged.initialMs = Math.max(250, merged.initialMs); merged.maxMs = Math.max(merged.initialMs, merged.maxMs); merged.factor = clamp(merged.factor, 1.1, 10); merged.jitter = clamp(merged.jitter, 0, 1); merged.maxAttempts = Math.max(0, Math.floor(merged.maxAttempts)); return merged; } export function computeBackoff(policy: ReconnectPolicy, attempt: number) { const base = policy.initialMs * policy.factor ** Math.max(attempt - 1, 0); const jitter = base * policy.jitter * Math.random(); return Math.min(policy.maxMs, Math.round(base + jitter)); } export function sleepWithAbort(ms: number, abortSignal?: AbortSignal) { if (ms <= 0) return Promise.resolve(); return new Promise((resolve, reject) => { const timer = setTimeout(() => { cleanup(); resolve(); }, ms); const onAbort = () => { cleanup(); reject(new Error("aborted")); }; const cleanup = () => { clearTimeout(timer); abortSignal?.removeEventListener("abort", onAbort); }; if (abortSignal) { abortSignal.addEventListener("abort", onAbort, { once: true }); } }); } export function newConnectionId() { return randomUUID(); }