browser/components/sessionstore/content/content-sessionStore.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/sessionstore/content/content-sessionStore.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,725 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +function debug(msg) {
    1.11 +  Services.console.logStringMessage("SessionStoreContent: " + msg);
    1.12 +}
    1.13 +
    1.14 +let Cu = Components.utils;
    1.15 +let Cc = Components.classes;
    1.16 +let Ci = Components.interfaces;
    1.17 +let Cr = Components.results;
    1.18 +
    1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    1.20 +Cu.import("resource://gre/modules/Timer.jsm", this);
    1.21 +
    1.22 +XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
    1.23 +  "resource:///modules/sessionstore/DocShellCapabilities.jsm");
    1.24 +XPCOMUtils.defineLazyModuleGetter(this, "FormData",
    1.25 +  "resource://gre/modules/FormData.jsm");
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
    1.27 +  "resource:///modules/sessionstore/PageStyle.jsm");
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
    1.29 +  "resource://gre/modules/ScrollPosition.jsm");
    1.30 +XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
    1.31 +  "resource:///modules/sessionstore/SessionHistory.jsm");
    1.32 +XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
    1.33 +  "resource:///modules/sessionstore/SessionStorage.jsm");
    1.34 +
    1.35 +Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
    1.36 +let gFrameTree = new FrameTree(this);
    1.37 +
    1.38 +Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
    1.39 +XPCOMUtils.defineLazyGetter(this, 'gContentRestore',
    1.40 +                            () => { return new ContentRestore(this) });
    1.41 +
    1.42 +/**
    1.43 + * Returns a lazy function that will evaluate the given
    1.44 + * function |fn| only once and cache its return value.
    1.45 + */
    1.46 +function createLazy(fn) {
    1.47 +  let cached = false;
    1.48 +  let cachedValue = null;
    1.49 +
    1.50 +  return function lazy() {
    1.51 +    if (!cached) {
    1.52 +      cachedValue = fn();
    1.53 +      cached = true;
    1.54 +    }
    1.55 +
    1.56 +    return cachedValue;
    1.57 +  };
    1.58 +}
    1.59 +
    1.60 +/**
    1.61 + * Determines whether the given storage event was triggered by changes
    1.62 + * to the sessionStorage object and not the local or globalStorage.
    1.63 + */
    1.64 +function isSessionStorageEvent(event) {
    1.65 +  try {
    1.66 +    return event.storageArea == content.sessionStorage;
    1.67 +  } catch (ex if ex instanceof Ci.nsIException && ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
    1.68 +    // This page does not have a DOMSessionStorage
    1.69 +    // (this is typically the case for about: pages)
    1.70 +    return false;
    1.71 +  }
    1.72 +}
    1.73 +
    1.74 +/**
    1.75 + * Listens for and handles content events that we need for the
    1.76 + * session store service to be notified of state changes in content.
    1.77 + */
    1.78 +let EventListener = {
    1.79 +
    1.80 +  init: function () {
    1.81 +    addEventListener("load", this, true);
    1.82 +  },
    1.83 +
    1.84 +  handleEvent: function (event) {
    1.85 +    // Ignore load events from subframes.
    1.86 +    if (event.target != content.document) {
    1.87 +      return;
    1.88 +    }
    1.89 +
    1.90 +    // If we're in the process of restoring, this load may signal
    1.91 +    // the end of the restoration.
    1.92 +    let epoch = gContentRestore.getRestoreEpoch();
    1.93 +    if (!epoch) {
    1.94 +      return;
    1.95 +    }
    1.96 +
    1.97 +    // Restore the form data and scroll position.
    1.98 +    gContentRestore.restoreDocument();
    1.99 +
   1.100 +    // Ask SessionStore.jsm to trigger SSTabRestored.
   1.101 +    sendAsyncMessage("SessionStore:restoreDocumentComplete", {epoch: epoch});
   1.102 +  }
   1.103 +};
   1.104 +
   1.105 +/**
   1.106 + * Listens for and handles messages sent by the session store service.
   1.107 + */
   1.108 +let MessageListener = {
   1.109 +
   1.110 +  MESSAGES: [
   1.111 +    "SessionStore:restoreHistory",
   1.112 +    "SessionStore:restoreTabContent",
   1.113 +    "SessionStore:resetRestore",
   1.114 +  ],
   1.115 +
   1.116 +  init: function () {
   1.117 +    this.MESSAGES.forEach(m => addMessageListener(m, this));
   1.118 +  },
   1.119 +
   1.120 +  receiveMessage: function ({name, data}) {
   1.121 +    switch (name) {
   1.122 +      case "SessionStore:restoreHistory":
   1.123 +        let reloadCallback = () => {
   1.124 +          // Inform SessionStore.jsm about the reload. It will send
   1.125 +          // restoreTabContent in response.
   1.126 +          sendAsyncMessage("SessionStore:reloadPendingTab", {epoch: data.epoch});
   1.127 +        };
   1.128 +        gContentRestore.restoreHistory(data.epoch, data.tabData, reloadCallback);
   1.129 +
   1.130 +        // When restoreHistory finishes, we send a synchronous message to
   1.131 +        // SessionStore.jsm so that it can run SSTabRestoring. Users of
   1.132 +        // SSTabRestoring seem to get confused if chrome and content are out of
   1.133 +        // sync about the state of the restore (particularly regarding
   1.134 +        // docShell.currentURI). Using a synchronous message is the easiest way
   1.135 +        // to temporarily synchronize them.
   1.136 +        sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch: data.epoch});
   1.137 +        break;
   1.138 +      case "SessionStore:restoreTabContent":
   1.139 +        let epoch = gContentRestore.getRestoreEpoch();
   1.140 +        let finishCallback = () => {
   1.141 +          // Tell SessionStore.jsm that it may want to restore some more tabs,
   1.142 +          // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
   1.143 +          sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch: epoch});
   1.144 +        };
   1.145 +
   1.146 +        // We need to pass the value of didStartLoad back to SessionStore.jsm.
   1.147 +        let didStartLoad = gContentRestore.restoreTabContent(finishCallback);
   1.148 +
   1.149 +        sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch: epoch});
   1.150 +
   1.151 +        if (!didStartLoad) {
   1.152 +          // Pretend that the load succeeded so that event handlers fire correctly.
   1.153 +          sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch: epoch});
   1.154 +          sendAsyncMessage("SessionStore:restoreDocumentComplete", {epoch: epoch});
   1.155 +        }
   1.156 +        break;
   1.157 +      case "SessionStore:resetRestore":
   1.158 +        gContentRestore.resetRestore();
   1.159 +        break;
   1.160 +      default:
   1.161 +        debug("received unknown message '" + name + "'");
   1.162 +        break;
   1.163 +    }
   1.164 +  }
   1.165 +};
   1.166 +
   1.167 +/**
   1.168 + * On initialization, this handler gets sent to the parent process as a CPOW.
   1.169 + * The parent will use it only to flush pending data from the frame script
   1.170 + * when needed, i.e. when closing a tab, closing a window, shutting down, etc.
   1.171 + *
   1.172 + * This will hopefully not be needed in the future once we have async APIs for
   1.173 + * closing windows and tabs.
   1.174 + */
   1.175 +let SyncHandler = {
   1.176 +  init: function () {
   1.177 +    // Send this object as a CPOW to chrome. In single-process mode,
   1.178 +    // the synchronous send ensures that the handler object is
   1.179 +    // available in SessionStore.jsm immediately upon loading
   1.180 +    // content-sessionStore.js.
   1.181 +    sendSyncMessage("SessionStore:setupSyncHandler", {}, {handler: this});
   1.182 +  },
   1.183 +
   1.184 +  /**
   1.185 +   * This function is used to make the tab process flush all data that
   1.186 +   * hasn't been sent to the parent process, yet.
   1.187 +   *
   1.188 +   * @param id (int)
   1.189 +   *        A unique id that represents the last message received by the chrome
   1.190 +   *        process before flushing. We will use this to determine data that
   1.191 +   *        would be lost when data has been sent asynchronously shortly
   1.192 +   *        before flushing synchronously.
   1.193 +   */
   1.194 +  flush: function (id) {
   1.195 +    MessageQueue.flush(id);
   1.196 +  },
   1.197 +
   1.198 +  /**
   1.199 +   * DO NOT USE - DEBUGGING / TESTING ONLY
   1.200 +   *
   1.201 +   * This function is used to simulate certain situations where race conditions
   1.202 +   * can occur by sending data shortly before flushing synchronously.
   1.203 +   */
   1.204 +  flushAsync: function () {
   1.205 +    MessageQueue.flushAsync();
   1.206 +  }
   1.207 +};
   1.208 +
   1.209 +/**
   1.210 + * Listens for changes to the session history. Whenever the user navigates
   1.211 + * we will collect URLs and everything belonging to session history.
   1.212 + *
   1.213 + * Causes a SessionStore:update message to be sent that contains the current
   1.214 + * session history.
   1.215 + *
   1.216 + * Example:
   1.217 + *   {entries: [{url: "about:mozilla", ...}, ...], index: 1}
   1.218 + */
   1.219 +let SessionHistoryListener = {
   1.220 +  init: function () {
   1.221 +    // The frame tree observer is needed to handle navigating away from
   1.222 +    // an about page. Currently nsISHistoryListener does not have
   1.223 +    // OnHistoryNewEntry() called for about pages because the history entry is
   1.224 +    // modified to point at the new page. Once Bug 981900 lands the frame tree
   1.225 +    // observer can be removed.
   1.226 +    gFrameTree.addObserver(this);
   1.227 +
   1.228 +    // By adding the SHistoryListener immediately, we will unfortunately be
   1.229 +    // notified of every history entry as the tab is restored. We don't bother
   1.230 +    // waiting to add the listener later because these notifications are cheap.
   1.231 +    // We will likely only collect once since we are batching collection on
   1.232 +    // a delay.
   1.233 +    docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.
   1.234 +      addSHistoryListener(this);
   1.235 +
   1.236 +    // Collect data if we start with a non-empty shistory.
   1.237 +    if (!SessionHistory.isEmpty(docShell)) {
   1.238 +      this.collect();
   1.239 +    }
   1.240 +  },
   1.241 +
   1.242 +  uninit: function () {
   1.243 +    let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
   1.244 +    if (sessionHistory) {
   1.245 +      sessionHistory.removeSHistoryListener(this);
   1.246 +    }
   1.247 +  },
   1.248 +
   1.249 +  collect: function () {
   1.250 +    if (docShell) {
   1.251 +      MessageQueue.push("history", () => SessionHistory.collect(docShell));
   1.252 +    }
   1.253 +  },
   1.254 +
   1.255 +  onFrameTreeCollected: function () {
   1.256 +    this.collect();
   1.257 +  },
   1.258 +
   1.259 +  onFrameTreeReset: function () {
   1.260 +    this.collect();
   1.261 +  },
   1.262 +
   1.263 +  OnHistoryNewEntry: function (newURI) {
   1.264 +    this.collect();
   1.265 +  },
   1.266 +
   1.267 +  OnHistoryGoBack: function (backURI) {
   1.268 +    this.collect();
   1.269 +    return true;
   1.270 +  },
   1.271 +
   1.272 +  OnHistoryGoForward: function (forwardURI) {
   1.273 +    this.collect();
   1.274 +    return true;
   1.275 +  },
   1.276 +
   1.277 +  OnHistoryGotoIndex: function (index, gotoURI) {
   1.278 +    this.collect();
   1.279 +    return true;
   1.280 +  },
   1.281 +
   1.282 +  OnHistoryPurge: function (numEntries) {
   1.283 +    this.collect();
   1.284 +    return true;
   1.285 +  },
   1.286 +
   1.287 +  OnHistoryReload: function (reloadURI, reloadFlags) {
   1.288 +    this.collect();
   1.289 +    return true;
   1.290 +  },
   1.291 +
   1.292 +  OnHistoryReplaceEntry: function (index) {
   1.293 +    this.collect();
   1.294 +  },
   1.295 +
   1.296 +  QueryInterface: XPCOMUtils.generateQI([
   1.297 +    Ci.nsISHistoryListener,
   1.298 +    Ci.nsISupportsWeakReference
   1.299 +  ])
   1.300 +};
   1.301 +
   1.302 +/**
   1.303 + * Listens for scroll position changes. Whenever the user scrolls the top-most
   1.304 + * frame we update the scroll position and will restore it when requested.
   1.305 + *
   1.306 + * Causes a SessionStore:update message to be sent that contains the current
   1.307 + * scroll positions as a tree of strings. If no frame of the whole frame tree
   1.308 + * is scrolled this will return null so that we don't tack a property onto
   1.309 + * the tabData object in the parent process.
   1.310 + *
   1.311 + * Example:
   1.312 + *   {scroll: "100,100", children: [null, null, {scroll: "200,200"}]}
   1.313 + */
   1.314 +let ScrollPositionListener = {
   1.315 +  init: function () {
   1.316 +    addEventListener("scroll", this);
   1.317 +    gFrameTree.addObserver(this);
   1.318 +  },
   1.319 +
   1.320 +  handleEvent: function (event) {
   1.321 +    let frame = event.target && event.target.defaultView;
   1.322 +
   1.323 +    // Don't collect scroll data for frames created at or after the load event
   1.324 +    // as SessionStore can't restore scroll data for those.
   1.325 +    if (frame && gFrameTree.contains(frame)) {
   1.326 +      MessageQueue.push("scroll", () => this.collect());
   1.327 +    }
   1.328 +  },
   1.329 +
   1.330 +  onFrameTreeCollected: function () {
   1.331 +    MessageQueue.push("scroll", () => this.collect());
   1.332 +  },
   1.333 +
   1.334 +  onFrameTreeReset: function () {
   1.335 +    MessageQueue.push("scroll", () => null);
   1.336 +  },
   1.337 +
   1.338 +  collect: function () {
   1.339 +    return gFrameTree.map(ScrollPosition.collect);
   1.340 +  }
   1.341 +};
   1.342 +
   1.343 +/**
   1.344 + * Listens for changes to input elements. Whenever the value of an input
   1.345 + * element changes we will re-collect data for the current frame tree and send
   1.346 + * a message to the parent process.
   1.347 + *
   1.348 + * Causes a SessionStore:update message to be sent that contains the form data
   1.349 + * for all reachable frames.
   1.350 + *
   1.351 + * Example:
   1.352 + *   {
   1.353 + *     formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}},
   1.354 + *     children: [
   1.355 + *       null,
   1.356 + *       {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}}
   1.357 + *     ]
   1.358 + *   }
   1.359 + */
   1.360 +let FormDataListener = {
   1.361 +  init: function () {
   1.362 +    addEventListener("input", this, true);
   1.363 +    addEventListener("change", this, true);
   1.364 +    gFrameTree.addObserver(this);
   1.365 +  },
   1.366 +
   1.367 +  handleEvent: function (event) {
   1.368 +    let frame = event.target &&
   1.369 +                event.target.ownerDocument &&
   1.370 +                event.target.ownerDocument.defaultView;
   1.371 +
   1.372 +    // Don't collect form data for frames created at or after the load event
   1.373 +    // as SessionStore can't restore form data for those.
   1.374 +    if (frame && gFrameTree.contains(frame)) {
   1.375 +      MessageQueue.push("formdata", () => this.collect());
   1.376 +    }
   1.377 +  },
   1.378 +
   1.379 +  onFrameTreeReset: function () {
   1.380 +    MessageQueue.push("formdata", () => null);
   1.381 +  },
   1.382 +
   1.383 +  collect: function () {
   1.384 +    return gFrameTree.map(FormData.collect);
   1.385 +  }
   1.386 +};
   1.387 +
   1.388 +/**
   1.389 + * Listens for changes to the page style. Whenever a different page style is
   1.390 + * selected or author styles are enabled/disabled we send a message with the
   1.391 + * currently applied style to the chrome process.
   1.392 + *
   1.393 + * Causes a SessionStore:update message to be sent that contains the currently
   1.394 + * selected pageStyle for all reachable frames.
   1.395 + *
   1.396 + * Example:
   1.397 + *   {pageStyle: "Dusk", children: [null, {pageStyle: "Mozilla"}]}
   1.398 + */
   1.399 +let PageStyleListener = {
   1.400 +  init: function () {
   1.401 +    Services.obs.addObserver(this, "author-style-disabled-changed", false);
   1.402 +    Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false);
   1.403 +    gFrameTree.addObserver(this);
   1.404 +  },
   1.405 +
   1.406 +  uninit: function () {
   1.407 +    Services.obs.removeObserver(this, "author-style-disabled-changed");
   1.408 +    Services.obs.removeObserver(this, "style-sheet-applicable-state-changed");
   1.409 +  },
   1.410 +
   1.411 +  observe: function (subject, topic) {
   1.412 +    let frame = subject.defaultView;
   1.413 +
   1.414 +    if (frame && gFrameTree.contains(frame)) {
   1.415 +      MessageQueue.push("pageStyle", () => this.collect());
   1.416 +    }
   1.417 +  },
   1.418 +
   1.419 +  collect: function () {
   1.420 +    return PageStyle.collect(docShell, gFrameTree);
   1.421 +  },
   1.422 +
   1.423 +  onFrameTreeCollected: function () {
   1.424 +    MessageQueue.push("pageStyle", () => this.collect());
   1.425 +  },
   1.426 +
   1.427 +  onFrameTreeReset: function () {
   1.428 +    MessageQueue.push("pageStyle", () => null);
   1.429 +  }
   1.430 +};
   1.431 +
   1.432 +/**
   1.433 + * Listens for changes to docShell capabilities. Whenever a new load is started
   1.434 + * we need to re-check the list of capabilities and send message when it has
   1.435 + * changed.
   1.436 + *
   1.437 + * Causes a SessionStore:update message to be sent that contains the currently
   1.438 + * disabled docShell capabilities (all nsIDocShell.allow* properties set to
   1.439 + * false) as a string - i.e. capability names separate by commas.
   1.440 + */
   1.441 +let DocShellCapabilitiesListener = {
   1.442 +  /**
   1.443 +   * This field is used to compare the last docShell capabilities to the ones
   1.444 +   * that have just been collected. If nothing changed we won't send a message.
   1.445 +   */
   1.446 +  _latestCapabilities: "",
   1.447 +
   1.448 +  init: function () {
   1.449 +    gFrameTree.addObserver(this);
   1.450 +  },
   1.451 +
   1.452 +  /**
   1.453 +   * onFrameTreeReset() is called as soon as we start loading a page.
   1.454 +   */
   1.455 +  onFrameTreeReset: function() {
   1.456 +    // The order of docShell capabilities cannot change while we're running
   1.457 +    // so calling join() without sorting before is totally sufficient.
   1.458 +    let caps = DocShellCapabilities.collect(docShell).join(",");
   1.459 +
   1.460 +    // Send new data only when the capability list changes.
   1.461 +    if (caps != this._latestCapabilities) {
   1.462 +      this._latestCapabilities = caps;
   1.463 +      MessageQueue.push("disallow", () => caps || null);
   1.464 +    }
   1.465 +  }
   1.466 +};
   1.467 +
   1.468 +/**
   1.469 + * Listens for changes to the DOMSessionStorage. Whenever new keys are added,
   1.470 + * existing ones removed or changed, or the storage is cleared we will send a
   1.471 + * message to the parent process containing up-to-date sessionStorage data.
   1.472 + *
   1.473 + * Causes a SessionStore:update message to be sent that contains the current
   1.474 + * DOMSessionStorage contents. The data is a nested object using host names
   1.475 + * as keys and per-host DOMSessionStorage data as values.
   1.476 + */
   1.477 +let SessionStorageListener = {
   1.478 +  init: function () {
   1.479 +    addEventListener("MozStorageChanged", this);
   1.480 +    Services.obs.addObserver(this, "browser:purge-domain-data", false);
   1.481 +    gFrameTree.addObserver(this);
   1.482 +  },
   1.483 +
   1.484 +  uninit: function () {
   1.485 +    Services.obs.removeObserver(this, "browser:purge-domain-data");
   1.486 +  },
   1.487 +
   1.488 +  handleEvent: function (event) {
   1.489 +    // Ignore events triggered by localStorage or globalStorage changes.
   1.490 +    if (gFrameTree.contains(event.target) && isSessionStorageEvent(event)) {
   1.491 +      this.collect();
   1.492 +    }
   1.493 +  },
   1.494 +
   1.495 +  observe: function () {
   1.496 +    // Collect data on the next tick so that any other observer
   1.497 +    // that needs to purge data can do its work first.
   1.498 +    setTimeout(() => this.collect(), 0);
   1.499 +  },
   1.500 +
   1.501 +  collect: function () {
   1.502 +    if (docShell) {
   1.503 +      MessageQueue.push("storage", () => SessionStorage.collect(docShell, gFrameTree));
   1.504 +    }
   1.505 +  },
   1.506 +
   1.507 +  onFrameTreeCollected: function () {
   1.508 +    this.collect();
   1.509 +  },
   1.510 +
   1.511 +  onFrameTreeReset: function () {
   1.512 +    this.collect();
   1.513 +  }
   1.514 +};
   1.515 +
   1.516 +/**
   1.517 + * Listen for changes to the privacy status of the tab.
   1.518 + * By definition, tabs start in non-private mode.
   1.519 + *
   1.520 + * Causes a SessionStore:update message to be sent for
   1.521 + * field "isPrivate". This message contains
   1.522 + *  |true| if the tab is now private
   1.523 + *  |null| if the tab is now public - the field is therefore
   1.524 + *  not saved.
   1.525 + */
   1.526 +let PrivacyListener = {
   1.527 +  init: function() {
   1.528 +    docShell.addWeakPrivacyTransitionObserver(this);
   1.529 +
   1.530 +    // Check that value at startup as it might have
   1.531 +    // been set before the frame script was loaded.
   1.532 +    if (docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) {
   1.533 +      MessageQueue.push("isPrivate", () => true);
   1.534 +    }
   1.535 +  },
   1.536 +
   1.537 +  // Ci.nsIPrivacyTransitionObserver
   1.538 +  privateModeChanged: function(enabled) {
   1.539 +    MessageQueue.push("isPrivate", () => enabled || null);
   1.540 +  },
   1.541 +
   1.542 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivacyTransitionObserver,
   1.543 +                                         Ci.nsISupportsWeakReference])
   1.544 +};
   1.545 +
   1.546 +/**
   1.547 + * A message queue that takes collected data and will take care of sending it
   1.548 + * to the chrome process. It allows flushing using synchronous messages and
   1.549 + * takes care of any race conditions that might occur because of that. Changes
   1.550 + * will be batched if they're pushed in quick succession to avoid a message
   1.551 + * flood.
   1.552 + */
   1.553 +let MessageQueue = {
   1.554 +  /**
   1.555 +   * A unique, monotonically increasing ID used for outgoing messages. This is
   1.556 +   * important to make it possible to reuse tabs and allow sync flushes before
   1.557 +   * data could be destroyed.
   1.558 +   */
   1.559 +  _id: 1,
   1.560 +
   1.561 +  /**
   1.562 +   * A map (string -> lazy fn) holding lazy closures of all queued data
   1.563 +   * collection routines. These functions will return data collected from the
   1.564 +   * docShell.
   1.565 +   */
   1.566 +  _data: new Map(),
   1.567 +
   1.568 +  /**
   1.569 +   * A map holding the |this._id| value for every type of data back when it
   1.570 +   * was pushed onto the queue. We will use those IDs to find the data to send
   1.571 +   * and flush.
   1.572 +   */
   1.573 +  _lastUpdated: new Map(),
   1.574 +
   1.575 +  /**
   1.576 +   * The delay (in ms) used to delay sending changes after data has been
   1.577 +   * invalidated.
   1.578 +   */
   1.579 +  BATCH_DELAY_MS: 1000,
   1.580 +
   1.581 +  /**
   1.582 +   * The current timeout ID, null if there is no queue data. We use timeouts
   1.583 +   * to damp a flood of data changes and send lots of changes as one batch.
   1.584 +   */
   1.585 +  _timeout: null,
   1.586 +
   1.587 +  /**
   1.588 +   * Pushes a given |value| onto the queue. The given |key| represents the type
   1.589 +   * of data that is stored and can override data that has been queued before
   1.590 +   * but has not been sent to the parent process, yet.
   1.591 +   *
   1.592 +   * @param key (string)
   1.593 +   *        A unique identifier specific to the type of data this is passed.
   1.594 +   * @param fn (function)
   1.595 +   *        A function that returns the value that will be sent to the parent
   1.596 +   *        process.
   1.597 +   */
   1.598 +  push: function (key, fn) {
   1.599 +    this._data.set(key, createLazy(fn));
   1.600 +    this._lastUpdated.set(key, this._id);
   1.601 +
   1.602 +    if (!this._timeout) {
   1.603 +      // Wait a little before sending the message to batch multiple changes.
   1.604 +      this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS);
   1.605 +    }
   1.606 +  },
   1.607 +
   1.608 +  /**
   1.609 +   * Sends queued data to the chrome process.
   1.610 +   *
   1.611 +   * @param options (object)
   1.612 +   *        {id: 123} to override the update ID used to accumulate data to send.
   1.613 +   *        {sync: true} to send data to the parent process synchronously.
   1.614 +   */
   1.615 +  send: function (options = {}) {
   1.616 +    // Looks like we have been called off a timeout after the tab has been
   1.617 +    // closed. The docShell is gone now and we can just return here as there
   1.618 +    // is nothing to do.
   1.619 +    if (!docShell) {
   1.620 +      return;
   1.621 +    }
   1.622 +
   1.623 +    if (this._timeout) {
   1.624 +      clearTimeout(this._timeout);
   1.625 +      this._timeout = null;
   1.626 +    }
   1.627 +
   1.628 +    let sync = options && options.sync;
   1.629 +    let startID = (options && options.id) || this._id;
   1.630 +
   1.631 +    // We use sendRpcMessage in the sync case because we may have been called
   1.632 +    // through a CPOW. RPC messages are the only synchronous messages that the
   1.633 +    // child is allowed to send to the parent while it is handling a CPOW
   1.634 +    // request.
   1.635 +    let sendMessage = sync ? sendRpcMessage : sendAsyncMessage;
   1.636 +
   1.637 +    let durationMs = Date.now();
   1.638 +
   1.639 +    let data = {};
   1.640 +    for (let [key, id] of this._lastUpdated) {
   1.641 +      // There is no data for the given key anymore because
   1.642 +      // the parent process already marked it as received.
   1.643 +      if (!this._data.has(key)) {
   1.644 +        continue;
   1.645 +      }
   1.646 +
   1.647 +      if (startID > id) {
   1.648 +        // If the |id| passed by the parent process is higher than the one
   1.649 +        // stored in |_lastUpdated| for the given key we know that the parent
   1.650 +        // received all necessary data and we can remove it from the map.
   1.651 +        this._data.delete(key);
   1.652 +        continue;
   1.653 +      }
   1.654 +
   1.655 +      data[key] = this._data.get(key)();
   1.656 +    }
   1.657 +
   1.658 +    durationMs = Date.now() - durationMs;
   1.659 +    let telemetry = {
   1.660 +      FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS: durationMs
   1.661 +    }
   1.662 +
   1.663 +    // Send all data to the parent process.
   1.664 +    sendMessage("SessionStore:update", {
   1.665 +      id: this._id,
   1.666 +      data: data,
   1.667 +      telemetry: telemetry
   1.668 +    });
   1.669 +
   1.670 +    // Increase our unique message ID.
   1.671 +    this._id++;
   1.672 +  },
   1.673 +
   1.674 +  /**
   1.675 +   * This function is used to make the message queue flush all queue data that
   1.676 +   * hasn't been sent to the parent process, yet.
   1.677 +   *
   1.678 +   * @param id (int)
   1.679 +   *        A unique id that represents the latest message received by the
   1.680 +   *        chrome process. We can use this to determine which messages have not
   1.681 +   *        yet been received because they are still stuck in the event queue.
   1.682 +   */
   1.683 +  flush: function (id) {
   1.684 +    // It's important to always send data, even if there is nothing to flush.
   1.685 +    // The update message will be received by the parent process that can then
   1.686 +    // update its last received update ID to ignore stale messages.
   1.687 +    this.send({id: id + 1, sync: true});
   1.688 +
   1.689 +    this._data.clear();
   1.690 +    this._lastUpdated.clear();
   1.691 +  },
   1.692 +
   1.693 +  /**
   1.694 +   * DO NOT USE - DEBUGGING / TESTING ONLY
   1.695 +   *
   1.696 +   * This function is used to simulate certain situations where race conditions
   1.697 +   * can occur by sending data shortly before flushing synchronously.
   1.698 +   */
   1.699 +  flushAsync: function () {
   1.700 +    if (!Services.prefs.getBoolPref("browser.sessionstore.debug")) {
   1.701 +      throw new Error("flushAsync() must be used for testing, only.");
   1.702 +    }
   1.703 +
   1.704 +    this.send();
   1.705 +  }
   1.706 +};
   1.707 +
   1.708 +EventListener.init();
   1.709 +MessageListener.init();
   1.710 +FormDataListener.init();
   1.711 +SyncHandler.init();
   1.712 +PageStyleListener.init();
   1.713 +SessionHistoryListener.init();
   1.714 +SessionStorageListener.init();
   1.715 +ScrollPositionListener.init();
   1.716 +DocShellCapabilitiesListener.init();
   1.717 +PrivacyListener.init();
   1.718 +
   1.719 +addEventListener("unload", () => {
   1.720 +  // Remove all registered nsIObservers.
   1.721 +  PageStyleListener.uninit();
   1.722 +  SessionStorageListener.uninit();
   1.723 +  SessionHistoryListener.uninit();
   1.724 +
   1.725 +  // We don't need to take care of any gFrameTree observers as the gFrameTree
   1.726 +  // will die with the content script. The same goes for the privacy transition
   1.727 +  // observer that will die with the docShell when the tab is closed.
   1.728 +});

mercurial