Bug 1637473 [wpt PR 23557] - De-flaky pointerevents/pointerevent_capture_mouse.html...
[gecko.git] / remote / Sync.jsm
blob0e43c1fd5d6cc15f3ade8fe072767c34fa4e707a
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 "use strict";
7 var EXPORTED_SYMBOLS = [
8   "EventPromise",
9   "executeSoon",
10   "MessagePromise",
11   "PollPromise",
14 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
16 const { TYPE_REPEATING_SLACK } = Ci.nsITimer;
18 /**
19  * Wait for a single event to be fired on a specific EventListener.
20  *
21  * The returned promise is guaranteed to not be called before the
22  * next event tick after the event listener is called, so that all
23  * other event listeners for the element are executed before the
24  * handler is executed.  For example:
25  *
26  *     const promise = new EventPromise(element, "myEvent");
27  *     // same event tick here
28  *     await promise;
29  *     // next event tick here
30  *
31  * @param {EventListener} listener
32  *     Object which receives a notification (an object that implements
33  *     the Event interface) when an event of the specificed type occurs.
34  * @param {string} type
35  *     Case-sensitive string representing the event type to listen for.
36  * @param {boolean?} [false] options.capture
37  *     Indicates the event will be despatched to this subject,
38  *     before it bubbles down to any EventTarget beneath it in the
39  *     DOM tree.
40  * @param {boolean?} [false] options.wantsUntrusted
41  *     Receive synthetic events despatched by web content.
42  * @param {boolean?} [false] options.mozSystemGroup
43  *     Determines whether to add listener to the system group.
44  *
45  * @return {Promise.<Event>}
46  *
47  * @throws {TypeError}
48  */
49 function EventPromise(
50   listener,
51   type,
52   options = {
53     capture: false,
54     wantsUntrusted: false,
55     mozSystemGroup: false,
56   }
57 ) {
58   if (!listener || !("addEventListener" in listener)) {
59     throw new TypeError();
60   }
61   if (typeof type != "string") {
62     throw new TypeError();
63   }
64   if (
65     ("capture" in options && typeof options.capture != "boolean") ||
66     ("wantsUntrusted" in options &&
67       typeof options.wantsUntrusted != "boolean") ||
68     ("mozSystemGroup" in options && typeof options.mozSystemGroup != "boolean")
69   ) {
70     throw new TypeError();
71   }
73   options.once = true;
75   return new Promise(resolve => {
76     listener.addEventListener(
77       type,
78       event => {
79         executeSoon(() => resolve(event));
80       },
81       options
82     );
83   });
86 /**
87  * Wait for the next tick in the event loop to execute a callback.
88  *
89  * @param {function} fn
90  *     Function to be executed.
91  */
92 function executeSoon(fn) {
93   if (typeof fn != "function") {
94     throw new TypeError();
95   }
97   Services.tm.dispatchToMainThread(fn);
101  * Awaits a single IPC message.
103  * @param {nsIMessageSender} target
104  * @param {string} name
106  * @return {Promise}
108  * @throws {TypeError}
109  *     If target is not an nsIMessageSender.
110  */
111 function MessagePromise(target, name) {
112   if (!(target instanceof Ci.nsIMessageSender)) {
113     throw new TypeError();
114   }
116   return new Promise(resolve => {
117     const onMessage = (...args) => {
118       target.removeMessageListener(name, onMessage);
119       resolve(...args);
120     };
121     target.addMessageListener(name, onMessage);
122   });
126  * Runs a Promise-like function off the main thread until it is resolved
127  * through ``resolve`` or ``rejected`` callbacks.  The function is
128  * guaranteed to be run at least once, irregardless of the timeout.
130  * The ``func`` is evaluated every ``interval`` for as long as its
131  * runtime duration does not exceed ``interval``.  Evaluations occur
132  * sequentially, meaning that evaluations of ``func`` are queued if
133  * the runtime evaluation duration of ``func`` is greater than ``interval``.
135  * ``func`` is given two arguments, ``resolve`` and ``reject``,
136  * of which one must be called for the evaluation to complete.
137  * Calling ``resolve`` with an argument indicates that the expected
138  * wait condition was met and will return the passed value to the
139  * caller.  Conversely, calling ``reject`` will evaluate ``func``
140  * again until the ``timeout`` duration has elapsed or ``func`` throws.
141  * The passed value to ``reject`` will also be returned to the caller
142  * once the wait has expired.
144  * Usage::
146  *     let els = new PollPromise((resolve, reject) => {
147  *       let res = document.querySelectorAll("p");
148  *       if (res.length > 0) {
149  *         resolve(Array.from(res));
150  *       } else {
151  *         reject([]);
152  *       }
153  *     }, {timeout: 1000});
155  * @param {Condition} func
156  *     Function to run off the main thread.
157  * @param {number=} [timeout] timeout
158  *     Desired timeout if wanted.  If 0 or less than the runtime evaluation
159  *     time of ``func``, ``func`` is guaranteed to run at least once.
160  *     Defaults to using no timeout.
161  * @param {number=} [interval=10] interval
162  *     Duration between each poll of ``func`` in milliseconds.
163  *     Defaults to 10 milliseconds.
165  * @return {Promise.<*>}
166  *     Yields the value passed to ``func``'s
167  *     ``resolve`` or ``reject`` callbacks.
169  * @throws {*}
170  *     If ``func`` throws, its error is propagated.
171  * @throws {TypeError}
172  *     If `timeout` or `interval`` are not numbers.
173  * @throws {RangeError}
174  *     If `timeout` or `interval` are not unsigned integers.
175  */
176 function PollPromise(func, { timeout = null, interval = 10 } = {}) {
177   const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
179   if (typeof func != "function") {
180     throw new TypeError();
181   }
182   if (timeout != null && typeof timeout != "number") {
183     throw new TypeError();
184   }
185   if (typeof interval != "number") {
186     throw new TypeError();
187   }
188   if (
189     (timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
190     !Number.isInteger(interval) ||
191     interval < 0
192   ) {
193     throw new RangeError();
194   }
196   return new Promise((resolve, reject) => {
197     let start, end;
199     if (Number.isInteger(timeout)) {
200       start = new Date().getTime();
201       end = start + timeout;
202     }
204     let evalFn = () => {
205       new Promise(func)
206         .then(resolve, rejected => {
207           if (typeof rejected != "undefined") {
208             throw rejected;
209           }
211           // return if there is a timeout and set to 0,
212           // allowing |func| to be evaluated at least once
213           if (
214             typeof end != "undefined" &&
215             (start == end || new Date().getTime() >= end)
216           ) {
217             resolve(rejected);
218           }
219         })
220         .catch(reject);
221     };
223     // the repeating slack timer waits |interval|
224     // before invoking |evalFn|
225     evalFn();
227     timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
228   }).then(
229     res => {
230       timer.cancel();
231       return res;
232     },
233     err => {
234       timer.cancel();
235       throw err;
236     }
237   );