@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
102 lines (94 loc) • 2.9 kB
text/typescript
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)))
}
}