97 lines
2.6 KiB
JavaScript
97 lines
2.6 KiB
JavaScript
/**
|
|
* 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<T>} promise
|
|
* @param {BareOsAbortOpts | null | undefined} opts
|
|
* @param {string} [label]
|
|
* @returns {Promise<T>}
|
|
*/
|
|
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)
|
|
})
|
|
)
|
|
})
|
|
}
|