PoolOptions.js

const { ErrorMessages } = require('./errors');
const { toInt, toNullableInt, toBoolean } = require('./utils');

/**
 * @interface Options
 * @property {number} [minSize=0] The minimum number of objects the pool will try to maintain including borrowed objects. Cannot be greater than given maxSize. If this value is given without
 * a maxSize, maxSize will be adjusted if needed
 * @property {number} [maxSize=1] The maximum number of objects the pool can manage including objects being created. Cannot be less than given minSize. If this value is given without
 * a minSize, minSize will be adjusted if needed
 * @property {number|null} [defaultTimeoutInMs=30000] The default time in milliseconds calls to {@link Pool#acquire|pool.acquire()} and {@link Pool#use|pool.use()} will time out
 * @property {number|null} [maxPendingRequests=null] The number of requests that can be queued if the pool is at maximum size and has no objects available.
 * ie. the max number of requests waiting for an object to be returned. Note: Setting maxPendingRequests to `null` will disable maxPendingRequests which is different
 * from setting it to `0` which not allow any requests to be queued when the pool does not have any objects available
 * @property {number|null} [idleCheckIntervalInMs=null] The interval the pool checks for and removes idle objects
 * @property {number|null} [maxIdleToRemove=null] The max objects that can be removed when the pool checks for idle objects
 * @property {number|null} [softIdleTimeInMs=null] The amount of time an object must be idle before being eligible for soft removal. If an object is checked
 * and exceeds this time it will be destroyed if the currently available objects is above the minimum
 * @property {number|null} [hardIdleTimeInMs=30000] The amount of time an object must be idle before being eligible for hard removal. If an object is checked
 * exceeding this idle time it will be destroyed. If destroying the object would bring the pool below the minimum, a new object will be created
 * @property {boolean} [shouldAutoStart=true] If true, pool will start automatically. If a minSize is set the pool will start creating objects before any requests
 * are made
 * @property {boolean} [shouldValidateOnDispatch=false] If `true`, pool will call {@link Factory#validate|factory.validate()} on objects before dispatching them for use. If validation
 * fails the invalid object will be destroyed and the pool will attempt to dispatch another pooled object, creating one if required
 * @property {boolean} [shouldValidateOnReturn=false] If `true`, pool will call {@link Factory#validate|factory.validate()} on objects before adding them back to the available objects. If
 * validation fails, the invalid object will be destroyed. If destroying the object would bring the pool below the minimum, a new object will be created.
 * @property {boolean} [shouldUseFifo=true] Determines the order objects are dispatched. Either First in, First out (FIFO) or Last in, First out (LIFO). By default,
 * the pool dispatches the object that has been available the longest, working like a queue (FIFO). Setting this option to `false` will dispatch the most recently returned
 * or created object, working like a stack (LIFO)
 */

/**
 * @private
 */
class PoolOptions {
  /**
   * Merges given options with defaults
   *
   * @example
   * const options = new PoolOptions({ minSize: 2, maxSize: 6, defaultTimeoutInMs: null, shouldValidateOnDispatch: true })
   * options // { minSize: 2, maxSize: 6, defaultTimeoutInMs: null, maxPendingRequests: null, ...otherOptions }
   *
   *
   * // Just giving maxSize will use the default minSize
   * new PoolOptions({ maxSize: 6 }) // { minSize: 0, maxSize: 6, ...otherOptions }
   *
   * // Just giving minSize will increase maxSize to the given minSize if needed
   * new PoolOptions({ minSize: 4 }) //  { minSize: 4, maxSize: 4, ...otherOptions }
   *
   * // If both are given and minSize is greater than maxSize it will result in an error
   * new PoolOptions({ minSize: 5, maxSize: 2 })  //  RangeError
   *
   * @param {Partial<Options>} [options={}]
   */
  constructor(options = {}) {
    // set default options
    this.minSize = 0;
    this.maxSize = 1;
    /** @type {number|null} */
    this.defaultTimeoutInMs = 30000;
    /** @type {number|null} */
    this.maxPendingRequests = null;
    /** @type {number|null} */
    this.idleCheckIntervalInMs = null;
    /** @type {number|null} */
    this.maxIdleToRemove = null;
    /** @type {number|null} */
    this.softIdleTimeInMs = null;
    /** @type {number|null} */
    this.hardIdleTimeInMs = 30000;
    this.shouldAutoStart = true;
    this.shouldValidateOnDispatch = false;
    this.shouldValidateOnReturn = false;
    this.shouldUseFifo = true;
    // merge given options with defaults
    this.set(options);
  }

  /**
   * Sets the pool options to the given values if valid
   * @example
   * options.set({ maxSize: 5 })
   * @param {Partial<Options>} [options={}]
   * @returns {this}
   */
  set(options = {}) {
    // prevents undefined values from overriding current. If given option is unknown parseOption should throw
    const merged = Object.entries(options).reduce((merged, [key, value]) => {
      if (value === undefined) return merged;
      return { ...merged, [key]: PoolOptions.parseOption(key, value) };
    }, this);

    // if no maxSize provided and the new minSize is above the current maxSize adjust the merged value
    if (options.maxSize === undefined && merged.minSize > merged.maxSize) merged.maxSize = merged.minSize;
    // if no minSize provided and the new maxSize is below the current minSize adjust the merged value
    if (options.minSize === undefined && merged.minSize > merged.maxSize) merged.minSize = merged.maxSize;
    // if minSize and maxSize were both provided and minSize is above maxSize this will throw
    if (merged.minSize > merged.maxSize) throw new RangeError(ErrorMessages.MIN_ABOVE_MAX);

    // ensure at least one of softIdleTimeInMs or hardIdleTimeInMs is set if idleCheckIntervalInMs is set
    if (merged.idleCheckIntervalInMs && !merged.softIdleTimeInMs && !merged.hardIdleTimeInMs) {
      throw new ReferenceError(ErrorMessages.IDLE_CHECK_NEEDS_SOFT_OR_HARD_TIME);
    }

    // update option values
    Object.assign(this, merged);
    return this;
  }

  /**
   * Ensures the given option value has correct type and range
   * @param {string} option
   * @param {*} value
   * @returns {number|boolean|null}
   */
  static parseOption(option, value) {
    switch (option) {
      case 'minSize':
        return toInt(value, option, { min: 0 });
      case 'maxSize':
        return toInt(value, option, { min: 1 });
      case 'maxPendingRequests':
        return toNullableInt(value, option, { min: 0 });
      case 'defaultTimeoutInMs':
      case 'idleCheckIntervalInMs':
      case 'maxIdleToRemove':
      case 'softIdleTimeInMs':
      case 'hardIdleTimeInMs':
        return toNullableInt(value, option, { min: 1 });
      case 'shouldAutoStart':
      case 'shouldValidateOnDispatch':
      case 'shouldValidateOnReturn':
      case 'shouldUseFifo':
        return toBoolean(value, option);
      default:
        throw new ReferenceError(`Unknown option ${option}`);
    }
  }
}

module.exports = PoolOptions;