47 lines
2.1 KiB
TypeScript
47 lines
2.1 KiB
TypeScript
// 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<T>(say: (elapsedMs: number) => void, fn: () => Promise<T>, o: HeartbeatOptions = {}): Promise<T> {
|
|
const h = heartbeat(say, o);
|
|
try { return await fn(); } finally { h.stop(); }
|
|
}
|