sleepPreciseCancellable.mjs


import assert from 'nanoassert'
import sleepCancellable from './sleepCancellable.mjs'
import Deferred from './Deferred.mjs'

/**
 * Waits a given amount of time.
 *
 * This function returns both a promise and cancel function in order to cancel the
 * wait time if necessary. If cancelled, the promise will be rejected with a `CancelledError`.
 *
 * This function is similar to `sleep()` except it ensures that the amount of time measured
 * using the `Date` object is always greater than or equal the asked amount of time.
 *
 * This function can imply additional delay that can be bad for performances. As such it is
 * recommended to only use it in unit tests or very specific cases. Most applications should
 * be adapted to work with the usual `setTimout()` inconsistencies even if it can trigger some
 * milliseconds before the asked delay.
 *
 * @param {number} amount An amount of time in milliseconds
 * @returns {Array} A tuple of two objects:
 *   * `promise`: The promise
 *   * `cancel`: The cancel function. It will return a boolean that will be `true` if the promise was effectively cancelled,
 *     `false` otherwise.
 * @example
 * import { sleepPreciseCancellable, asyncRoot } from 'modern-async'
 *
 * asyncRoot(async () => {
 *   const [promise, cancel] = sleepPreciseCancellable(100) // schedule to resolve the promise after 100ms
 *
 *   cancel()
 *
 *   try {
 *     await promise
 *   } catch (e) {
 *     console.log(e.name) // prints CancelledError
 *   }
 * })
 */
function sleepPreciseCancellable (amount) {
  return _innerWaitPreciseCancellable(amount, (ellasped, amount) => {
    return ellasped >= amount
  })
}

export default sleepPreciseCancellable

/**
 * @ignore
 *
 * @param {*} amount ignored
 * @param {*} checkPassed ignored
 * @returns {*} ignored
 */
function _innerWaitPreciseCancellable (amount, checkPassed) {
  assert(typeof amount === 'number', 'amount must be a number')
  const start = new Date().getTime()
  const [p, cancel] = sleepCancellable(amount)
  let lastCancel = cancel
  const deferred = new Deferred()
  const reject = (e) => {
    deferred.reject(e)
  }
  const resolve = () => {
    const now = new Date().getTime()
    const ellasped = now - start
    if (checkPassed(ellasped, amount)) {
      deferred.resolve()
    } else {
      const [np, ncancel] = sleepCancellable(amount - ellasped)
      lastCancel = ncancel
      np.then(resolve, reject)
    }
  }
  p.then(resolve, reject)
  return [deferred.promise, () => {
    return lastCancel()
  }]
}

export { _innerWaitPreciseCancellable }