/** * Optional {@link AbortSignal} and wall-clock timeouts for `ctx.execLine`, `ctx.readLine`, * and VFS I/O. Domains may be absent in Bare; treat as optional. */ /** @typedef {{ signal?: AbortSignal | null | undefined, timeoutMs?: number | null | undefined }} BareOsAbortOpts */ /** * @param {BareOsAbortOpts | null | undefined} opts * @returns {AbortSignal | undefined} */ export function effectiveAbortSignal(opts) { if (!opts || typeof opts !== 'object') return undefined const s = opts.signal if (s && typeof s.aborted === 'boolean') return s return undefined } /** * @param {BareOsAbortOpts | null | undefined} opts * @returns {number | undefined} positive timeout in ms */ export function effectiveTimeoutMs(opts) { if (!opts || typeof opts !== 'object') return undefined const n = Number(opts.timeoutMs) if (!Number.isFinite(n) || n <= 0) return undefined return Math.min(Math.floor(n), 86_400_000) } /** * Race `promise` with abort and/or timeout. On abort: reject with `DOMException` name AbortError if available. * @template T * @param {Promise} promise * @param {BareOsAbortOpts | null | undefined} opts * @param {string} [label] * @returns {Promise} */ export function raceWithAbortAndTimeout(promise, opts, label = 'operation') { const sig = effectiveAbortSignal(opts) const ms = effectiveTimeoutMs(opts) if (!sig && ms == null) return promise return new Promise((resolve, reject) => { let settled = false const finish = (fn) => { if (settled) return settled = true fn() } const onAbort = () => { finish(() => { try { const DOMException = globalThis.DOMException if (DOMException && typeof DOMException === 'function') { reject(new DOMException(`${label} aborted`, 'AbortError')) return } } catch { /* ignore */ } reject(new Error(`${label} aborted`)) }) } let timer = null if (ms != null) { timer = setTimeout(() => { finish(() => reject(new Error(`${label} timed out after ${ms}ms`))) }, ms) } if (sig) { if (sig.aborted) { onAbort() return } sig.addEventListener('abort', onAbort, { once: true }) } Promise.resolve(promise).then( (v) => finish(() => { if (timer) clearTimeout(timer) if (sig) sig.removeEventListener('abort', onAbort) resolve(v) }), (e) => finish(() => { if (timer) clearTimeout(timer) if (sig) sig.removeEventListener('abort', onAbort) reject(e) }) ) }) }