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");
8 * Helpers for various async operations.
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.
17 * @usage this._chain = Async.chain;
18 * this._chain(this.foo, this.bar, this.baz)(args, for, foo)
20 * This is equivalent to:
23 * self.foo(args, for, foo, function (bars, args) {
24 * self.bar(bars, args, function (baz, params) {
25 * self.baz(baz, params);
29 chain: function chain(...funcs) {
31 return function callback() {
33 let args = [...arguments, callback];
34 let f = funcs.shift();
35 f.apply(thisObj, args);
41 * Check if the app is still ready (not quitting). Returns true, or throws an
42 * exception if not ready.
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(
53 exception.appIsShuttingDown = true;
56 }, "quit-application");
57 // In the common case, checkAppReady just returns true
58 return (Async.checkAppReady = function () {
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.
69 return Async.checkAppReady();
71 if (!Async.isShutdownException(ex)) {
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.
83 isShutdownException(exception) {
84 return exception && exception.appIsShuttingDown === true;
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
91 * You should probably not use this method directly and instead use jankYielder
93 * Some reference here:
94 * - https://gist.github.com/jesstelford/bbb30b983bddaa6e5fef2eb867d37678
95 * - https://bugzilla.mozilla.org/show_bug.cgi?id=1094248
98 return new Promise(resolve => {
99 Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
104 * Shared state for yielding every N calls.
106 * Can be passed to multiple Async.yieldingForEach to have them overall yield
107 * every N iterations.
109 yieldState(yieldEvery = 50) {
115 return iterations % yieldEvery === 0;
121 * Apply the given function to each element of the iterable, yielding the
122 * event loop every yieldEvery iterations.
124 * @param iterable {Iterable}
125 * The iterable or iterator to iterate through.
127 * @param fn {(*) -> void|boolean}
128 * The function to be called on each element of the iterable.
130 * Returning true from the function will stop the iteration.
132 * @param [yieldEvery = 50] {number|object}
133 * The number of iterations to complete before yielding back to the event
137 * Whether or not the function returned early.
139 async yieldingForEach(iterable, fn, yieldEvery = 50) {
141 typeof yieldEvery === "number"
142 ? Async.yieldState(yieldEvery)
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;
155 if (result === true) {
159 if (yieldState.shouldYield()) {
160 await Async.promiseYield();
161 Async.checkAppReady();
168 asyncQueueCaller(log) {
169 return new AsyncQueueCaller(log);
172 asyncObserver(log, obj) {
173 return new AsyncObserver(log, obj);
177 return new Watchdog();
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
186 class AsyncQueueCaller {
189 this._queue = Promise.resolve();
190 this.QueryInterface = ChromeUtils.generateQI([
192 "nsISupportsWeakReference",
197 * /!\ Never await on another function that calls enqueueCall /!\
198 * on the same queue or we will deadlock.
201 this._queue = (async () => {
212 promiseCallsComplete() {
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
222 class AsyncObserver extends AsyncQueueCaller {
223 constructor(obj, log) {
228 observe(subject, topic, data) {
229 this.enqueueCall(() => this.obj.observe(subject, topic, data));
232 promiseObserversComplete() {
233 return this.promiseCallsComplete();
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.
244 this.controller = new AbortController();
245 this.timer = new Timer();
248 * The reason for signaling an abort. `null` if not signaled,
249 * `"timeout"` if the watchdog timer fired, or `"shutdown"` if the app is
254 this.abortReason = null;
258 * Returns the abort signal for this watchdog. This can be passed to APIs
259 * that take a signal for cancellation, like `SyncedBookmarksMirror::apply`
262 * @type {AbortSignal}
265 return this.controller.signal;
269 * Starts the watchdog timer, and listens for the app quitting.
271 * @param {Number} delay
272 * The time to wait before signaling the operation to abort.
275 if (!this.signal.aborted) {
276 Services.obs.addObserver(this, "quit-application");
277 this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
282 * Stops the watchdog timer and removes any listeners. This should be called
283 * after the operation finishes.
286 if (!this.signal.aborted) {
287 Services.obs.removeObserver(this, "quit-application");
292 observe(subject, topic, data) {
293 if (topic == "timer-callback") {
294 this.abortReason = "timeout";
295 } else if (topic == "quit-application") {
296 this.abortReason = "shutdown";
299 this.controller.abort();