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