Source

utils/promise.js

/**
 * @namespace Utils
 * @category Utils
 * @module Promise*/

/**
 * Generic helper for progresive searchs.
 *
 * Used to split heavy requests without pagination support (Geored APIV2).
 *
 * @param {Function} options.generateSubsets Handler to generate subsets
 * @param {Function} options.handleSubsets Handler to process and resolve subsets
 * @param {Function} options.withSubsetResult Handler to control a subset result
 * @param {Boolean} options.sequential Subsets will resolve sequentially
 */
export async function splitOperation(options = {}) {
  const sequential = require('promise-sequential')
  let subsets = options.generateSubsets()
  if (options.sequential === true) {
    return await sequential(
      subsets.map((subset) => {
        return async () => {
          let r = await options.handleSubset(subset)
          options.withSubsetResult && options.withSubsetResult(r, subset)
          return r
        }
      })
    )
  } else {
    return await Promise.all(
      subsets.map((subset) =>
        (async () => {
          let r = await options.handleSubset(subset)
          options.withSubsetResult && options.withSubsetResult(r, subset)
          return r
        })()
      )
    )
  }
}

/**
 * Queue function to be called after certain time (like setTimeout) but skips if already queued.
 *
 * Useful for delaying operations in vue watchers/computed properties (To avoid multiple simultaneous calls and performance issues)
 *
 * This code is an async function called queueOperationOnce with 3 parameters (uniqueId, handler and millisecondsOrOptions). The function checkes if the parameters are valid and then starts a timeout that calls the specified handler. This code is used to ensure that only one instance of an operation runs at any given time - when another instance of the same operation is triggered, the new instance will be skipped and cleared or replaced, depending on the specified option.
 *
 * @param {String} uniqueId The unique identifier for the queue
 * @param {Function} handler handler to be queued and executed after certain time.
 * @param {Number} millisecondsOrOptions Timeout in milliseconds
 * @param {Number} millisecondsOrOptions.timeout Timeout in milliseconds
 * @param {Boolean} millisecondsOrOptions.isSequential Skip if similar operation is processing
 * @param {Boolean} millisecondsOrOptions.clearPreviousTimeout If true, previous queued operations will be overrided
 */
export async function queueOperationOnce(
  uniqueId,
  handler,
  millisecondsOrOptions = 1000
) {
  if (!uniqueId) {
    throw new Error('string expected (uniqueId)')
  }
  if (typeof handler !== 'function') {
    throw new Error('function expected (handler)')
  }
  let options =
    typeof millisecondsOrOptions !== 'object' ? {} : millisecondsOrOptions
  let timeoutMilliseconds =
    typeof millisecondsOrOptions !== 'object'
      ? millisecondsOrOptions
      : millisecondsOrOptions.timeout || 1000

  const globalStateWinPropName = '_queueOperations'

  if (window[globalStateWinPropName] === undefined) {
    window[globalStateWinPropName] = {}
  }
  const hasActiveTimeout = () =>
    window[globalStateWinPropName][uniqueId] &&
    window[globalStateWinPropName][uniqueId].windowTimeout
  const removeActiveTimeout = () => {
    if (hasActiveTimeout()) {
      window.clearTimeout(
        window[globalStateWinPropName][uniqueId].windowTimeout
      )
      delete window[globalStateWinPropName][uniqueId].windowTimeout
    }
  }
  const createQueueScope = (obj) =>
    (window[globalStateWinPropName][uniqueId] = obj)

  const hasQueueScope = () => !!window[globalStateWinPropName][uniqueId]

  const updateQueueScope = (attributeName, value) => {
    if (hasQueueScope()) {
      window[globalStateWinPropName][uniqueId][attributeName] = value
    }
  }

  const isProcessing = () =>
    hasQueueScope() && window[globalStateWinPropName][uniqueId]['processing']

  const incrementQueueScopeValue = (attributeName) =>
    window[globalStateWinPropName][uniqueId][attributeName]++

  return new Promise((resolve, reject) => {
    if (hasActiveTimeout()) {
      if (options.clearPreviousTimeout) {
        removeActiveTimeout() //Replace previous timeout
        incrementQueueScopeValue('clearCount')
      } else {
        incrementQueueScopeValue('skipCount')
        return resolve() //Skip if the same operation is already queued
      }
    }

    if (options.isSequential && isProcessing()) {
      incrementQueueScopeValue('skipAlreadyProcessingCount')
      return resolve()
    }

    let windowTimeout = window.setTimeout(() => {
      try {
        updateQueueScope('processing', true)
        let handlerResponse = handler()
        if (handlerResponse instanceof Promise) {
          handlerResponse
            .then((response) => {
              incrementQueueScopeValue('resolvedCount')
              resolve(response)
              options.resolve && options.resolve(handlerResponse)
            })
            .catch(reject)
            .finally(() => {
              updateQueueScope('processing', false)
              removeActiveTimeout()
            })
        } else {
          removeActiveTimeout()
          incrementQueueScopeValue('resolvedCount')
          updateQueueScope('processing', false)
          resolve(handlerResponse)
          options.resolve && options.resolve(handlerResponse)
        }
      } catch (err) {
        removeActiveTimeout
        reject(err)
      }
    }, timeoutMilliseconds)

    if (!hasQueueScope()) {
      createQueueScope({
        uniqueId,
        skipCount: 0,
        skipAlreadyProcessingCount: 0,
        clearCount: 0,
        resolvedCount: 0,
        windowTimeout,
        processing: false,
      })
    } else {
      updateQueueScope('windowTimeout', windowTimeout)
    }
  })
}