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 var EXPORTED_SYMBOLS = ["Async"];
7 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer");
12 * Helpers for various async operations.
16 * Execute an arbitrary number of asynchronous functions one after the
17 * other, passing the callback arguments on to the next one. All functions
18 * must take a callback function as their last argument. The 'this' object
19 * will be whatever chain()'s is.
21 * @usage this._chain = Async.chain;
22 * this._chain(this.foo, this.bar, this.baz)(args, for, foo)
24 * This is equivalent to:
27 * self.foo(args, for, foo, function (bars, args) {
28 * self.bar(bars, args, function (baz, params) {
29 * self.baz(baz, params);
33 chain: function chain(...funcs) {
35 return function callback() {
37 let args = [...arguments, callback];
38 let f = funcs.shift();
39 f.apply(thisObj, args);
45 * Check if the app is still ready (not quitting). Returns true, or throws an
46 * exception if not ready.
48 checkAppReady: function checkAppReady() {
49 // Watch for app-quit notification to stop any sync calls
50 Services.obs.addObserver(function onQuitApplication() {
51 Services.obs.removeObserver(onQuitApplication, "quit-application");
52 Async.checkAppReady = Async.promiseYield = function() {
53 let exception = Components.Exception(
57 exception.appIsShuttingDown = true;
60 }, "quit-application");
61 // In the common case, checkAppReady just returns true
62 return (Async.checkAppReady = function() {
68 * Check if the app is still ready (not quitting). Returns true if the app
69 * is ready, or false if it is being shut down.
73 return Async.checkAppReady();
75 if (!Async.isShutdownException(ex)) {
83 * Check if the passed exception is one raised by checkAppReady. Typically
84 * this will be used in exception handlers to allow such exceptions to
85 * make their way to the top frame and allow the app to actually terminate.
87 isShutdownException(exception) {
88 return exception && exception.appIsShuttingDown === true;
92 * A "tight loop" of promises can still lock up the browser for some time.
93 * Periodically waiting for a promise returned by this function will solve
95 * You should probably not use this method directly and instead use jankYielder
97 * Some reference here:
98 * - https://gist.github.com/jesstelford/bbb30b983bddaa6e5fef2eb867d37678
99 * - https://bugzilla.mozilla.org/show_bug.cgi?id=1094248
102 return new Promise(resolve => {
103 Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
108 * Shared state for yielding every N calls.
110 * Can be passed to multiple Async.yieldingForEach to have them overall yield
111 * every N iterations.
113 yieldState(yieldEvery = 50) {
119 return iterations % yieldEvery === 0;
125 * Apply the given function to each element of the iterable, yielding the
126 * event loop every yieldEvery iterations.
128 * @param iterable {Iterable}
129 * The iterable or iterator to iterate through.
131 * @param fn {(*) -> void|boolean}
132 * The function to be called on each element of the iterable.
134 * Returning true from the function will stop the iteration.
136 * @param [yieldEvery = 50] {number|object}
137 * The number of iterations to complete before yielding back to the event
141 * Whether or not the function returned early.
143 async yieldingForEach(iterable, fn, yieldEvery = 50) {
145 typeof yieldEvery === "number"
146 ? Async.yieldState(yieldEvery)
150 for (const item of iterable) {
151 let result = fn(item, iteration++);
152 if (typeof result !== "undefined" && typeof result.then !== "undefined") {
153 // If we await result when it is not a Promise, we create an
154 // automatically resolved promise, which is exactly the case that we
155 // are trying to avoid.
156 result = await result;
159 if (result === true) {
163 if (yieldState.shouldYield()) {
164 await Async.promiseYield();
165 Async.checkAppReady();
172 asyncQueueCaller(log) {
173 return new AsyncQueueCaller(log);
176 asyncObserver(log, obj) {
177 return new AsyncObserver(log, obj);
181 return new Watchdog();
186 * Allows consumers to enqueue asynchronous callbacks to be called in order.
187 * Typically this is used when providing a callback to a caller that doesn't
190 class AsyncQueueCaller {
193 this._queue = Promise.resolve();
194 this.QueryInterface = ChromeUtils.generateQI([
196 "nsISupportsWeakReference",
201 * /!\ Never await on another function that calls enqueueCall /!\
202 * on the same queue or we will deadlock.
205 this._queue = (async () => {
216 promiseCallsComplete() {
222 * Subclass of AsyncQueueCaller that can be used with Services.obs directly.
223 * When this observe() is called, it will enqueue a call to the consumers's
226 class AsyncObserver extends AsyncQueueCaller {
227 constructor(obj, log) {
232 observe(subject, topic, data) {
233 this.enqueueCall(() => this.obj.observe(subject, topic, data));
236 promiseObserversComplete() {
237 return this.promiseCallsComplete();
242 * Woof! Signals an operation to abort, either at shutdown or after a timeout.
243 * The buffered engine uses this to abort long-running merges, so that they
244 * don't prevent Firefox from quitting, or block future syncs.
248 this.controller = new AbortController();
249 this.timer = new Timer();
252 * The reason for signaling an abort. `null` if not signaled,
253 * `"timeout"` if the watchdog timer fired, or `"shutdown"` if the app is
258 this.abortReason = null;
262 * Returns the abort signal for this watchdog. This can be passed to APIs
263 * that take a signal for cancellation, like `SyncedBookmarksMirror::apply`
266 * @type {AbortSignal}
269 return this.controller.signal;
273 * Starts the watchdog timer, and listens for the app quitting.
275 * @param {Number} delay
276 * The time to wait before signaling the operation to abort.
279 if (!this.signal.aborted) {
280 Services.obs.addObserver(this, "quit-application");
281 this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
286 * Stops the watchdog timer and removes any listeners. This should be called
287 * after the operation finishes.
290 if (!this.signal.aborted) {
291 Services.obs.removeObserver(this, "quit-application");
296 observe(subject, topic, data) {
297 if (topic == "timer-callback") {
298 this.abortReason = "timeout";
299 } else if (topic == "quit-application") {
300 this.abortReason = "shutdown";
303 this.controller.abort();