tor-browser

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

sync.sys.mjs (12875B)


      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 lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
      9  Log: "chrome://remote/content/shared/Log.sys.mjs",
     10 });
     11 
     12 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     13  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
     14 );
     15 
     16 const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
     17 
     18 /**
     19 * Runs a Promise-like function off the main thread until it is resolved
     20 * through ``resolve`` or ``rejected`` callbacks.  The function is
     21 * guaranteed to be run at least once, regardless of the timeout.
     22 *
     23 * The ``func`` is evaluated every ``interval`` for as long as its
     24 * runtime duration does not exceed ``interval``.  Evaluations occur
     25 * sequentially, meaning that evaluations of ``func`` are queued if
     26 * the runtime evaluation duration of ``func`` is greater than ``interval``.
     27 *
     28 * ``func`` is given two arguments, ``resolve`` and ``reject``,
     29 * of which one must be called for the evaluation to complete.
     30 * Calling ``resolve`` with an argument indicates that the expected
     31 * wait condition was met and will return the passed value to the
     32 * caller.  Conversely, calling ``reject`` will evaluate ``func``
     33 * again until the ``timeout`` duration has elapsed or ``func`` throws.
     34 * The passed value to ``reject`` will also be returned to the caller
     35 * once the wait has expired.
     36 *
     37 * Usage::
     38 *
     39 *     let els = new PollPromise((resolve, reject) => {
     40 *       let res = document.querySelectorAll("p");
     41 *       if (res.length > 0) {
     42 *         resolve(Array.from(res));
     43 *       } else {
     44 *         reject([]);
     45 *       }
     46 *     }, {timeout: 1000});
     47 *
     48 * @param {Condition} func
     49 *     Function to run off the main thread.
     50 * @param {object=} options
     51 * @param {number=} options.timeout
     52 *     Desired timeout if wanted.  If 0 or less than the runtime evaluation
     53 *     time of ``func``, ``func`` is guaranteed to run at least once.
     54 *     Defaults to using no timeout.
     55 * @param {number=} options.interval
     56 *     Duration between each poll of ``func`` in milliseconds.
     57 *     Defaults to 10 milliseconds.
     58 *
     59 * @returns {Promise.<*>}
     60 *     Yields the value passed to ``func``'s
     61 *     ``resolve`` or ``reject`` callbacks.
     62 *
     63 * @throws {*}
     64 *     If ``func`` throws, its error is propagated.
     65 * @throws {TypeError}
     66 *     If `timeout` or `interval`` are not numbers.
     67 * @throws {RangeError}
     68 *     If `timeout` or `interval` are not unsigned integers.
     69 */
     70 export function PollPromise(func, { timeout = null, interval = 10 } = {}) {
     71  const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     72 
     73  if (typeof func != "function") {
     74    throw new TypeError();
     75  }
     76  if (timeout != null && typeof timeout != "number") {
     77    throw new TypeError();
     78  }
     79  if (typeof interval != "number") {
     80    throw new TypeError();
     81  }
     82  if (
     83    (timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
     84    !Number.isInteger(interval) ||
     85    interval < 0
     86  ) {
     87    throw new RangeError();
     88  }
     89 
     90  return new Promise((resolve, reject) => {
     91    let start, end;
     92 
     93    if (Number.isInteger(timeout)) {
     94      start = new Date().getTime();
     95      end = start + timeout;
     96    }
     97 
     98    let evalFn = () => {
     99      new Promise(func)
    100        .then(resolve, rejected => {
    101          if (lazy.error.isError(rejected)) {
    102            throw rejected;
    103          }
    104 
    105          // return if there is a timeout and set to 0,
    106          // allowing |func| to be evaluated at least once
    107          if (
    108            typeof end != "undefined" &&
    109            (start == end || new Date().getTime() >= end)
    110          ) {
    111            resolve(rejected);
    112          }
    113        })
    114        .catch(reject);
    115    };
    116 
    117    // the repeating slack timer waits |interval|
    118    // before invoking |evalFn|
    119    evalFn();
    120 
    121    timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
    122  }).then(
    123    res => {
    124      timer.cancel();
    125      return res;
    126    },
    127    err => {
    128      timer.cancel();
    129      throw err;
    130    }
    131  );
    132 }
    133 
    134 /**
    135 * Pauses for the given duration.
    136 *
    137 * @param {number} timeout
    138 *     Duration to wait before fulfilling promise in milliseconds.
    139 *
    140 * @returns {Promise}
    141 *     Promise that fulfills when the `timeout` is elapsed.
    142 *
    143 * @throws {TypeError}
    144 *     If `timeout` is not a number.
    145 * @throws {RangeError}
    146 *     If `timeout` is not an unsigned integer.
    147 */
    148 export function Sleep(timeout) {
    149  if (typeof timeout != "number") {
    150    throw new TypeError();
    151  }
    152  if (!Number.isInteger(timeout) || timeout < 0) {
    153    throw new RangeError();
    154  }
    155 
    156  return new Promise(resolve => {
    157    const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    158    timer.init(
    159      () => {
    160        // Bug 1663880 - Explicitly cancel the timer for now to prevent a hang
    161        timer.cancel();
    162        resolve();
    163      },
    164      timeout,
    165      TYPE_ONE_SHOT
    166    );
    167  });
    168 }
    169 
    170 /**
    171 * Detects when the specified message manager has been destroyed.
    172 *
    173 * One can observe the removal and detachment of a content browser
    174 * (`<xul:browser>`) or a chrome window by its message manager
    175 * disconnecting.
    176 *
    177 * When a browser is associated with a tab, this is safer than only
    178 * relying on the event `TabClose` which signalises the _intent to_
    179 * remove a tab and consequently would lead to the destruction of
    180 * the content browser and its browser message manager.
    181 *
    182 * When closing a chrome window it is safer than only relying on
    183 * the event 'unload' which signalises the _intent to_ close the
    184 * chrome window and consequently would lead to the destruction of
    185 * the window and its window message manager.
    186 *
    187 * @param {MessageListenerManager} messageManager
    188 *     The message manager to observe for its disconnect state.
    189 *     Use the browser message manager when closing a content browser,
    190 *     and the window message manager when closing a chrome window.
    191 *
    192 * @returns {Promise}
    193 *     A promise that resolves when the message manager has been destroyed.
    194 */
    195 export function MessageManagerDestroyedPromise(messageManager) {
    196  return new Promise(resolve => {
    197    function observe(subject, topic) {
    198      lazy.logger.trace(`Received observer notification ${topic}`);
    199 
    200      if (subject == messageManager) {
    201        Services.obs.removeObserver(this, "message-manager-disconnect");
    202        resolve();
    203      }
    204    }
    205 
    206    Services.obs.addObserver(observe, "message-manager-disconnect");
    207  });
    208 }
    209 
    210 /**
    211 * Wraps a callback function, that, as long as it continues to be
    212 * invoked, will not be triggered.  The given function will be
    213 * called after the timeout duration is reached, after no more
    214 * events fire.
    215 *
    216 * This class implements the {@link EventListener} interface,
    217 * which means it can be used interchangeably with `addEventHandler`.
    218 *
    219 * Debouncing events can be useful when dealing with e.g. DOM events
    220 * that fire at a high rate.  It is generally advisable to avoid
    221 * computationally expensive operations such as DOM modifications
    222 * under these circumstances.
    223 *
    224 * One such high frequenecy event is `resize` that can fire multiple
    225 * times before the window reaches its final dimensions.  In order
    226 * to delay an operation until the window has completed resizing,
    227 * it is possible to use this technique to only invoke the callback
    228 * after the last event has fired::
    229 *
    230 *     let cb = new DebounceCallback(event => {
    231 *       // fires after the final resize event
    232 *       console.log("resize", event);
    233 *     });
    234 *     window.addEventListener("resize", cb);
    235 *
    236 * Note that it is not possible to use this synchronisation primitive
    237 * with `addEventListener(..., {once: true})`.
    238 *
    239 * @param {function(Event): void} fn
    240 *     Callback function that is guaranteed to be invoked once only,
    241 *     after `timeout`.
    242 * @param {number=} [timeout = 250] timeout
    243 *     Time since last event firing, before `fn` will be invoked.
    244 */
    245 export class DebounceCallback {
    246  constructor(fn, { timeout = 250 } = {}) {
    247    if (typeof fn != "function" || typeof timeout != "number") {
    248      throw new TypeError();
    249    }
    250    if (!Number.isInteger(timeout) || timeout < 0) {
    251      throw new RangeError();
    252    }
    253 
    254    this.fn = fn;
    255    this.timeout = timeout;
    256    this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    257  }
    258 
    259  handleEvent(ev) {
    260    this.timer.cancel();
    261    this.timer.initWithCallback(
    262      () => {
    263        this.timer.cancel();
    264        this.fn(ev);
    265      },
    266      this.timeout,
    267      TYPE_ONE_SHOT
    268    );
    269  }
    270 }
    271 
    272 /**
    273 * Wait for a message to be fired from a particular message manager.
    274 *
    275 * This method has been duplicated from BrowserTestUtils.sys.mjs.
    276 *
    277 * @param {nsIMessageManager} messageManager
    278 *     The message manager that should be used.
    279 * @param {string} messageName
    280 *     The message to wait for.
    281 * @param {object=} options
    282 *     Extra options.
    283 * @param {function(Message): boolean=} options.checkFn
    284 *     Called with the ``Message`` object as argument, should return ``true``
    285 *     if the message is the expected one, or ``false`` if it should be
    286 *     ignored and listening should continue. If not specified, the first
    287 *     message with the specified name resolves the returned promise.
    288 *
    289 * @returns {Promise.<object>}
    290 *     Promise which resolves to the data property of the received
    291 *     ``Message``.
    292 */
    293 export function waitForMessage(
    294  messageManager,
    295  messageName,
    296  { checkFn = undefined } = {}
    297 ) {
    298  if (messageManager == null || !("addMessageListener" in messageManager)) {
    299    throw new TypeError();
    300  }
    301  if (typeof messageName != "string") {
    302    throw new TypeError();
    303  }
    304  if (checkFn && typeof checkFn != "function") {
    305    throw new TypeError();
    306  }
    307 
    308  return new Promise(resolve => {
    309    messageManager.addMessageListener(messageName, function onMessage(msg) {
    310      lazy.logger.trace(`Received ${messageName} for ${msg.target}`);
    311      if (checkFn && !checkFn(msg)) {
    312        return;
    313      }
    314      messageManager.removeMessageListener(messageName, onMessage);
    315      resolve(msg.data);
    316    });
    317  });
    318 }
    319 
    320 /**
    321 * Wait for the specified observer topic to be observed.
    322 *
    323 * This method has been duplicated from TestUtils.sys.mjs.
    324 *
    325 * Because this function is intended for testing, any error in checkFn
    326 * will cause the returned promise to be rejected instead of waiting for
    327 * the next notification, since this is probably a bug in the test.
    328 *
    329 * @param {string} topic
    330 *     The topic to observe.
    331 * @param {object=} options
    332 *     Extra options.
    333 * @param {function(string, object): boolean=} options.checkFn
    334 *     Called with ``subject``, and ``data`` as arguments, should return true
    335 *     if the notification is the expected one, or false if it should be
    336 *     ignored and listening should continue. If not specified, the first
    337 *     notification for the specified topic resolves the returned promise.
    338 * @param {number=} options.timeout
    339 *     Timeout duration in milliseconds, if provided.
    340 *     If specified, then the returned promise will be rejected with
    341 *     TimeoutError, if not already resolved, after this duration has elapsed.
    342 *     If not specified, then no timeout is used. Defaults to null.
    343 *
    344 * @returns {Promise.<Array<string, object>>}
    345 *     Promise which is either resolved to an array of ``subject``, and ``data``
    346 *     from the observed notification, or rejected with TimeoutError after
    347 *     options.timeout milliseconds if specified.
    348 *
    349 * @throws {TypeError}
    350 * @throws {RangeError}
    351 */
    352 export function waitForObserverTopic(topic, options = {}) {
    353  const { checkFn = null, timeout = null } = options;
    354  if (typeof topic != "string") {
    355    throw new TypeError();
    356  }
    357  if (
    358    (checkFn != null && typeof checkFn != "function") ||
    359    (timeout !== null && typeof timeout != "number")
    360  ) {
    361    throw new TypeError();
    362  }
    363  if (timeout && (!Number.isInteger(timeout) || timeout < 0)) {
    364    throw new RangeError();
    365  }
    366 
    367  return new Promise((resolve, reject) => {
    368    let timer;
    369 
    370    function cleanUp() {
    371      Services.obs.removeObserver(observer, topic);
    372      timer?.cancel();
    373    }
    374 
    375    function observer(subject, _topic, data) {
    376      lazy.logger.trace(`Received observer notification ${_topic}`);
    377      try {
    378        if (checkFn && !checkFn(subject, data)) {
    379          return;
    380        }
    381        cleanUp();
    382        resolve({ subject, data });
    383      } catch (ex) {
    384        cleanUp();
    385        reject(ex);
    386      }
    387    }
    388 
    389    Services.obs.addObserver(observer, topic);
    390 
    391    if (timeout !== null) {
    392      timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    393      timer.init(
    394        () => {
    395          cleanUp();
    396          reject(
    397            new lazy.error.TimeoutError(
    398              `waitForObserverTopic timed out after ${timeout} ms`
    399            )
    400          );
    401        },
    402        timeout,
    403        TYPE_ONE_SHOT
    404      );
    405    }
    406  });
    407 }