Bug 1728955: part 3) Add logging to `nsBaseClipboard`. r=masayuki
[gecko.git] / services / common / async.js
blob42bfb72a26ebd34ebf87e2285d192513a30da8e6
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.
13  */
14 var Async = {
15   /**
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.
20    *
21    * @usage this._chain = Async.chain;
22    *        this._chain(this.foo, this.bar, this.baz)(args, for, foo)
23    *
24    * This is equivalent to:
25    *
26    *   let self = this;
27    *   self.foo(args, for, foo, function (bars, args) {
28    *     self.bar(bars, args, function (baz, params) {
29    *       self.baz(baz, params);
30    *     });
31    *   });
32    */
33   chain: function chain(...funcs) {
34     let thisObj = this;
35     return function callback() {
36       if (funcs.length) {
37         let args = [...arguments, callback];
38         let f = funcs.shift();
39         f.apply(thisObj, args);
40       }
41     };
42   },
44   /**
45    * Check if the app is still ready (not quitting). Returns true, or throws an
46    * exception if not ready.
47    */
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(
54           "App. Quitting",
55           Cr.NS_ERROR_ABORT
56         );
57         exception.appIsShuttingDown = true;
58         throw exception;
59       };
60     }, "quit-application");
61     // In the common case, checkAppReady just returns true
62     return (Async.checkAppReady = function() {
63       return true;
64     })();
65   },
67   /**
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.
70    */
71   isAppReady() {
72     try {
73       return Async.checkAppReady();
74     } catch (ex) {
75       if (!Async.isShutdownException(ex)) {
76         throw ex;
77       }
78     }
79     return false;
80   },
82   /**
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.
86    */
87   isShutdownException(exception) {
88     return exception && exception.appIsShuttingDown === true;
89   },
91   /**
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
94    * that.
95    * You should probably not use this method directly and instead use jankYielder
96    * below.
97    * Some reference here:
98    * - https://gist.github.com/jesstelford/bbb30b983bddaa6e5fef2eb867d37678
99    * - https://bugzilla.mozilla.org/show_bug.cgi?id=1094248
100    */
101   promiseYield() {
102     return new Promise(resolve => {
103       Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
104     });
105   },
107   /**
108    * Shared state for yielding every N calls.
109    *
110    * Can be passed to multiple Async.yieldingForEach to have them overall yield
111    * every N iterations.
112    */
113   yieldState(yieldEvery = 50) {
114     let iterations = 0;
116     return {
117       shouldYield() {
118         ++iterations;
119         return iterations % yieldEvery === 0;
120       },
121     };
122   },
124   /**
125    * Apply the given function to each element of the iterable, yielding the
126    * event loop every yieldEvery iterations.
127    *
128    * @param iterable {Iterable}
129    *        The iterable or iterator to iterate through.
130    *
131    * @param fn {(*) -> void|boolean}
132    *        The function to be called on each element of the iterable.
133    *
134    *        Returning true from the function will stop the iteration.
135    *
136    * @param [yieldEvery = 50] {number|object}
137    *        The number of iterations to complete before yielding back to the event
138    *        loop.
139    *
140    * @return {boolean}
141    *         Whether or not the function returned early.
142    */
143   async yieldingForEach(iterable, fn, yieldEvery = 50) {
144     const yieldState =
145       typeof yieldEvery === "number"
146         ? Async.yieldState(yieldEvery)
147         : yieldEvery;
148     let iteration = 0;
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;
157       }
159       if (result === true) {
160         return true;
161       }
163       if (yieldState.shouldYield()) {
164         await Async.promiseYield();
165         Async.checkAppReady();
166       }
167     }
169     return false;
170   },
172   asyncQueueCaller(log) {
173     return new AsyncQueueCaller(log);
174   },
176   asyncObserver(log, obj) {
177     return new AsyncObserver(log, obj);
178   },
180   watchdog() {
181     return new Watchdog();
182   },
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
188  * await on promises.
189  */
190 class AsyncQueueCaller {
191   constructor(log) {
192     this._log = log;
193     this._queue = Promise.resolve();
194     this.QueryInterface = ChromeUtils.generateQI([
195       "nsIObserver",
196       "nsISupportsWeakReference",
197     ]);
198   }
200   /**
201    * /!\ Never await on another function that calls enqueueCall /!\
202    *     on the same queue or we will deadlock.
203    */
204   enqueueCall(func) {
205     this._queue = (async () => {
206       await this._queue;
207       try {
208         return await func();
209       } catch (e) {
210         this._log.error(e);
211         return false;
212       }
213     })();
214   }
216   promiseCallsComplete() {
217     return this._queue;
218   }
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
224  * observe().
225  */
226 class AsyncObserver extends AsyncQueueCaller {
227   constructor(obj, log) {
228     super(log);
229     this.obj = obj;
230   }
232   observe(subject, topic, data) {
233     this.enqueueCall(() => this.obj.observe(subject, topic, data));
234   }
236   promiseObserversComplete() {
237     return this.promiseCallsComplete();
238   }
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.
245  */
246 class Watchdog {
247   constructor() {
248     this.controller = new AbortController();
249     this.timer = new Timer();
251     /**
252      * The reason for signaling an abort. `null` if not signaled,
253      * `"timeout"` if the watchdog timer fired, or `"shutdown"` if the app is
254      * is quitting.
255      *
256      * @type {String?}
257      */
258     this.abortReason = null;
259   }
261   /**
262    * Returns the abort signal for this watchdog. This can be passed to APIs
263    * that take a signal for cancellation, like `SyncedBookmarksMirror::apply`
264    * or `fetch`.
265    *
266    * @type {AbortSignal}
267    */
268   get signal() {
269     return this.controller.signal;
270   }
272   /**
273    * Starts the watchdog timer, and listens for the app quitting.
274    *
275    * @param {Number} delay
276    *                 The time to wait before signaling the operation to abort.
277    */
278   start(delay) {
279     if (!this.signal.aborted) {
280       Services.obs.addObserver(this, "quit-application");
281       this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
282     }
283   }
285   /**
286    * Stops the watchdog timer and removes any listeners. This should be called
287    * after the operation finishes.
288    */
289   stop() {
290     if (!this.signal.aborted) {
291       Services.obs.removeObserver(this, "quit-application");
292       this.timer.cancel();
293     }
294   }
296   observe(subject, topic, data) {
297     if (topic == "timer-callback") {
298       this.abortReason = "timeout";
299     } else if (topic == "quit-application") {
300       this.abortReason = "shutdown";
301     }
302     this.stop();
303     this.controller.abort();
304   }