summaryrefslogtreecommitdiffstats
path: root/remote/marionette/sync.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/marionette/sync.sys.mjs')
-rw-r--r--remote/marionette/sync.sys.mjs497
1 files changed, 497 insertions, 0 deletions
diff --git a/remote/marionette/sync.sys.mjs b/remote/marionette/sync.sys.mjs
new file mode 100644
index 0000000000..890773bfbe
--- /dev/null
+++ b/remote/marionette/sync.sys.mjs
@@ -0,0 +1,497 @@
+/* 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/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
+
+const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
+
+/**
+ * Dispatch a function to be executed on the main thread.
+ *
+ * @param {function} func
+ * Function to be executed.
+ */
+export function executeSoon(func) {
+ if (typeof func != "function") {
+ throw new TypeError();
+ }
+
+ Services.tm.dispatchToMainThread(func);
+}
+
+/**
+ * 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([]);
+ * }
+ * }, {timeout: 1000});
+ *
+ * @param {Condition} func
+ * Function to run off the main thread.
+ * @param {number=} [timeout] 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 {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.
+ * @throws {TypeError}
+ * If `timeout` or `interval`` are not numbers.
+ * @throws {RangeError}
+ * If `timeout` or `interval` are not unsigned integers.
+ */
+export function PollPromise(func, { timeout = null, interval = 10 } = {}) {
+ const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ 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)
+ ) {
+ 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;
+ }
+ );
+}
+
+/**
+ * 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 {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 {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=TimeoutError]
+ * 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.
+ *
+ * @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 = () => {
+ if (throws !== null) {
+ let err = new throws(`${errorMessage} after ${timeout} ms`);
+ reject(err);
+ } else {
+ lazy.logger.warn(`${errorMessage} after ${timeout} ms`, 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;
+ }
+ );
+}
+
+/**
+ * Pauses for the given duration.
+ *
+ * @param {number} timeout
+ * Duration to wait before fulfilling promise in milliseconds.
+ *
+ * @return {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(
+ () => {
+ // Bug 1663880 - Explicitely cancel the timer for now to prevent a hang
+ timer.cancel();
+ resolve();
+ },
+ 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.
+ *
+ * @return {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");
+ });
+}
+
+/**
+ * Throttle until the main thread is idle and `window` has performed
+ * an animation frame (in that order).
+ *
+ * @param {ChromeWindow} win
+ * Window to request the animation frame from.
+ *
+ * @return Promise
+ */
+export function IdlePromise(win) {
+ const animationFramePromise = new Promise(resolve => {
+ executeSoon(() => {
+ win.requestAnimationFrame(resolve);
+ });
+ });
+
+ // Abort if the underlying window gets closed
+ const windowClosedPromise = new PollPromise(resolve => {
+ if (win.closed) {
+ resolve();
+ }
+ });
+
+ return Promise.race([animationFramePromise, windowClosedPromise]);
+}
+
+/**
+ * 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 interchangably 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)} 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
+ );
+ }
+}
+
+/**
+ * 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)=} 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.
+ *
+ * @return {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)=} 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.
+ *
+ * @return {Promise.<Array<String, Object>>}
+ * Promise which resolves to an array of ``subject``, and ``data`` from
+ * the observed notification.
+ */
+export function waitForObserverTopic(topic, { checkFn = null } = {}) {
+ if (typeof topic != "string") {
+ throw new TypeError();
+ }
+ if (checkFn != null && typeof checkFn != "function") {
+ throw new TypeError();
+ }
+
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function observer(subject, topic, data) {
+ lazy.logger.trace(`Received observer notification ${topic}`);
+ try {
+ if (checkFn && !checkFn(subject, data)) {
+ return;
+ }
+ Services.obs.removeObserver(observer, topic);
+ resolve({ subject, data });
+ } catch (ex) {
+ Services.obs.removeObserver(observer, topic);
+ reject(ex);
+ }
+ }, topic);
+ });
+}