Source: sync.js

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {classes: Cc, interfaces: Ci, utils: Cu} = Components;

const {
  error,
  TimeoutError,
} = Cu.import("chrome://marionette/content/error.js", {});

/* exported PollPromise, TimedPromise */
this.EXPORTED_SYMBOLS = ["PollPromise", "TimedPromise"];

const {TYPE_ONE_SHOT, TYPE_REPEATING_SLACK} = Ci.nsITimer;

/**
 * @callback Condition
 *
 * @param {function(*)} resolve
 *     To be called when the condition has been met.  Will return the
 *     resolved value.
 * @param {function} reject
 *     To be called when the condition has not been met.  Will cause
 *     the condition to be revaluated or time out.
 *
 * @return {*}
 *     The value from calling <code>resolve</code>.
 */

/**
 * Runs a promise-like function off the main thread until it is resolved
 * through <code>resolve</code> or <code>rejected</code> callbacks.
 * The function is guaranteed to be run at least once, irregardless of
 * the timeout.
 *
 * The <var>func</var> is evaluated every <var>interval</var> for as
 * long as its runtime duration does not exceed <var>interval</var>.
 * Evaluations occur sequentially, meaning that evaluations of
 * <var>func</var> are queued if the runtime evaluation duration of
 * <var>func</var> is greater than <var>interval</var>.
 *
 * <var>func</var> is given two arguments, <code>resolve</code> and
 * <code>reject</code>, of which one must be called for the evaluation
 * to complete.  Calling <code>resolve</code> with an argument
 * indicates that the expected wait condition was met and will return
 * the passed value to the caller.  Conversely, calling
 * <code>reject</code> will evaluate <var>func</var> again until
 * the <var>timeout</var> duration has elapsed or <var>func</var>
 * throws.  The passed value to <code>reject</code> will also be
 * returned to the caller once the wait has expired.
 *
 * Usage:
 *
 * <pre><code>
 *     let els = new PollPromise((resolve, reject) => {
 *       let res = document.querySelectorAll("p");
 *       if (res.length > 0) {
 *         resolve(Array.from(res));
 *       } else {
 *         reject([]);
 *       }
 *     });
 * </pre></code>
 *
 * @param {Condition} func
 *     Function to run off the main thread.
 * @param {number=} [timeout=2000] timeout
 *     Desired timeout.  If 0 or less than the runtime evaluation
 *     time of <var>func</var>, <var>func</var> is guaranteed to run
 *     at least once.  The default is 2000 milliseconds.
 * @param {number=} [interval=10] interval
 *     Duration between each poll of <var>func</var> in milliseconds.
 *     Defaults to 10 milliseconds.
 *
 * @return {Promise.<*>}
 *     Yields the value passed to <var>func</var>'s
 *     <code>resolve</code> or <code>reject</code> callbacks.
 *
 * @throws {*}
 *     If <var>func</var> throws, its error is propagated.
 */
function PollPromise(func, {timeout = 2000, interval = 10} = {}) {
  const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);

  return new Promise((resolve, reject) => {
    const start = new Date().getTime();
    const end = start + timeout;

    let evalFn = () => {
      new Promise(func).then(resolve, rejected => {
        if (error.isError(rejected)) {
          throw rejected;
        }

        // return if timeout is 0, allowing |func| to be evaluated at
        // least once
        if (start == end || new Date().getTime() >= end) {
          resolve(rejected);
        }
      }).catch(reject);
    };

    // the repeating slack timer waits |interval|
    // before invoking |evalFn|
    evalFn();

    timer.init(evalFn, interval, TYPE_REPEATING_SLACK);

  }).then(res => {
    timer.cancel();
    return res;
  }, err => {
    timer.cancel();
    throw err;
  });
}

/**
 * The <code>TimedPromise</code> object represents the timed, eventual
 * completion (or failure) of an asynchronous operation, and its
 * resulting value.
 *
 * In contrast to a regular {@link Promise}, it times out after
 * <var>timeout</var>.
 *
 * @param {Condition} func
 *     Function to run, which will have its <code>reject</code>
 *     callback invoked after the <var>timeout</var> duration is reached.
 *     It is given two callbacks: <code>resolve(value)</code> and
 *     <code>reject(error)</code>.
 * @param {timeout=} [timeout=1500] timeout
 *     <var>condition</var>'s <code>reject</code> callback will be called
 *     after this timeout.
 * @param {Error=} [throws=TimeoutError] throws
 *     When the <var>timeout</var> is hit, this error class will be
 *     thrown.  If it is null, no error is thrown and the promise is
 *     instead resolved on timeout.
 *
 * @return {Promise.<*>}
 *     Timed promise.
 */
function TimedPromise(fn, {timeout = 1500, throws = TimeoutError} = {}) {
  const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);

  return new Promise((resolve, reject) => {
    // Reject only if |throws| is given.  Otherwise it is assumed that
    // the user is OK with the promise timing out.
    let bail = () => {
      if (throws !== null) {
        let err = new throws();
        reject(err);
      } else {
        resolve();
      }
    };

    timer.initWithCallback({notify: bail}, timeout, TYPE_ONE_SHOT);

    try {
      fn(resolve, reject);
    } catch (e) {
      reject(e);
    }

  }).then(res => {
    timer.cancel();
    return res;
  }, err => {
    timer.cancel();
    throw err;
  });
}