PoolRequest.js

const { TimeoutError, ErrorMessages } = require('./errors');

/**
 * A wrapped promise with a timeout
 *
 * @example
 * const requestQueue = []
 *
 * // A function that creates requests and stores them in an array
 * function createRequest(timeoutInMs) {
 *  const request = new PoolRequest(timeoutInMs)
 *  requestQueue.push(request) // store the request so it can be resolved later
 *  return request.getPromise() // only return the promise to the caller of makeRequest
 * }
 *
 * // A function that creates an object and tries to resolve a request
 * function createThingyAndTryResolveRequest(){
 *  const thingy = new Thingy()
 *  const request = requestQueue.unshift()
 *  if(request && !request.didTimeout()) request.resolve(thingy)
 * }
 *
 * @template {object} T
 */
class PoolRequest {
  /**
   *
   * @param {number | null} [timeoutInMs=null] Number of milliseconds to wait before request times out. `null` will disable the timeout
   * @param {function():number} [getTimestamp=Date.now] Function to get the current timestamp. See [Date.now()]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now}
   */
  constructor(timeoutInMs = null, getTimestamp = Date.now) {
    /**
     * Function to get the current timestamp
     * @type {function():number}
     */
    this._getTimestamp = getTimestamp;

    /**
     * The wrapped promise that resolves to the request type
     * @type {Promise<T>}
     */
    this._promise = new Promise((resolve, reject) => {
      /**
       * Function use to resolve the wrapped promise
       * @type {function(T):void}
       * @param {T} object
       */
      this._resolve = resolve;
      /**
       * Function used to reject the wrapped promise
       * @type {function(Error):void}
       * @param {Error} reason
       */
      this._reject = reject;
    });

    /**
     * Timestamp captured if the request timed out
     * @type {number}
     */
    this._timedOutAt = 0;

    /**
     * Timeout object that will reject the request if called. Used to clear the timeout if resolved/rejected before request times out
     * @type {NodeJS.Timeout|null}
     */
    this._timeoutId = null;

    // Chose to use null here instead of 0 in case timeoutInMs is calculated eg. timeoutInMs = shouldTimeoutAt - Date.now() // 0
    if (timeoutInMs !== null) {
      this._timeoutId = setTimeout(() => this._handleTimeout(), timeoutInMs);
    }
  }

  /**
   * Records the time the request timed out and rejects the promise with a TimeoutError
   */
  _handleTimeout() {
    this._timedOutAt = this._getTimestamp();
    this._reject(new TimeoutError(ErrorMessages.REQUEST_DID_TIMEOUT));
  }

  /**
   *  Whether the requests has timed out
   *  @returns {boolean}
   */
  didTimeout() {
    return !!this._timedOutAt;
  }

  /**
   * The promise for this request
   * @returns {Promise<T>}
   */
  getPromise() {
    return this._promise;
  }

  /**
   * Whether the requests was created with a timeout
   * @returns {boolean}
   */
  hasTimeout() {
    return !!this._timeoutId;
  }

  /**
   * Rejects the request and cancels the timeout if required
   * @param {Error} [reason] reason the request was rejected
   */
  reject(reason = new Error(ErrorMessages.UNKNOWN_REJECTION)) {
    if (this._timeoutId) clearTimeout(this._timeoutId);
    this._reject(reason);
  }

  /**
   * Resolves the request and cancels the timeout if required
   * @param {T} object object being dispatched to request
   */
  resolve(object) {
    if (this._timeoutId) clearTimeout(this._timeoutId);
    this._resolve(object);
  }
}

module.exports = PoolRequest;