// Progress for long blocking steps (uploads, store processing waits, external tools): a heartbeat line every few // seconds so a run is never silent for minutes, and the size / elapsed formats those lines use. The timer is // injectable (Ticker), so tests drive beats by hand instead of sleeping. /** Calls fn every ms until the returned stop function is called. */ export type Ticker = (fn: () => void, ms: number) => () => void; export const realTicker: Ticker = (fn, ms) => { const t = setInterval(fn, ms); (t as { unref?: () => void }).unref?.(); return () => clearInterval(t); }; /** Default heartbeat interval of uploads and waits. */ export const HEARTBEAT_MS = 20_000; /** 4.2 MB, 312 MB (whole MB from 10 MB up). */ export const mb = (bytes: number): string => `${(bytes / 1048576).toFixed(bytes < 10 * 1048576 ? 1 : 0)} MB`; /** 45s, 4m30s, 1h02m. */ export function elapsed(ms: number): string { const s = Math.max(0, Math.round(ms / 1000)); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); if (m < 60) return `${m}m${String(s % 60).padStart(2, '0')}s`; return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}m`; } /** "312/890 MB (35%)". */ export const share = (done: number, total: number): string => `${(done / 1048576).toFixed(total < 10 * 1048576 ? 1 : 0)}/${mb(total)} (${total ? Math.floor((done / total) * 100) : 100}%)`; export interface HeartbeatOptions { everyMs?: number; ticker?: Ticker; now?: () => number } /** Runs `say(elapsedMs)` every everyMs while a step runs; stop() ends it. */ export function heartbeat(say: (elapsedMs: number) => void, o: HeartbeatOptions = {}): { stop: () => void } { const now = o.now ?? Date.now; const started = now(); const stop = (o.ticker ?? realTicker)(() => say(now() - started), o.everyMs ?? HEARTBEAT_MS); return { stop }; } /** Runs fn with a heartbeat; the heartbeat stops however fn ends. */ export async function withHeartbeat(say: (elapsedMs: number) => void, fn: () => Promise, o: HeartbeatOptions = {}): Promise { const h = heartbeat(say, o); try { return await fn(); } finally { h.stop(); } }