Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / remote / shared / Sync.sys.mjs
blob5562937ee804b62b65b0ea3814b429eef7670f31
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
9   Log: "chrome://remote/content/shared/Log.sys.mjs",
10 });
12 const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
14 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
15   lazy.Log.get(lazy.Log.TYPES.REMOTE_AGENT)
18 /**
19  * Throttle until the `window` has performed an animation frame.
20  *
21  * The animation frame is requested after the main thread has processed
22  * all the already queued-up runnables.
23  *
24  * @param {ChromeWindow} win
25  *     Window to request the animation frame from.
26  *
27  * @returns {Promise}
28  */
29 export function AnimationFramePromise(win) {
30   const animationFramePromise = new Promise(resolve => {
31     executeSoon(() => {
32       win.requestAnimationFrame(resolve);
33     });
34   });
36   // Abort if the underlying window is no longer active (closed, BFCache)
37   const unloadPromise = new EventPromise(win, "pagehide");
39   return Promise.race([animationFramePromise, unloadPromise]);
42 /**
43  * Create a helper object to defer a promise.
44  *
45  * @returns {object}
46  *     An object that returns the following properties:
47  *       - fulfilled Flag that indicates that the promise got resolved
48  *       - pending Flag that indicates a not yet fulfilled/rejected promise
49  *       - promise The actual promise
50  *       - reject Callback to reject the promise
51  *       - rejected Flag that indicates that the promise got rejected
52  *       - resolve Callback to resolve the promise
53  */
54 export function Deferred() {
55   const deferred = {};
57   deferred.promise = new Promise((resolve, reject) => {
58     deferred.fulfilled = false;
59     deferred.pending = true;
60     deferred.rejected = false;
62     deferred.resolve = (...args) => {
63       deferred.fulfilled = true;
64       deferred.pending = false;
65       resolve(...args);
66     };
68     deferred.reject = (...args) => {
69       deferred.pending = false;
70       deferred.rejected = true;
71       reject(...args);
72     };
73   });
75   return deferred;
78 /**
79  * Wait for an event to be fired on a specified element.
80  *
81  * The returned promise is guaranteed to not resolve before the
82  * next event tick after the event listener is called, so that all
83  * other event listeners for the element are executed before the
84  * handler is executed.  For example:
85  *
86  *     const promise = new EventPromise(element, "myEvent");
87  *     // same event tick here
88  *     await promise;
89  *     // next event tick here
90  *
91  * @param {Element} subject
92  *     The element that should receive the event.
93  * @param {string} eventName
94  *     Case-sensitive string representing the event name to listen for.
95  * @param {object=} options
96  * @param {boolean=} options.capture
97  *     Indicates the event will be despatched to this subject,
98  *     before it bubbles down to any EventTarget beneath it in the
99  *     DOM tree. Defaults to false.
100  * @param {Function=} options.checkFn
101  *     Called with the Event object as argument, should return true if the
102  *     event is the expected one, or false if it should be ignored and
103  *     listening should continue. If not specified, the first event with
104  *     the specified name resolves the returned promise. Defaults to null.
105  * @param {number=} options.timeout
106  *     Timeout duration in milliseconds, if provided.
107  *     If specified, then the returned promise will be rejected with
108  *     TimeoutError, if not already resolved, after this duration has elapsed.
109  *     If not specified, then no timeout is used. Defaults to null.
110  * @param {boolean=} options.mozSystemGroup
111  *     Determines whether to add listener to the system group. Defaults to
112  *     false.
113  * @param {boolean=} options.wantUntrusted
114  *     Receive synthetic events despatched by web content. Defaults to false.
116  * @returns {Promise<Event>}
117  *     Either fulfilled with the first described event, satisfying
118  *     options.checkFn if specified, or rejected with TimeoutError after
119  *     options.timeout milliseconds if specified.
121  * @throws {TypeError}
122  * @throws {RangeError}
123  */
124 export function EventPromise(subject, eventName, options = {}) {
125   const {
126     capture = false,
127     checkFn = null,
128     timeout = null,
129     mozSystemGroup = false,
130     wantUntrusted = false,
131   } = options;
132   if (
133     !subject ||
134     !("addEventListener" in subject) ||
135     typeof eventName != "string" ||
136     typeof capture != "boolean" ||
137     (checkFn && typeof checkFn != "function") ||
138     (timeout !== null && typeof timeout != "number") ||
139     typeof mozSystemGroup != "boolean" ||
140     typeof wantUntrusted != "boolean"
141   ) {
142     throw new TypeError();
143   }
144   if (timeout < 0) {
145     throw new RangeError();
146   }
148   return new Promise((resolve, reject) => {
149     let timer;
151     function cleanUp() {
152       subject.removeEventListener(eventName, listener, capture);
153       timer?.cancel();
154     }
156     function listener(event) {
157       lazy.logger.trace(`Received DOM event ${event.type} for ${event.target}`);
158       try {
159         if (checkFn && !checkFn(event)) {
160           return;
161         }
162       } catch (e) {
163         // Treat an exception in the callback as a falsy value
164         lazy.logger.warn(`Event check failed: ${e.message}`);
165       }
167       cleanUp();
168       executeSoon(() => resolve(event));
169     }
171     subject.addEventListener(eventName, listener, {
172       capture,
173       mozSystemGroup,
174       wantUntrusted,
175     });
177     if (timeout !== null) {
178       timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
179       timer.init(
180         () => {
181           cleanUp();
182           reject(
183             new lazy.error.TimeoutError(
184               `EventPromise timed out after ${timeout} ms`
185             )
186           );
187         },
188         timeout,
189         TYPE_ONE_SHOT
190       );
191     }
192   });
196  * Wait for the next tick in the event loop to execute a callback.
198  * @param {Function} fn
199  *     Function to be executed.
200  */
201 export function executeSoon(fn) {
202   if (typeof fn != "function") {
203     throw new TypeError();
204   }
206   Services.tm.dispatchToMainThread(fn);
210  * Runs a Promise-like function off the main thread until it is resolved
211  * through ``resolve`` or ``rejected`` callbacks.  The function is
212  * guaranteed to be run at least once, irregardless of the timeout.
214  * The ``func`` is evaluated every ``interval`` for as long as its
215  * runtime duration does not exceed ``interval``.  Evaluations occur
216  * sequentially, meaning that evaluations of ``func`` are queued if
217  * the runtime evaluation duration of ``func`` is greater than ``interval``.
219  * ``func`` is given two arguments, ``resolve`` and ``reject``,
220  * of which one must be called for the evaluation to complete.
221  * Calling ``resolve`` with an argument indicates that the expected
222  * wait condition was met and will return the passed value to the
223  * caller.  Conversely, calling ``reject`` will evaluate ``func``
224  * again until the ``timeout`` duration has elapsed or ``func`` throws.
225  * The passed value to ``reject`` will also be returned to the caller
226  * once the wait has expired.
228  * Usage::
230  *     let els = new PollPromise((resolve, reject) => {
231  *       let res = document.querySelectorAll("p");
232  *       if (res.length > 0) {
233  *         resolve(Array.from(res));
234  *       } else {
235  *         reject([]);
236  *       }
237  *     }, {timeout: 1000});
239  * @param {Condition} func
240  *     Function to run off the main thread.
241  * @param {object=} options
242  * @param {string=} options.errorMessage
243  *     Message to use to send a warning if ``timeout`` is over.
244  *     Defaults to `PollPromise timed out`.
245  * @param {number=} options.timeout
246  *     Desired timeout if wanted.  If 0 or less than the runtime evaluation
247  *     time of ``func``, ``func`` is guaranteed to run at least once.
248  *     Defaults to using no timeout.
249  * @param {number=} options.interval
250  *     Duration between each poll of ``func`` in milliseconds.
251  *     Defaults to 10 milliseconds.
253  * @returns {Promise.<*>}
254  *     Yields the value passed to ``func``'s
255  *     ``resolve`` or ``reject`` callbacks.
257  * @throws {*}
258  *     If ``func`` throws, its error is propagated.
259  * @throws {TypeError}
260  *     If `timeout` or `interval`` are not numbers.
261  * @throws {RangeError}
262  *     If `timeout` or `interval` are not unsigned integers.
263  */
264 export function PollPromise(func, options = {}) {
265   const {
266     errorMessage = "PollPromise timed out",
267     interval = 10,
268     timeout = null,
269   } = options;
270   const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
271   let didTimeOut = false;
273   if (typeof func != "function") {
274     throw new TypeError();
275   }
276   if (timeout != null && typeof timeout != "number") {
277     throw new TypeError();
278   }
279   if (typeof interval != "number") {
280     throw new TypeError();
281   }
282   if (
283     (timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
284     !Number.isInteger(interval) ||
285     interval < 0
286   ) {
287     throw new RangeError();
288   }
290   return new Promise((resolve, reject) => {
291     let start, end;
293     if (Number.isInteger(timeout)) {
294       start = new Date().getTime();
295       end = start + timeout;
296     }
298     let evalFn = () => {
299       new Promise(func)
300         .then(resolve, rejected => {
301           if (typeof rejected != "undefined") {
302             throw rejected;
303           }
305           // return if there is a timeout and set to 0,
306           // allowing |func| to be evaluated at least once
307           if (
308             typeof end != "undefined" &&
309             (start == end || new Date().getTime() >= end)
310           ) {
311             didTimeOut = true;
312             resolve(rejected);
313           }
314         })
315         .catch(reject);
316     };
318     // the repeating slack timer waits |interval|
319     // before invoking |evalFn|
320     evalFn();
322     timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
323   }).then(
324     res => {
325       if (didTimeOut) {
326         lazy.logger.warn(`${errorMessage} after ${timeout} ms`);
327       }
328       timer.cancel();
329       return res;
330     },
331     err => {
332       timer.cancel();
333       throw err;
334     }
335   );