cahilfoley/utils

View on GitHub
src/async/makeCancelable.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
/**
 * @module async
 */

/** An error that indicates a promise has rejected because it was canceled */
export const canceledError = { isCanceled: true }

/** A promise that can have it's resolution cancelled */
export interface CancelableWrappedPromise<T> extends Promise<T> {
  cancel(): void
}

/**
 *
 * Allows the provided promise to be canceled after starting. This does not stop the promise from executing but will
 * cause it to reject with the value `{ isCanceled: true }` once it finishes, regardless of outcome.
 *
 * @param promise The promise that is executing
 * @return The cancelable version of the promise
 *
 * @example
 * ```typescript
 *
 * const promise = new Promise((res, rej) => {
 *   setTimeout(() => res('I finished!'), 3000)
 * })
 *
 * // Create a cancelable version of the promise
 * const cancelablePromise = makeCancelable(promise)
 *
 * // Stop the cancelable promise from resolving
 * cancelablePromise.cancel()
 *
 * promise
 *   .then(result => console.log('Normal', result)) // This will log `'I finished!'` after 3000ms
 *   .catch(err => console.log('Normal', err)) // Will reject as per normal
 *
 * cancelablePromise
 *   .then(result => console.log('Cancelable', result)) // Never fires, the promise will not resolve after being cancelled
 *   .catch(err => console.log('Cancelable', err)) // Resolves after 3000ms with the value `{ isCanceled: true }`
 * ```
 *
 */
export default function makeCancelable<T>(promise: Promise<T>): CancelableWrappedPromise<T> {
  let hasCanceled = false

  const cancelablePromise: Partial<CancelableWrappedPromise<T>> = new Promise((resolve, reject) => {
    promise.then(
      val => (hasCanceled ? reject(canceledError) : resolve(val)),
      error => (hasCanceled ? reject(canceledError) : reject(error)),
    )
  })

  cancelablePromise.cancel = () => {
    hasCanceled = true
  }

  return cancelablePromise as CancelableWrappedPromise<T>
}