UNPKG

race-cancellation

Version:

Utilities for using Promise.race([task, cancellation]) for async/await code.

103 lines (93 loc) 2.95 kB
import { AsyncFn, CancelError, CancelExecutorFn, CancelReason, IsCancelledFn, RaceCancelFn, } from "./interfaces.js"; // we are casting the sentinel value to a unique symbol // to get === union narrowing const sentinelValue: unique symbol = { toString: () => "[cancel sentinel]", } as never; type AsyncCancelFn = () => Promise<CancelSentinelValue>; type CancelSentinelValue = typeof sentinelValue; /** * Create a {@link RaceCancelFn} function. * * @param isCancelled - a function that returns whether or not we are currently cancelled * @param executor - a callback to resolve the cancellation * @param cancelReason - an optional reason for the cancellation * @public */ export default function newRaceCancel( isCancelled: IsCancelledFn, executor: CancelExecutorFn, cancelReason?: (() => CancelReason | undefined) | CancelReason ): RaceCancelFn { // the async cancel executor should only run 0 or 1 times const memoizedAsyncCancel = memoizeAsyncCancel(isCancelled, executor); return async (asyncFnOrPromise) => throwIfCancelled( await raceCancel(asyncFnOrPromise, isCancelled, memoizedAsyncCancel), cancelReason ); } function raceCancel<TResult>( asyncFnOrPromise: AsyncFn<TResult> | PromiseLike<TResult>, isCancelled: IsCancelledFn, memoizedAsyncCancel: AsyncCancelFn ): Promise<TResult | CancelSentinelValue> { return typeof asyncFnOrPromise === "function" ? isCancelled() ? memoizedAsyncCancel() : Promise.race([asyncFnOrPromise(), memoizedAsyncCancel()]) : Promise.race([asyncFnOrPromise, memoizedAsyncCancel()]); } function throwIfCancelled<TResult>( result: TResult | CancelSentinelValue, cancelReason?: (() => CancelReason | undefined) | CancelReason ): TResult { if (result === sentinelValue) { throw intoCancelError(cancelReason); } return result; } function memoizeAsyncCancel( isCancelled: IsCancelledFn, executor: CancelExecutorFn ): AsyncCancelFn { let promise: Promise<CancelSentinelValue> | undefined; return () => { if (promise === undefined) { promise = isCancelled() ? Promise.resolve(sentinelValue) : waitForCancel(executor); } return promise; }; } async function waitForCancel( cancelExecutor: CancelExecutorFn ): Promise<CancelSentinelValue> { await new Promise(cancelExecutor); return sentinelValue; } function intoCancelError( cancelReason?: (() => CancelReason | undefined) | CancelReason ): CancelError<string> { if (typeof cancelReason === "function") { cancelReason = cancelReason(); } if (cancelReason === undefined || typeof cancelReason === "string") { return newCancelError(cancelReason); } return cancelReason; } function newCancelError(message = "The operation was cancelled"): CancelError { const cancelError = new Error(message) as CancelError; cancelError.name = "CancelError"; cancelError.isCancelled = true; return cancelError; }