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 +});