Bug 1842773 - Part 28: Enable Atomics with resizable buffers. r=sfink
[gecko.git] / services / common / async.sys.mjs
blob564b46a07164a8f6df1db100e9d115b0ac448571
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer");
7 /*
8  * Helpers for various async operations.
9  */
10 export var Async = {
11   /**
12    * Execute an arbitrary number of asynchronous functions one after the
13    * other, passing the callback arguments on to the next one.  All functions
14    * must take a callback function as their last argument.  The 'this' object
15    * will be whatever chain()'s is.
16    *
17    * @usage this._chain = Async.chain;
18    *        this._chain(this.foo, this.bar, this.baz)(args, for, foo)
19    *
20    * This is equivalent to:
21    *
22    *   let self = this;
23    *   self.foo(args, for, foo, function (bars, args) {
24    *     self.bar(bars, args, function (baz, params) {
25    *       self.baz(baz, params);
26    *     });
27    *   });
28    */
29   chain: function chain(...funcs) {
30     let thisObj = this;
31     return function callback() {
32       if (funcs.length) {
33         let args = [...arguments, callback];
34         let f = funcs.shift();
35         f.apply(thisObj, args);
36       }
37     };
38   },
40   /**
41    * Check if the app is still ready (not quitting). Returns true, or throws an
42    * exception if not ready.
43    */
44   checkAppReady: function checkAppReady() {
45     // Watch for app-quit notification to stop any sync calls
46     Services.obs.addObserver(function onQuitApplication() {
47       Services.obs.removeObserver(onQuitApplication, "quit-application");
48       Async.checkAppReady = Async.promiseYield = function () {
49         let exception = Components.Exception(
50           "App. Quitting",
51           Cr.NS_ERROR_ABORT
52         );
53         exception.appIsShuttingDown = true;
54         throw exception;
55       };
56     }, "quit-application");
57     // In the common case, checkAppReady just returns true
58     return (Async.checkAppReady = function () {
59       return true;
60     })();
61   },
63   /**
64    * Check if the app is still ready (not quitting). Returns true if the app
65    * is ready, or false if it is being shut down.
66    */
67   isAppReady() {
68     try {
69       return Async.checkAppReady();
70     } catch (ex) {
71       if (!Async.isShutdownException(ex)) {
72         throw ex;
73       }
74     }
75     return false;
76   },
78   /**
79    * Check if the passed exception is one raised by checkAppReady. Typically
80    * this will be used in exception handlers to allow such exceptions to
81    * make their way to the top frame and allow the app to actually terminate.
82    */
83   isShutdownException(exception) {
84     return exception && exception.appIsShuttingDown === true;
85   },
87   /**
88    * A "tight loop" of promises can still lock up the browser for some time.
89    * Periodically waiting for a promise returned by this function will solve
90    * that.
91    * You should probably not use this method directly and instead use jankYielder
92    * below.
93    * Some reference here:
94    * - https://gist.github.com/jesstelford/bbb30b983bddaa6e5fef2eb867d37678
95    * - https://bugzilla.mozilla.org/show_bug.cgi?id=1094248
96    */
97   promiseYield() {
98     return new Promise(resolve => {
99       Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
100     });
101   },
103   /**
104    * Shared state for yielding every N calls.
105    *
106    * Can be passed to multiple Async.yieldingForEach to have them overall yield
107    * every N iterations.
108    */
109   yieldState(yieldEvery = 50) {
110     let iterations = 0;
112     return {
113       shouldYield() {
114         ++iterations;
115         return iterations % yieldEvery === 0;
116       },
117     };
118   },
120   /**
121    * Apply the given function to each element of the iterable, yielding the
122    * event loop every yieldEvery iterations.
123    *
124    * @param iterable {Iterable}
125    *        The iterable or iterator to iterate through.
126    *
127    * @param fn {(*) -> void|boolean}
128    *        The function to be called on each element of the iterable.
129    *
130    *        Returning true from the function will stop the iteration.
131    *
132    * @param [yieldEvery = 50] {number|object}
133    *        The number of iterations to complete before yielding back to the event
134    *        loop.
135    *
136    * @return {boolean}
137    *         Whether or not the function returned early.
138    */
139   async yieldingForEach(iterable, fn, yieldEvery = 50) {
140     const yieldState =
141       typeof yieldEvery === "number"
142         ? Async.yieldState(yieldEvery)
143         : yieldEvery;
144     let iteration = 0;
146     for (const item of iterable) {
147       let result = fn(item, iteration++);
148       if (typeof result !== "undefined" && typeof result.then !== "undefined") {
149         // If we await result when it is not a Promise, we create an
150         // automatically resolved promise, which is exactly the case that we
151         // are trying to avoid.
152         result = await result;
153       }
155       if (result === true) {
156         return true;
157       }
159       if (yieldState.shouldYield()) {
160         await Async.promiseYield();
161         Async.checkAppReady();
162       }
163     }
165     return false;
166   },
168   asyncQueueCaller(log) {
169     return new AsyncQueueCaller(log);
170   },
172   asyncObserver(log, obj) {
173     return new AsyncObserver(log, obj);
174   },
176   watchdog() {
177     return new Watchdog();
178   },
182  * Allows consumers to enqueue asynchronous callbacks to be called in order.
183  * Typically this is used when providing a callback to a caller that doesn't
184  * await on promises.
185  */
186 class AsyncQueueCaller {
187   constructor(log) {
188     this._log = log;
189     this._queue = Promise.resolve();
190     this.QueryInterface = ChromeUtils.generateQI([
191       "nsIObserver",
192       "nsISupportsWeakReference",
193     ]);
194   }
196   /**
197    * /!\ Never await on another function that calls enqueueCall /!\
198    *     on the same queue or we will deadlock.
199    */
200   enqueueCall(func) {
201     this._queue = (async () => {
202       await this._queue;
203       try {
204         return await func();
205       } catch (e) {
206         this._log.error(e);
207         return false;
208       }
209     })();
210   }
212   promiseCallsComplete() {
213     return this._queue;
214   }
218  * Subclass of AsyncQueueCaller that can be used with Services.obs directly.
219  * When this observe() is called, it will enqueue a call to the consumers's
220  * observe().
221  */
222 class AsyncObserver extends AsyncQueueCaller {
223   constructor(obj, log) {
224     super(log);
225     this.obj = obj;
226   }
228   observe(subject, topic, data) {
229     this.enqueueCall(() => this.obj.observe(subject, topic, data));
230   }
232   promiseObserversComplete() {
233     return this.promiseCallsComplete();
234   }
238  * Woof! Signals an operation to abort, either at shutdown or after a timeout.
239  * The buffered engine uses this to abort long-running merges, so that they
240  * don't prevent Firefox from quitting, or block future syncs.
241  */
242 class Watchdog {
243   constructor() {
244     this.controller = new AbortController();
245     this.timer = new Timer();
247     /**
248      * The reason for signaling an abort. `null` if not signaled,
249      * `"timeout"` if the watchdog timer fired, or `"shutdown"` if the app is
250      * is quitting.
251      *
252      * @type {String?}
253      */
254     this.abortReason = null;
255   }
257   /**
258    * Returns the abort signal for this watchdog. This can be passed to APIs
259    * that take a signal for cancellation, like `SyncedBookmarksMirror::apply`
260    * or `fetch`.
261    *
262    * @type {AbortSignal}
263    */
264   get signal() {
265     return this.controller.signal;
266   }
268   /**
269    * Starts the watchdog timer, and listens for the app quitting.
270    *
271    * @param {Number} delay
272    *                 The time to wait before signaling the operation to abort.
273    */
274   start(delay) {
275     if (!this.signal.aborted) {
276       Services.obs.addObserver(this, "quit-application");
277       this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
278     }
279   }
281   /**
282    * Stops the watchdog timer and removes any listeners. This should be called
283    * after the operation finishes.
284    */
285   stop() {
286     if (!this.signal.aborted) {
287       Services.obs.removeObserver(this, "quit-application");
288       this.timer.cancel();
289     }
290   }
292   observe(subject, topic, data) {
293     if (topic == "timer-callback") {
294       this.abortReason = "timeout";
295     } else if (topic == "quit-application") {
296       this.abortReason = "shutdown";
297     }
298     this.stop();
299     this.controller.abort();
300   }