tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }