mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 05:11:16 +00:00
6e02473120
Differential Revision: https://phabricator.services.mozilla.com/D202986
302 lines
8.0 KiB
JavaScript
302 lines
8.0 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer");
|
|
|
|
/*
|
|
* Helpers for various async operations.
|
|
*/
|
|
export var Async = {
|
|
/**
|
|
* Execute an arbitrary number of asynchronous functions one after the
|
|
* other, passing the callback arguments on to the next one. All functions
|
|
* must take a callback function as their last argument. The 'this' object
|
|
* will be whatever chain()'s is.
|
|
*
|
|
* @usage this._chain = Async.chain;
|
|
* this._chain(this.foo, this.bar, this.baz)(args, for, foo)
|
|
*
|
|
* This is equivalent to:
|
|
*
|
|
* let self = this;
|
|
* self.foo(args, for, foo, function (bars, args) {
|
|
* self.bar(bars, args, function (baz, params) {
|
|
* self.baz(baz, params);
|
|
* });
|
|
* });
|
|
*/
|
|
chain: function chain(...funcs) {
|
|
let thisObj = this;
|
|
return function callback() {
|
|
if (funcs.length) {
|
|
let args = [...arguments, callback];
|
|
let f = funcs.shift();
|
|
f.apply(thisObj, args);
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Check if the app is still ready (not quitting). Returns true, or throws an
|
|
* exception if not ready.
|
|
*/
|
|
checkAppReady: function checkAppReady() {
|
|
// Watch for app-quit notification to stop any sync calls
|
|
Services.obs.addObserver(function onQuitApplication() {
|
|
Services.obs.removeObserver(onQuitApplication, "quit-application");
|
|
Async.checkAppReady = Async.promiseYield = function () {
|
|
let exception = Components.Exception(
|
|
"App. Quitting",
|
|
Cr.NS_ERROR_ABORT
|
|
);
|
|
exception.appIsShuttingDown = true;
|
|
throw exception;
|
|
};
|
|
}, "quit-application");
|
|
// In the common case, checkAppReady just returns true
|
|
return (Async.checkAppReady = function () {
|
|
return true;
|
|
})();
|
|
},
|
|
|
|
/**
|
|
* Check if the app is still ready (not quitting). Returns true if the app
|
|
* is ready, or false if it is being shut down.
|
|
*/
|
|
isAppReady() {
|
|
try {
|
|
return Async.checkAppReady();
|
|
} catch (ex) {
|
|
if (!Async.isShutdownException(ex)) {
|
|
throw ex;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Check if the passed exception is one raised by checkAppReady. Typically
|
|
* this will be used in exception handlers to allow such exceptions to
|
|
* make their way to the top frame and allow the app to actually terminate.
|
|
*/
|
|
isShutdownException(exception) {
|
|
return exception && exception.appIsShuttingDown === true;
|
|
},
|
|
|
|
/**
|
|
* A "tight loop" of promises can still lock up the browser for some time.
|
|
* Periodically waiting for a promise returned by this function will solve
|
|
* that.
|
|
* You should probably not use this method directly and instead use jankYielder
|
|
* below.
|
|
* Some reference here:
|
|
* - https://gist.github.com/jesstelford/bbb30b983bddaa6e5fef2eb867d37678
|
|
* - https://bugzilla.mozilla.org/show_bug.cgi?id=1094248
|
|
*/
|
|
promiseYield() {
|
|
return new Promise(resolve => {
|
|
Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Shared state for yielding every N calls.
|
|
*
|
|
* Can be passed to multiple Async.yieldingForEach to have them overall yield
|
|
* every N iterations.
|
|
*/
|
|
yieldState(yieldEvery = 50) {
|
|
let iterations = 0;
|
|
|
|
return {
|
|
shouldYield() {
|
|
++iterations;
|
|
return iterations % yieldEvery === 0;
|
|
},
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Apply the given function to each element of the iterable, yielding the
|
|
* event loop every yieldEvery iterations.
|
|
*
|
|
* @param iterable {Iterable}
|
|
* The iterable or iterator to iterate through.
|
|
*
|
|
* @param fn {(*) -> void|boolean}
|
|
* The function to be called on each element of the iterable.
|
|
*
|
|
* Returning true from the function will stop the iteration.
|
|
*
|
|
* @param [yieldEvery = 50] {number|object}
|
|
* The number of iterations to complete before yielding back to the event
|
|
* loop.
|
|
*
|
|
* @return {boolean}
|
|
* Whether or not the function returned early.
|
|
*/
|
|
async yieldingForEach(iterable, fn, yieldEvery = 50) {
|
|
const yieldState =
|
|
typeof yieldEvery === "number"
|
|
? Async.yieldState(yieldEvery)
|
|
: yieldEvery;
|
|
let iteration = 0;
|
|
|
|
for (const item of iterable) {
|
|
let result = fn(item, iteration++);
|
|
if (typeof result !== "undefined" && typeof result.then !== "undefined") {
|
|
// If we await result when it is not a Promise, we create an
|
|
// automatically resolved promise, which is exactly the case that we
|
|
// are trying to avoid.
|
|
result = await result;
|
|
}
|
|
|
|
if (result === true) {
|
|
return true;
|
|
}
|
|
|
|
if (yieldState.shouldYield()) {
|
|
await Async.promiseYield();
|
|
Async.checkAppReady();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
asyncQueueCaller(log) {
|
|
return new AsyncQueueCaller(log);
|
|
},
|
|
|
|
asyncObserver(log, obj) {
|
|
return new AsyncObserver(log, obj);
|
|
},
|
|
|
|
watchdog() {
|
|
return new Watchdog();
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Allows consumers to enqueue asynchronous callbacks to be called in order.
|
|
* Typically this is used when providing a callback to a caller that doesn't
|
|
* await on promises.
|
|
*/
|
|
class AsyncQueueCaller {
|
|
constructor(log) {
|
|
this._log = log;
|
|
this._queue = Promise.resolve();
|
|
this.QueryInterface = ChromeUtils.generateQI([
|
|
"nsIObserver",
|
|
"nsISupportsWeakReference",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* /!\ Never await on another function that calls enqueueCall /!\
|
|
* on the same queue or we will deadlock.
|
|
*/
|
|
enqueueCall(func) {
|
|
this._queue = (async () => {
|
|
await this._queue;
|
|
try {
|
|
return await func();
|
|
} catch (e) {
|
|
this._log.error(e);
|
|
return false;
|
|
}
|
|
})();
|
|
}
|
|
|
|
promiseCallsComplete() {
|
|
return this._queue;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Subclass of AsyncQueueCaller that can be used with Services.obs directly.
|
|
* When this observe() is called, it will enqueue a call to the consumers's
|
|
* observe().
|
|
*/
|
|
class AsyncObserver extends AsyncQueueCaller {
|
|
constructor(obj, log) {
|
|
super(log);
|
|
this.obj = obj;
|
|
}
|
|
|
|
observe(subject, topic, data) {
|
|
this.enqueueCall(() => this.obj.observe(subject, topic, data));
|
|
}
|
|
|
|
promiseObserversComplete() {
|
|
return this.promiseCallsComplete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Woof! Signals an operation to abort, either at shutdown or after a timeout.
|
|
* The buffered engine uses this to abort long-running merges, so that they
|
|
* don't prevent Firefox from quitting, or block future syncs.
|
|
*/
|
|
class Watchdog {
|
|
constructor() {
|
|
this.controller = new AbortController();
|
|
this.timer = new Timer();
|
|
|
|
/**
|
|
* The reason for signaling an abort. `null` if not signaled,
|
|
* `"timeout"` if the watchdog timer fired, or `"shutdown"` if the app is
|
|
* is quitting.
|
|
*
|
|
* @type {String?}
|
|
*/
|
|
this.abortReason = null;
|
|
}
|
|
|
|
/**
|
|
* Returns the abort signal for this watchdog. This can be passed to APIs
|
|
* that take a signal for cancellation, like `SyncedBookmarksMirror::apply`
|
|
* or `fetch`.
|
|
*
|
|
* @type {AbortSignal}
|
|
*/
|
|
get signal() {
|
|
return this.controller.signal;
|
|
}
|
|
|
|
/**
|
|
* Starts the watchdog timer, and listens for the app quitting.
|
|
*
|
|
* @param {Number} delay
|
|
* The time to wait before signaling the operation to abort.
|
|
*/
|
|
start(delay) {
|
|
if (!this.signal.aborted) {
|
|
Services.obs.addObserver(this, "quit-application");
|
|
this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops the watchdog timer and removes any listeners. This should be called
|
|
* after the operation finishes.
|
|
*/
|
|
stop() {
|
|
if (!this.signal.aborted) {
|
|
Services.obs.removeObserver(this, "quit-application");
|
|
this.timer.cancel();
|
|
}
|
|
}
|
|
|
|
observe(subject, topic) {
|
|
if (topic == "timer-callback") {
|
|
this.abortReason = "timeout";
|
|
} else if (topic == "quit-application") {
|
|
this.abortReason = "shutdown";
|
|
}
|
|
this.stop();
|
|
this.controller.abort();
|
|
}
|
|
}
|