diff options
Diffstat (limited to 'remote/shared/Sync.sys.mjs')
-rw-r--r-- | remote/shared/Sync.sys.mjs | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/remote/shared/Sync.sys.mjs b/remote/shared/Sync.sys.mjs new file mode 100644 index 0000000000..7b14f8b2c8 --- /dev/null +++ b/remote/shared/Sync.sys.mjs @@ -0,0 +1,335 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + 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; + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.REMOTE_AGENT) +); + +/** + * Throttle until the `window` has performed an animation frame. + * + * @param {ChromeWindow} win + * Window to request the animation frame from. + * + * @returns {Promise} + */ +export function AnimationFramePromise(win) { + const animationFramePromise = new Promise(resolve => { + win.requestAnimationFrame(resolve); + }); + + // Abort if the underlying window gets closed + const windowClosedPromise = new PollPromise(resolve => { + if (win.closed) { + resolve(); + } + }); + + return Promise.race([animationFramePromise, windowClosedPromise]); +} + +/** + * 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 despatched 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 despatched 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(); + executeSoon(() => 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 + ); + } + }); +} + +/** + * Wait for the next tick in the event loop to execute a callback. + * + * @param {Function} fn + * Function to be executed. + */ +export function executeSoon(fn) { + if (typeof fn != "function") { + throw new TypeError(); + } + + Services.tm.dispatchToMainThread(fn); +} + +/** + * 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 {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 {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, + } = 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 (typeof rejected != "undefined") { + 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) { + lazy.logger.warn(`${errorMessage} after ${timeout} ms`); + } + timer.cancel(); + return res; + }, + err => { + timer.cancel(); + throw err; + } + ); +} |