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