tor-browser

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

Sync.sys.mjs (13880B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     11  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     12 
     13  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     14  Log: "chrome://remote/content/shared/Log.sys.mjs",
     15 });
     16 
     17 const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
     18 
     19 const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
     20 
     21 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     22  lazy.Log.get(lazy.Log.TYPES.REMOTE_AGENT)
     23 );
     24 
     25 /**
     26 * Throttle until the `window` has performed an animation frame.
     27 *
     28 * The animation frame is requested after the main thread has processed
     29 * all the already queued-up runnables.
     30 *
     31 * @param {ChromeWindow} win
     32 *     Window to request the animation frame from.
     33 * @param {object=} options
     34 * @param {number=} options.timeout
     35 *     Timeout duration in milliseconds.
     36 *     This copes with navigating away from hidden iframes: if
     37 *     fragmentNavigated happens before their animation finishes, this would
     38 *     never resolve otherwise. By default 1500 ms in an optimised build and
     39 *     4500 ms in debug builds. Specify null to disable the timeout.
     40 *
     41 * @returns {Promise}
     42 *
     43 * @throws {TypeError}
     44 * @throws {RangeError}
     45 */
     46 export function AnimationFramePromise(win, options = {}) {
     47  const { timeout = PROMISE_TIMEOUT } = options;
     48 
     49  if (timeout !== null) {
     50    if (typeof timeout != "number") {
     51      throw new TypeError("timeout must be a number or null");
     52    }
     53 
     54    if (!Number.isInteger(timeout) || timeout < 0) {
     55      throw new RangeError("timeout must be a non-negative integer");
     56    }
     57  }
     58 
     59  const animationFramePromise = new Promise(resolve => {
     60    executeSoon(() => {
     61      win.requestAnimationFrame(resolve);
     62    });
     63  });
     64 
     65  const promises = [
     66    animationFramePromise,
     67    new EventPromise(win, "pagehide"), // window closed or moved to BFCache
     68  ];
     69 
     70  let timer;
     71  if (timeout != null) {
     72    promises.push(
     73      new Promise(resolve => {
     74        timer = lazy.setTimeout(() => {
     75          lazy.logger.warn("Timed out waiting for animation frame");
     76          resolve();
     77        }, timeout);
     78      })
     79    );
     80  }
     81 
     82  return Promise.race(promises).then(() => lazy.clearTimeout(timer));
     83 }
     84 
     85 /**
     86 * Create a helper object to defer a promise.
     87 *
     88 * @returns {object}
     89 *     An object that returns the following properties:
     90 *       - fulfilled Flag that indicates that the promise got resolved
     91 *       - pending Flag that indicates a not yet fulfilled/rejected promise
     92 *       - promise The actual promise
     93 *       - reject Callback to reject the promise
     94 *       - rejected Flag that indicates that the promise got rejected
     95 *       - resolve Callback to resolve the promise
     96 */
     97 export function Deferred() {
     98  const deferred = {};
     99 
    100  deferred.promise = new Promise((resolve, reject) => {
    101    deferred.fulfilled = false;
    102    deferred.pending = true;
    103    deferred.rejected = false;
    104 
    105    deferred.resolve = (...args) => {
    106      deferred.fulfilled = true;
    107      deferred.pending = false;
    108      resolve(...args);
    109    };
    110 
    111    deferred.reject = (...args) => {
    112      deferred.pending = false;
    113      deferred.rejected = true;
    114      reject(...args);
    115    };
    116  });
    117 
    118  return deferred;
    119 }
    120 
    121 /**
    122 * Wait for an event to be fired on a specified element.
    123 *
    124 * The returned promise is guaranteed to not resolve before the
    125 * next event tick after the event listener is called, so that all
    126 * other event listeners for the element are executed before the
    127 * handler is executed.  For example:
    128 *
    129 *     const promise = new EventPromise(element, "myEvent");
    130 *     // same event tick here
    131 *     await promise;
    132 *     // next event tick here
    133 *
    134 * @param {Element} subject
    135 *     The element that should receive the event.
    136 * @param {string} eventName
    137 *     Case-sensitive string representing the event name to listen for.
    138 * @param {object=} options
    139 * @param {boolean=} options.capture
    140 *     Indicates the event will be dispatched to this subject,
    141 *     before it bubbles down to any EventTarget beneath it in the
    142 *     DOM tree. Defaults to false.
    143 * @param {Function=} options.checkFn
    144 *     Called with the Event object as argument, should return true if the
    145 *     event is the expected one, or false if it should be ignored and
    146 *     listening should continue. If not specified, the first event with
    147 *     the specified name resolves the returned promise. Defaults to null.
    148 * @param {number=} options.timeout
    149 *     Timeout duration in milliseconds, if provided.
    150 *     If specified, then the returned promise will be rejected with
    151 *     TimeoutError, if not already resolved, after this duration has elapsed.
    152 *     If not specified, then no timeout is used. Defaults to null.
    153 * @param {boolean=} options.mozSystemGroup
    154 *     Determines whether to add listener to the system group. Defaults to
    155 *     false.
    156 * @param {boolean=} options.wantUntrusted
    157 *     Receive synthetic events dispatched by web content. Defaults to false.
    158 *
    159 * @returns {Promise<Event>}
    160 *     Either fulfilled with the first described event, satisfying
    161 *     options.checkFn if specified, or rejected with TimeoutError after
    162 *     options.timeout milliseconds if specified.
    163 *
    164 * @throws {TypeError}
    165 * @throws {RangeError}
    166 */
    167 export function EventPromise(subject, eventName, options = {}) {
    168  const {
    169    capture = false,
    170    checkFn = null,
    171    timeout = null,
    172    mozSystemGroup = false,
    173    wantUntrusted = false,
    174  } = options;
    175  if (
    176    !subject ||
    177    !("addEventListener" in subject) ||
    178    typeof eventName != "string" ||
    179    typeof capture != "boolean" ||
    180    (checkFn && typeof checkFn != "function") ||
    181    (timeout !== null && typeof timeout != "number") ||
    182    typeof mozSystemGroup != "boolean" ||
    183    typeof wantUntrusted != "boolean"
    184  ) {
    185    throw new TypeError();
    186  }
    187  if (timeout < 0) {
    188    throw new RangeError();
    189  }
    190 
    191  return new Promise((resolve, reject) => {
    192    let timer;
    193 
    194    function cleanUp() {
    195      subject.removeEventListener(eventName, listener, capture);
    196      timer?.cancel();
    197    }
    198 
    199    function listener(event) {
    200      lazy.logger.trace(`Received DOM event ${event.type} for ${event.target}`);
    201      try {
    202        if (checkFn && !checkFn(event)) {
    203          return;
    204        }
    205      } catch (e) {
    206        // Treat an exception in the callback as a falsy value
    207        lazy.logger.warn(`Event check failed: ${e.message}`);
    208      }
    209 
    210      cleanUp();
    211      executeSoon(() => resolve(event));
    212    }
    213 
    214    subject.addEventListener(eventName, listener, {
    215      capture,
    216      mozSystemGroup,
    217      wantUntrusted,
    218    });
    219 
    220    if (timeout !== null) {
    221      timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    222      timer.init(
    223        () => {
    224          cleanUp();
    225          reject(
    226            new lazy.error.TimeoutError(
    227              `EventPromise timed out after ${timeout} ms`
    228            )
    229          );
    230        },
    231        timeout,
    232        TYPE_ONE_SHOT
    233      );
    234    }
    235  });
    236 }
    237 
    238 /**
    239 * Wait for the next tick in the event loop to execute a callback.
    240 *
    241 * @param {Function} fn
    242 *     Function to be executed.
    243 */
    244 export function executeSoon(fn) {
    245  if (typeof fn != "function") {
    246    throw new TypeError();
    247  }
    248 
    249  Services.tm.dispatchToMainThread(fn);
    250 }
    251 
    252 /**
    253 * Runs a Promise-like function off the main thread until it is resolved
    254 * through ``resolve`` or ``rejected`` callbacks.  The function is
    255 * guaranteed to be run at least once, regardless of the timeout.
    256 *
    257 * The ``func`` is evaluated every ``interval`` for as long as its
    258 * runtime duration does not exceed ``interval``.  Evaluations occur
    259 * sequentially, meaning that evaluations of ``func`` are queued if
    260 * the runtime evaluation duration of ``func`` is greater than ``interval``.
    261 *
    262 * ``func`` is given two arguments, ``resolve`` and ``reject``,
    263 * of which one must be called for the evaluation to complete.
    264 * Calling ``resolve`` with an argument indicates that the expected
    265 * wait condition was met and will return the passed value to the
    266 * caller.  Conversely, calling ``reject`` will evaluate ``func``
    267 * again until the ``timeout`` duration has elapsed or ``func`` throws.
    268 * The passed value to ``reject`` will also be returned to the caller
    269 * once the wait has expired.
    270 *
    271 * Usage::
    272 *
    273 *     let els = new PollPromise((resolve, reject) => {
    274 *       let res = document.querySelectorAll("p");
    275 *       if (res.length > 0) {
    276 *         resolve(Array.from(res));
    277 *       } else {
    278 *         reject([]);
    279 *       }
    280 *     }, {timeout: 1000});
    281 *
    282 * @param {Condition} func
    283 *     Function to run off the main thread.
    284 * @param {object=} options
    285 * @param {string=} options.errorMessage
    286 *     Message to use to send a warning if ``timeout`` is over.
    287 *     Defaults to `PollPromise timed out`.
    288 * @param {number=} options.timeout
    289 *     Desired timeout if wanted.  If 0 or less than the runtime evaluation
    290 *     time of ``func``, ``func`` is guaranteed to run at least once.
    291 *     Defaults to using no timeout.
    292 * @param {number=} options.interval
    293 *     Duration between each poll of ``func`` in milliseconds.
    294 *     Defaults to 10 milliseconds.
    295 *
    296 * @returns {Promise.<*>}
    297 *     Yields the value passed to ``func``'s
    298 *     ``resolve`` or ``reject`` callbacks.
    299 *
    300 * @throws {*}
    301 *     If ``func`` throws, its error is propagated.
    302 * @throws {TypeError}
    303 *     If `timeout` or `interval`` are not numbers.
    304 * @throws {RangeError}
    305 *     If `timeout` or `interval` are not unsigned integers.
    306 */
    307 export function PollPromise(func, options = {}) {
    308  const {
    309    errorMessage = "PollPromise timed out",
    310    interval = 10,
    311    timeout = null,
    312  } = options;
    313  const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    314  let didTimeOut = false;
    315 
    316  if (typeof func != "function") {
    317    throw new TypeError();
    318  }
    319  if (timeout != null && typeof timeout != "number") {
    320    throw new TypeError();
    321  }
    322  if (typeof interval != "number") {
    323    throw new TypeError();
    324  }
    325  if (
    326    (timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
    327    !Number.isInteger(interval) ||
    328    interval < 0
    329  ) {
    330    throw new RangeError();
    331  }
    332 
    333  return new Promise((resolve, reject) => {
    334    let start, end;
    335 
    336    if (Number.isInteger(timeout)) {
    337      start = new Date().getTime();
    338      end = start + timeout;
    339    }
    340 
    341    let evalFn = () => {
    342      new Promise(func)
    343        .then(resolve, rejected => {
    344          if (typeof rejected != "undefined") {
    345            throw rejected;
    346          }
    347 
    348          // return if there is a timeout and set to 0,
    349          // allowing |func| to be evaluated at least once
    350          if (
    351            typeof end != "undefined" &&
    352            (start == end || new Date().getTime() >= end)
    353          ) {
    354            didTimeOut = true;
    355            resolve(rejected);
    356          }
    357        })
    358        .catch(reject);
    359    };
    360 
    361    // the repeating slack timer waits |interval|
    362    // before invoking |evalFn|
    363    evalFn();
    364 
    365    timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
    366  }).then(
    367    res => {
    368      if (didTimeOut) {
    369        lazy.logger.warn(`${errorMessage} after ${timeout} ms`);
    370      }
    371      timer.cancel();
    372      return res;
    373    },
    374    err => {
    375      timer.cancel();
    376      throw err;
    377    }
    378  );
    379 }
    380 
    381 /**
    382 * Represents the timed, eventual completion (or failure) of an
    383 * asynchronous operation, and its resulting value.
    384 *
    385 * In contrast to a regular Promise, it times out after ``timeout``.
    386 *
    387 * @param {Function} fn
    388 *     Function to run, which will have its ``reject``
    389 *     callback invoked after the ``timeout`` duration is reached.
    390 *     It is given two callbacks: ``resolve(value)`` and
    391 *     ``reject(error)``.
    392 * @param {object=} options
    393 * @param {string} options.errorMessage
    394 *     Message to use for the thrown error.
    395 * @param {number=} options.timeout
    396 *     ``condition``'s ``reject`` callback will be called
    397 *     after this timeout, given in milliseconds.
    398 *     By default 1500 ms in an optimised build and 4500 ms in
    399 *     debug builds.
    400 * @param {Error=} options.throws
    401 *     When the ``timeout`` is hit, this error class will be
    402 *     thrown.  If it is null, no error is thrown and the promise is
    403 *     instead resolved on timeout with a TimeoutError.
    404 *
    405 * @returns {Promise.<*>}
    406 *     Timed promise.
    407 *
    408 * @throws {TypeError}
    409 *     If `timeout` is not a number.
    410 * @throws {RangeError}
    411 *     If `timeout` is not an unsigned integer.
    412 */
    413 export function TimedPromise(fn, options = {}) {
    414  const {
    415    errorMessage = "TimedPromise timed out",
    416    timeout = PROMISE_TIMEOUT,
    417    throws = lazy.error.TimeoutError,
    418  } = options;
    419 
    420  const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    421 
    422  if (typeof fn != "function") {
    423    throw new TypeError();
    424  }
    425  if (typeof timeout != "number") {
    426    throw new TypeError();
    427  }
    428  if (!Number.isInteger(timeout) || timeout < 0) {
    429    throw new RangeError();
    430  }
    431 
    432  return new Promise((resolve, reject) => {
    433    let trace;
    434 
    435    // Reject only if |throws| is given.  Otherwise it is assumed that
    436    // the user is OK with the promise timing out.
    437    let bail = () => {
    438      const message = `${errorMessage} after ${timeout} ms`;
    439      if (throws !== null) {
    440        let err = new throws(message);
    441        reject(err);
    442      } else {
    443        lazy.logger.warn(message, trace);
    444        resolve();
    445      }
    446    };
    447 
    448    trace = lazy.error.stack();
    449    timer.initWithCallback({ notify: bail }, timeout, TYPE_ONE_SHOT);
    450 
    451    try {
    452      fn(resolve, reject);
    453    } catch (e) {
    454      reject(e);
    455    }
    456  }).then(
    457    res => {
    458      timer.cancel();
    459      return res;
    460    },
    461    err => {
    462      timer.cancel();
    463      throw err;
    464    }
    465  );
    466 }