NaturalCycles/js-lib

View on GitHub
src/promise/pTimeout.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
import type { ErrorData } from '../error/error.model'
import { _errorDataAppend, TimeoutError } from '../error/error.util'
import type { AnyAsyncFunction, NumberOfMilliseconds } from '../types'

export interface PTimeoutOptions {
  /**
   * Timeout in milliseconds.
   *
   * If 0 is passed - the function would be executed right away, with no timeout.
   */
  timeout: NumberOfMilliseconds

  /**
   * If set - will be included in the error message.
   * Can be used to identify the place in the code that failed.
   */
  name?: string

  /**
   * If provided - will be called INSTEAD of throwing an error.
   * Can be used to thrown a custom error OR resolve a promise without throwing.
   *
   * err (which is TimeoutError) is passed as an argument for convenience, so it can
   * be logged or such. You don't have to consume it in any way though.
   */
  onTimeout?: (err: TimeoutError) => any

  /**
   * If passed - fakeError.stack will be used as a stacktrace.
   * This is to "keep stacktrace" when pTimeout is called from another
   * function (like pRetry).
   */
  fakeError?: Error

  /**
   * Will be merged with `err.data` object.
   */
  errorData?: ErrorData
}

/**
 * Decorates a Function with a timeout.
 * Returns a decorated Function.
 *
 * Throws an Error if the Function is not resolved in a certain time.
 * If the Function rejects - passes this rejection further.
 */
export function pTimeoutFn<T extends AnyAsyncFunction>(fn: T, opt: PTimeoutOptions): T {
  opt.name ||= fn.name

  if (!opt.timeout) {
    return fn
  }

  return async function pTimeoutInternalFn(this: any, ...args: any[]) {
    return await pTimeout(() => fn.apply(this, args), opt)
  } as T
}

/**
 * Decorates a Function with a timeout and immediately calls it.
 *
 * Throws an Error if the Function is not resolved in a certain time.
 * If the Function rejects - passes this rejection further.
 */
export async function pTimeout<T>(fn: AnyAsyncFunction<T>, opt: PTimeoutOptions): Promise<T> {
  if (!opt.timeout) {
    // short-circuit to direct execution if 0 timeout is passed
    return await fn()
  }

  const { timeout, name = fn.name || 'pTimeout function', onTimeout } = opt
  const fakeError = opt.fakeError || new Error('TimeoutError')

  // eslint-disable-next-line no-async-promise-executor
  return await new Promise(async (resolve, reject) => {
    // Prepare the timeout timer
    const timer = setTimeout(() => {
      const err = new TimeoutError(`"${name}" timed out after ${timeout} ms`, opt.errorData)
      // keep original stack
      err.stack = fakeError.stack!.replace('Error: TimeoutError', 'TimeoutError: ' + err.message)

      if (onTimeout) {
        try {
          resolve(onTimeout(err))
        } catch (err: any) {
          // keep original stack
          err.stack = fakeError.stack!.replace('Error: TimeoutError', err.name + ': ' + err.message)
          reject(_errorDataAppend(err, opt.errorData))
        }
        return
      }

      reject(err)
    }, timeout)

    // Execute the Function
    try {
      resolve(await fn())
    } catch (err) {
      reject(err)
    } finally {
      clearTimeout(timer)
    }
  })
}