102 lines
2.8 KiB
Plaintext
102 lines
2.8 KiB
Plaintext
import { TaskAbortError } from './exceptions'
|
|
import type { AbortSignalWithReason, TaskResult } from './types'
|
|
import { addAbortSignalListener, catchRejection, noop } from './utils'
|
|
|
|
/**
|
|
* Synchronously raises {@link TaskAbortError} if the task tied to the input `signal` has been cancelled.
|
|
* @param signal
|
|
* @param reason
|
|
* @see {TaskAbortError}
|
|
*/
|
|
export const validateActive = (signal: AbortSignal): void => {
|
|
if (signal.aborted) {
|
|
const { reason } = signal as AbortSignalWithReason<string>
|
|
throw new TaskAbortError(reason)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a race between the promise(s) and the AbortSignal
|
|
* This avoids `Promise.race()`-related memory leaks:
|
|
* https://github.com/nodejs/node/issues/17469#issuecomment-349794909
|
|
*/
|
|
export function raceWithSignal<T>(
|
|
signal: AbortSignalWithReason<string>,
|
|
promise: Promise<T>,
|
|
): Promise<T> {
|
|
let cleanup = noop
|
|
return new Promise<T>((resolve, reject) => {
|
|
const notifyRejection = () => reject(new TaskAbortError(signal.reason))
|
|
|
|
if (signal.aborted) {
|
|
notifyRejection()
|
|
return
|
|
}
|
|
|
|
cleanup = addAbortSignalListener(signal, notifyRejection)
|
|
promise.finally(() => cleanup()).then(resolve, reject)
|
|
}).finally(() => {
|
|
// after this point, replace `cleanup` with a noop, so there is no reference to `signal` any more
|
|
cleanup = noop
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Runs a task and returns promise that resolves to {@link TaskResult}.
|
|
* Second argument is an optional `cleanUp` function that always runs after task.
|
|
*
|
|
* **Note:** `runTask` runs the executor in the next microtask.
|
|
* @returns
|
|
*/
|
|
export const runTask = async <T>(
|
|
task: () => Promise<T>,
|
|
cleanUp?: () => void,
|
|
): Promise<TaskResult<T>> => {
|
|
try {
|
|
await Promise.resolve()
|
|
const value = await task()
|
|
return {
|
|
status: 'ok',
|
|
value,
|
|
}
|
|
} catch (error: any) {
|
|
return {
|
|
status: error instanceof TaskAbortError ? 'cancelled' : 'rejected',
|
|
error,
|
|
}
|
|
} finally {
|
|
cleanUp?.()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given an input `AbortSignal` and a promise returns another promise that resolves
|
|
* as soon the input promise is provided or rejects as soon as
|
|
* `AbortSignal.abort` is `true`.
|
|
* @param signal
|
|
* @returns
|
|
*/
|
|
export const createPause = <T>(signal: AbortSignal) => {
|
|
return (promise: Promise<T>): Promise<T> => {
|
|
return catchRejection(
|
|
raceWithSignal(signal, promise).then((output) => {
|
|
validateActive(signal)
|
|
return output
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given an input `AbortSignal` and `timeoutMs` returns a promise that resolves
|
|
* after `timeoutMs` or rejects as soon as `AbortSignal.abort` is `true`.
|
|
* @param signal
|
|
* @returns
|
|
*/
|
|
export const createDelay = (signal: AbortSignal) => {
|
|
const pause = createPause<void>(signal)
|
|
return (timeoutMs: number): Promise<void> => {
|
|
return pause(new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)))
|
|
}
|
|
}
|