/** * Time-related helpers shared across packages. * * @module */ import { logger } from './logger.js'; export type TimedOptions = { /** Receives the success/failure timing line. Defaults to the shared Enbox logger. */ log?: (message: string) => void; }; const durationUnitMultipliers = new Map([ ['ms', 1], ['msec', 1], ['msecs', 1], ['millisecond', 1], ['milliseconds', 1], ['s', 1000], ['sec', 1000], ['secs', 1000], ['second', 1000], ['seconds', 1000], ['m', 60 * 1000], ['min', 60 * 1000], ['mins', 60 * 1000], ['minute', 60 * 1000], ['minutes', 60 * 1000], ['h', 60 * 60 * 1000], ['hr', 60 * 60 * 1000], ['hrs', 60 * 60 * 1000], ['hour', 60 * 60 * 1000], ['hours', 60 * 60 * 1000], ['d', 24 * 60 * 60 * 1000], ['day', 24 * 60 * 60 * 1000], ['days', 24 * 60 * 60 * 1000], ['w', 7 * 24 * 60 * 60 * 1000], ['week', 7 * 24 * 60 * 60 * 1000], ['weeks', 7 * 24 * 60 * 60 * 1000], ['y', 365.25 * 24 * 60 * 60 * 1000], ['yr', 365.25 * 24 * 60 * 60 * 1000], ['yrs', 365.25 * 24 * 60 * 60 * 1000], ['year', 365.25 * 24 * 60 * 60 * 1000], ['years', 365.25 * 24 * 60 * 60 * 1000], ]); /** * Returns a high-resolution monotonic timestamp in milliseconds. * * Uses `performance.now()` when available so elapsed durations are not * affected by wall-clock changes. Falls back to `Date.now()` in runtimes * that do not expose `performance`. */ export function nowMs(): number { if (typeof performance !== 'undefined' && typeof performance.now === 'function') { return performance.now(); } return Date.now(); } /** * Parses a human-readable duration string into milliseconds. * * Accepted units: milliseconds (`ms`), seconds (`s`), minutes (`m`), hours (`h`), * days (`d`), weeks (`w`), and years (`y`), including their common long-form * aliases such as `minutes` and `hours`. A bare numeric string is treated as * milliseconds. * * @throws Error if the input is empty, negative, non-finite, or uses an unknown unit. */ export function parseDurationInMilliseconds(duration: string): number { const match = /^((?:\d+|\d*\.\d+))\s*([a-zA-Z]+)?$/.exec(duration.trim()); if (match === null) { throw new Error(`Invalid duration: '${duration}'`); } const durationUnit = match[2]?.toLowerCase() ?? 'ms'; const multiplier = durationUnitMultipliers.get(durationUnit); if (multiplier === undefined) { throw new Error(`Invalid duration unit: '${durationUnit}'`); } const durationInMilliseconds = Number(match[1]) * multiplier; if (!Number.isFinite(durationInMilliseconds)) { throw new TypeError(`Invalid duration: '${duration}'`); } return durationInMilliseconds; } /** * Times an async operation and logs a single success/failure duration line. * * The label is intentionally caller-defined so packages can include their own * log namespace, e.g. `[connect.perf] response.sign`. */ export async function timed( label: string, fn: () => Promise, { log = logger.log.bind(logger) }: TimedOptions = {} ): Promise { const start = nowMs(); try { const result = await fn(); const elapsed = nowMs() - start; log(`${label} ok in ${elapsed.toFixed(1)}ms`); return result; } catch (err) { const elapsed = nowMs() - start; log(`${label} fail in ${elapsed.toFixed(1)}ms`); throw err; } } /** Largest delay accepted by the native timer APIs without overflow coercion. */ export const MAX_TIMER_DELAY_MS = 2_147_483_647; /** * Returns a promise that resolves after the given duration or rejects when * `signal` aborts. Long waits are split into native-timer-sized chunks so an * oversized delay cannot be coerced by the runtime into an immediate timer. * * Use this anywhere you would otherwise inline * `new Promise(resolve => setTimeout(resolve, ms))` — retry backoff, * polling intervals, throttled tests, etc. Centralizing the idiom keeps * call sites readable and ensures every retry/poll path has one obvious * primitive to reach for. * * Negative or zero durations resolve on the next macrotask via * `setTimeout(_, 0)`; they do not throw. * * @param durationInMilliseconds - How long to wait, in milliseconds. * @param signal - Optional cancellation signal for the wait. * @returns A promise that resolves after the duration elapses. * * @example * ```ts * import { sleep } from '@enbox/common'; * * await sleep(250); // pause for 250ms * ``` */ export function sleep(durationInMilliseconds: number, signal?: AbortSignal): Promise { if (!Number.isFinite(durationInMilliseconds)) { return Promise.reject(new TypeError('sleep duration must be finite')); } if (signal?.aborted === true) { return Promise.reject(signal.reason); } let remaining = Math.max(0, durationInMilliseconds); return new Promise((resolve, reject) => { let timer: ReturnType | undefined; const onAbort = (): void => { if (timer !== undefined) { clearTimeout(timer); } reject(signal?.reason); }; const schedule = (): void => { const delay = Math.min(remaining, MAX_TIMER_DELAY_MS); timer = setTimeout((): void => { remaining -= delay; if (remaining > 0) { schedule(); return; } signal?.removeEventListener('abort', onAbort); resolve(); }, delay); }; signal?.addEventListener('abort', onAbort, { once: true }); schedule(); }); }