85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
/**
|
|
* Abort-responsive sleep. Resolves after `ms` milliseconds, or immediately
|
|
* when `signal` aborts (so backoff loops don't block shutdown).
|
|
*
|
|
* By default, abort resolves silently; the caller should check
|
|
* `signal.aborted` after the await. Pass `throwOnAbort: true` to have
|
|
* abort reject — useful when the sleep is deep inside a retry loop
|
|
* and you want the rejection to bubble up and cancel the whole operation.
|
|
*
|
|
* Pass `abortError` to customize the rejection error (implies
|
|
* `throwOnAbort: true`). Useful for retry loops that catch a specific
|
|
* error class (e.g. `APIUserAbortError`).
|
|
*/
|
|
export function sleep(
|
|
ms: number,
|
|
signal?: AbortSignal,
|
|
opts?: { throwOnAbort?: boolean; abortError?: () => Error; unref?: boolean },
|
|
): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
// Check aborted state BEFORE setting up the timer. If we defined
|
|
// onAbort first and called it synchronously here, it would reference
|
|
// `timer` while still in the Temporal Dead Zone.
|
|
if (signal?.aborted) {
|
|
if (opts?.throwOnAbort || opts?.abortError) {
|
|
void reject(opts.abortError?.() ?? new Error('aborted'))
|
|
} else {
|
|
void resolve()
|
|
}
|
|
return
|
|
}
|
|
const timer = setTimeout(
|
|
(signal, onAbort, resolve) => {
|
|
signal?.removeEventListener('abort', onAbort)
|
|
void resolve()
|
|
},
|
|
ms,
|
|
signal,
|
|
onAbort,
|
|
resolve,
|
|
)
|
|
function onAbort(): void {
|
|
clearTimeout(timer)
|
|
if (opts?.throwOnAbort || opts?.abortError) {
|
|
void reject(opts.abortError?.() ?? new Error('aborted'))
|
|
} else {
|
|
void resolve()
|
|
}
|
|
}
|
|
signal?.addEventListener('abort', onAbort, { once: true })
|
|
if (opts?.unref) {
|
|
timer.unref()
|
|
}
|
|
})
|
|
}
|
|
|
|
function rejectWithTimeout(reject: (e: Error) => void, message: string): void {
|
|
reject(new Error(message))
|
|
}
|
|
|
|
/**
|
|
* Race a promise against a timeout. Rejects with `Error(message)` if the
|
|
* promise doesn't settle within `ms`. The timeout timer is cleared when
|
|
* the promise settles (no dangling timer) and unref'd so it doesn't
|
|
* block process exit.
|
|
*
|
|
* Note: this doesn't cancel the underlying work — if the promise is
|
|
* backed by a runaway async operation, that keeps running. This just
|
|
* returns control to the caller.
|
|
*/
|
|
export function withTimeout<T>(
|
|
promise: Promise<T>,
|
|
ms: number,
|
|
message: string,
|
|
): Promise<T> {
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
// eslint-disable-next-line no-restricted-syntax -- not a sleep: REJECTS after ms (timeout guard)
|
|
timer = setTimeout(rejectWithTimeout, ms, reject, message)
|
|
if (typeof timer === 'object') timer.unref?.()
|
|
})
|
|
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
if (timer !== undefined) clearTimeout(timer)
|
|
})
|
|
}
|