- : 99 %
- : 99 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 87 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 87 %
- : 98 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 87 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 87 %
- : 98 %
- : 98 %
- : 98 %
- : 87 %
- : 98 %
- : 88 %
- : 98 %
- : 87 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 87 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 88 %
- : 89 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 89 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 89 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 89 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 89 %
- : 88 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 89 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 89 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 79 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
Source code
Revision control
Copy as Markdown
Other Tools
/* 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
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
});
const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
lazy.Log.get(lazy.Log.TYPES.REMOTE_AGENT)
);
/**
* Throttle until the `window` has performed an animation frame.
*
* The animation frame is requested after the main thread has processed
* all the already queued-up runnables.
*
* @param {ChromeWindow} win
* Window to request the animation frame from.
* @param {object=} options
* @param {number=} options.timeout
* Timeout duration in milliseconds.
* This copes with navigating away from hidden iframes: if
* fragmentNavigated happens before their animation finishes, this would
* never resolve otherwise. By default 1500 ms in an optimised build and
* 4500 ms in debug builds. Specify null to disable the timeout.
*
* @returns {Promise}
*
* @throws {TypeError}
* @throws {RangeError}
*/
export function AnimationFramePromise(win, options = {}) {
const { timeout = PROMISE_TIMEOUT } = options;
if (timeout !== null) {
if (typeof timeout != "number") {
throw new TypeError("timeout must be a number or null");
}
if (!Number.isInteger(timeout) || timeout < 0) {
throw new RangeError("timeout must be a non-negative integer");
}
}
const animationFramePromise = new Promise(resolve => {
Services.tm.dispatchToMainThread(() => {
win.requestAnimationFrame(resolve);
});
});
const promises = [
animationFramePromise,
new EventPromise(win, "pagehide"), // window closed or moved to BFCache
];
let timer;
if (timeout != null) {
promises.push(
new Promise(resolve => {
timer = lazy.setTimeout(() => {
lazy.logger.warn("Timed out waiting for animation frame");
resolve();
}, timeout);
})
);
}
return Promise.race(promises).then(() => lazy.clearTimeout(timer));
}
/**
* Wraps a callback function, that, as long as it continues to be
* invoked, will not be triggered. The given function will be
* called after the timeout duration is reached, after no more
* events fire.
*
* This class implements the {@link EventListener} interface,
* which means it can be used interchangeably with `addEventHandler`.
*
* Debouncing events can be useful when dealing with e.g. DOM events
* that fire at a high rate. It is generally advisable to avoid
* computationally expensive operations such as DOM modifications
* under these circumstances.
*
* One such high frequenecy event is `resize` that can fire multiple
* times before the window reaches its final dimensions. In order
* to delay an operation until the window has completed resizing,
* it is possible to use this technique to only invoke the callback
* after the last event has fired::
*
* let cb = new DebounceCallback(event => {
* // fires after the final resize event
* console.log("resize", event);
* });
* window.addEventListener("resize", cb);
*
* Note that it is not possible to use this synchronisation primitive
* with `addEventListener(..., {once: true})`.
*
* @param {function(Event): void} fn
* Callback function that is guaranteed to be invoked once only,
* after `timeout`.
* @param {number=} [timeout = 250] timeout
* Time since last event firing, before `fn` will be invoked.
*/
export class DebounceCallback {
constructor(fn, { timeout = 250 } = {}) {
if (typeof fn != "function" || typeof timeout != "number") {
throw new TypeError();
}
if (!Number.isInteger(timeout) || timeout < 0) {
throw new RangeError();
}
this.fn = fn;
this.timeout = timeout;
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
}
handleEvent(ev) {
this.timer.cancel();
this.timer.initWithCallback(
() => {
this.timer.cancel();
this.fn(ev);
},
this.timeout,
TYPE_ONE_SHOT
);
}
}
/**
* Create a helper object to defer a promise.
*
* @returns {object}
* An object that returns the following properties:
* - fulfilled Flag that indicates that the promise got resolved
* - pending Flag that indicates a not yet fulfilled/rejected promise
* - promise The actual promise
* - reject Callback to reject the promise
* - rejected Flag that indicates that the promise got rejected
* - resolve Callback to resolve the promise
*/
export function Deferred() {
const deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.fulfilled = false;
deferred.pending = true;
deferred.rejected = false;
deferred.resolve = (...args) => {
deferred.fulfilled = true;
deferred.pending = false;
resolve(...args);
};
deferred.reject = (...args) => {
deferred.pending = false;
deferred.rejected = true;
reject(...args);
};
});
return deferred;
}
/**
* Wait for an event to be fired on a specified element.
*
* The returned promise is guaranteed to not resolve before the
* next event tick after the event listener is called, so that all
* other event listeners for the element are executed before the
* handler is executed. For example:
*
* const promise = new EventPromise(element, "myEvent");
* // same event tick here
* await promise;
* // next event tick here
*
* @param {Element} subject
* The element that should receive the event.
* @param {string} eventName
* Case-sensitive string representing the event name to listen for.
* @param {object=} options
* @param {boolean=} options.capture
* Indicates the event will be dispatched to this subject,
* before it bubbles down to any EventTarget beneath it in the
* DOM tree. Defaults to false.
* @param {Function=} options.checkFn
* Called with the Event object as argument, should return true if the
* event is the expected one, or false if it should be ignored and
* listening should continue. If not specified, the first event with
* the specified name resolves the returned promise. Defaults to null.
* @param {number=} options.timeout
* Timeout duration in milliseconds, if provided.
* If specified, then the returned promise will be rejected with
* TimeoutError, if not already resolved, after this duration has elapsed.
* If not specified, then no timeout is used. Defaults to null.
* @param {boolean=} options.mozSystemGroup
* Determines whether to add listener to the system group. Defaults to
* false.
* @param {boolean=} options.wantUntrusted
* Receive synthetic events dispatched by web content. Defaults to false.
*
* @returns {Promise<Event>}
* Either fulfilled with the first described event, satisfying
* options.checkFn if specified, or rejected with TimeoutError after
* options.timeout milliseconds if specified.
*
* @throws {TypeError}
* @throws {RangeError}
*/
export function EventPromise(subject, eventName, options = {}) {
const {
capture = false,
checkFn = null,
timeout = null,
mozSystemGroup = false,
wantUntrusted = false,
} = options;
if (
!subject ||
!("addEventListener" in subject) ||
typeof eventName != "string" ||
typeof capture != "boolean" ||
(checkFn && typeof checkFn != "function") ||
(timeout !== null && typeof timeout != "number") ||
typeof mozSystemGroup != "boolean" ||
typeof wantUntrusted != "boolean"
) {
throw new TypeError();
}
if (timeout < 0) {
throw new RangeError();
}
return new Promise((resolve, reject) => {
let timer;
function cleanUp() {
subject.removeEventListener(eventName, listener, capture);
timer?.cancel();
}
function listener(event) {
lazy.logger.trace(`Received DOM event ${event.type} for ${event.target}`);
try {
if (checkFn && !checkFn(event)) {
return;
}
} catch (e) {
// Treat an exception in the callback as a falsy value
lazy.logger.warn(`Event check failed: ${e.message}`);
}
cleanUp();
Services.tm.dispatchToMainThread(() => resolve(event));
}
subject.addEventListener(eventName, listener, {
capture,
mozSystemGroup,
wantUntrusted,
});
if (timeout !== null) {
timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.init(
() => {
cleanUp();
reject(
new lazy.error.TimeoutError(
`EventPromise timed out after ${timeout} ms`
)
);
},
timeout,
TYPE_ONE_SHOT
);
}
});
}
/**
* Detects when the specified message manager has been destroyed.
*
* One can observe the removal and detachment of a content browser
* (`<xul:browser>`) or a chrome window by its message manager
* disconnecting.
*
* When a browser is associated with a tab, this is safer than only
* relying on the event `TabClose` which signalises the _intent to_
* remove a tab and consequently would lead to the destruction of
* the content browser and its browser message manager.
*
* When closing a chrome window it is safer than only relying on
* the event 'unload' which signalises the _intent to_ close the
* chrome window and consequently would lead to the destruction of
* the window and its window message manager.
*
* @param {MessageListenerManager} messageManager
* The message manager to observe for its disconnect state.
* Use the browser message manager when closing a content browser,
* and the window message manager when closing a chrome window.
*
* @returns {Promise}
* A promise that resolves when the message manager has been destroyed.
*/
export function MessageManagerDestroyedPromise(messageManager) {
return new Promise(resolve => {
function observe(subject, topic) {
lazy.logger.trace(`Received observer notification ${topic}`);
if (subject == messageManager) {
Services.obs.removeObserver(this, "message-manager-disconnect");
resolve();
}
}
Services.obs.addObserver(observe, "message-manager-disconnect");
});
}
/**
* 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, regardless 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([]);
* }
* }, {timeout: 1000});
*
* @param {Condition} func
* Function to run off the main thread.
* @param {object=} options
* @param {string=} options.errorMessage
* Message to use to send a warning if ``timeout`` is over.
* Defaults to `PollPromise timed out`.
* @param {number=} options.timeout
* Desired timeout if wanted. If 0 or less than the runtime evaluation
* time of ``func``, ``func`` is guaranteed to run at least once.
* Defaults to using no timeout.
* @param {Error=} options.throws
* When the ``timeout`` is hit, this error class will be
* thrown. If it is null, no error is thrown.
* @param {number=} options.interval
* Duration between each poll of ``func`` in milliseconds.
* Defaults to 10 milliseconds.
*
* @returns {Promise.<*>}
* Yields the value passed to ``func``'s
* ``resolve`` or ``reject`` callbacks.
*
* @throws {*}
* If ``func`` throws, its error is propagated.
* @throws {TypeError}
* If `timeout` or `interval`` are not numbers.
* @throws {RangeError}
* If `timeout` or `interval` are not unsigned integers.
*/
export function PollPromise(func, options = {}) {
const {
errorMessage = "PollPromise timed out",
interval = 10,
timeout = null,
throws = lazy.error.TimeoutError,
} = options;
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let didTimeOut = false;
if (typeof func != "function") {
throw new TypeError();
}
if (timeout != null && typeof timeout != "number") {
throw new TypeError();
}
if (typeof interval != "number") {
throw new TypeError();
}
if (
(timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
!Number.isInteger(interval) ||
interval < 0
) {
throw new RangeError();
}
return new Promise((resolve, reject) => {
let start, end;
if (Number.isInteger(timeout)) {
start = new Date().getTime();
end = start + timeout;
}
let evalFn = () => {
new Promise(func)
.then(resolve, rejected => {
if (lazy.error.isError(rejected)) {
throw rejected;
}
// return if there is a timeout and set to 0,
// allowing |func| to be evaluated at least once
if (
typeof end != "undefined" &&
(start == end || new Date().getTime() >= end)
) {
didTimeOut = true;
resolve(rejected);
}
})
.catch(reject);
};
// the repeating slack timer waits |interval|
// before invoking |evalFn|
evalFn();
timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
}).then(
res => {
if (didTimeOut) {
const message = `${errorMessage} after ${timeout} ms`;
if (throws !== null) {
let err = new throws(message);
throw err;
} else {
lazy.logger.warn(message);
}
}
timer.cancel();
return res;
},
err => {
timer.cancel();
throw err;
}
);
}
/**
* Pauses for the given duration.
*
* @param {number} timeout
* Duration to wait before fulfilling promise in milliseconds.
*
* @returns {Promise}
* Promise that fulfills when the `timeout` is elapsed.
*
* @throws {TypeError}
* If `timeout` is not a number.
* @throws {RangeError}
* If `timeout` is not an unsigned integer.
*/
export function Sleep(timeout) {
if (typeof timeout != "number") {
throw new TypeError();
}
if (!Number.isInteger(timeout) || timeout < 0) {
throw new RangeError();
}
return new Promise(resolve => {
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.init(
() => {
timer.cancel();
resolve();
},
timeout,
TYPE_ONE_SHOT
);
});
}
/**
* Represents the timed, eventual completion (or failure) of an
* asynchronous operation, and its resulting value.
*
* In contrast to a regular Promise, it times out after ``timeout``.
*
* @param {Function} fn
* 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 {object=} options
* @param {string} options.errorMessage
* Message to use for the thrown error.
* @param {number=} options.timeout
* ``condition``'s ``reject`` callback will be called
* after this timeout, given in milliseconds.
* By default 1500 ms in an optimised build and 4500 ms in
* debug builds.
* @param {Error=} options.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 with a TimeoutError.
*
* @returns {Promise.<*>}
* Timed promise.
*
* @throws {TypeError}
* If `timeout` is not a number.
* @throws {RangeError}
* If `timeout` is not an unsigned integer.
*/
export function TimedPromise(fn, options = {}) {
const {
errorMessage = "TimedPromise timed out",
timeout = PROMISE_TIMEOUT,
throws = lazy.error.TimeoutError,
} = options;
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
if (typeof fn != "function") {
throw new TypeError();
}
if (typeof timeout != "number") {
throw new TypeError();
}
if (!Number.isInteger(timeout) || timeout < 0) {
throw new RangeError();
}
return new Promise((resolve, reject) => {
let trace;
// Reject only if |throws| is given. Otherwise it is assumed that
// the user is OK with the promise timing out.
let bail = () => {
const message = `${errorMessage} after ${timeout} ms`;
if (throws !== null) {
let err = new throws(message);
reject(err);
} else {
lazy.logger.warn(message, trace);
resolve();
}
};
trace = lazy.error.stack();
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;
}
);
}
/**
* Wait for a message to be fired from a particular message manager.
*
* This method has been duplicated from BrowserTestUtils.sys.mjs.
*
* @param {nsIMessageManager} messageManager
* The message manager that should be used.
* @param {string} messageName
* The message to wait for.
* @param {object=} options
* Extra options.
* @param {function(Message): boolean=} options.checkFn
* Called with the ``Message`` object as argument, should return ``true``
* if the message is the expected one, or ``false`` if it should be
* ignored and listening should continue. If not specified, the first
* message with the specified name resolves the returned promise.
*
* @returns {Promise.<object>}
* Promise which resolves to the data property of the received
* ``Message``.
*/
export function waitForMessage(
messageManager,
messageName,
{ checkFn = undefined } = {}
) {
if (messageManager == null || !("addMessageListener" in messageManager)) {
throw new TypeError();
}
if (typeof messageName != "string") {
throw new TypeError();
}
if (checkFn && typeof checkFn != "function") {
throw new TypeError();
}
return new Promise(resolve => {
messageManager.addMessageListener(messageName, function onMessage(msg) {
lazy.logger.trace(`Received ${messageName} for ${msg.target}`);
if (checkFn && !checkFn(msg)) {
return;
}
messageManager.removeMessageListener(messageName, onMessage);
resolve(msg.data);
});
});
}
/**
* Wait for the specified observer topic to be observed.
*
* This method has been duplicated from TestUtils.sys.mjs.
*
* Because this function is intended for testing, any error in checkFn
* will cause the returned promise to be rejected instead of waiting for
* the next notification, since this is probably a bug in the test.
*
* @param {string} topic
* The topic to observe.
* @param {object=} options
* Extra options.
* @param {function(string, object): boolean=} options.checkFn
* Called with ``subject``, and ``data`` as arguments, should return true
* if the notification is the expected one, or false if it should be
* ignored and listening should continue. If not specified, the first
* notification for the specified topic resolves the returned promise.
* @param {boolean=} options.logging
* Flag indicating whether the observer notification should be logged
* when received. Defaults to `true`.
* @param {number=} options.timeout
* Timeout duration in milliseconds, if provided.
* If specified, then the returned promise will be rejected with
* TimeoutError, if not already resolved, after this duration has elapsed.
* If not specified, then no timeout is used. Defaults to null.
*
* @returns {Promise.<Array<string, object>>}
* Promise which is either resolved to an array of ``subject``, and ``data``
* from the observed notification, or rejected with TimeoutError after
* options.timeout milliseconds if specified.
*
* @throws {TypeError}
* @throws {RangeError}
*/
export function waitForObserverTopic(topic, options = {}) {
const { checkFn = null, logging = true, timeout = null } = options;
if (typeof topic != "string") {
throw new TypeError();
}
if (
(checkFn != null && typeof checkFn != "function") ||
(timeout !== null && typeof timeout != "number")
) {
throw new TypeError();
}
if (timeout && (!Number.isInteger(timeout) || timeout < 0)) {
throw new RangeError();
}
return new Promise((resolve, reject) => {
let timer;
function cleanUp() {
Services.obs.removeObserver(observer, topic);
timer?.cancel();
}
function observer(subject, _topic, data) {
if (logging) {
lazy.logger.trace(`Received observer notification ${_topic}`);
}
try {
if (checkFn && !checkFn(subject, data)) {
return;
}
cleanUp();
resolve({ subject, data });
} catch (ex) {
cleanUp();
reject(ex);
}
}
Services.obs.addObserver(observer, topic);
if (timeout !== null) {
timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.init(
() => {
cleanUp();
reject(
new lazy.error.TimeoutError(
`waitForObserverTopic timed out after ${timeout} ms`
)
);
},
timeout,
TYPE_ONE_SHOT
);
}
});
}