michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: function debug(msg) { michael@0: Services.console.logStringMessage("SessionStoreContent: " + msg); michael@0: } michael@0: michael@0: let Cu = Components.utils; michael@0: let Cc = Components.classes; michael@0: let Ci = Components.interfaces; michael@0: let Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); michael@0: Cu.import("resource://gre/modules/Timer.jsm", this); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities", michael@0: "resource:///modules/sessionstore/DocShellCapabilities.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FormData", michael@0: "resource://gre/modules/FormData.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PageStyle", michael@0: "resource:///modules/sessionstore/PageStyle.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", michael@0: "resource://gre/modules/ScrollPosition.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", michael@0: "resource:///modules/sessionstore/SessionHistory.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", michael@0: "resource:///modules/sessionstore/SessionStorage.jsm"); michael@0: michael@0: Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this); michael@0: let gFrameTree = new FrameTree(this); michael@0: michael@0: Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this); michael@0: XPCOMUtils.defineLazyGetter(this, 'gContentRestore', michael@0: () => { return new ContentRestore(this) }); michael@0: michael@0: /** michael@0: * Returns a lazy function that will evaluate the given michael@0: * function |fn| only once and cache its return value. michael@0: */ michael@0: function createLazy(fn) { michael@0: let cached = false; michael@0: let cachedValue = null; michael@0: michael@0: return function lazy() { michael@0: if (!cached) { michael@0: cachedValue = fn(); michael@0: cached = true; michael@0: } michael@0: michael@0: return cachedValue; michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Determines whether the given storage event was triggered by changes michael@0: * to the sessionStorage object and not the local or globalStorage. michael@0: */ michael@0: function isSessionStorageEvent(event) { michael@0: try { michael@0: return event.storageArea == content.sessionStorage; michael@0: } catch (ex if ex instanceof Ci.nsIException && ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { michael@0: // This page does not have a DOMSessionStorage michael@0: // (this is typically the case for about: pages) michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Listens for and handles content events that we need for the michael@0: * session store service to be notified of state changes in content. michael@0: */ michael@0: let EventListener = { michael@0: michael@0: init: function () { michael@0: addEventListener("load", this, true); michael@0: }, michael@0: michael@0: handleEvent: function (event) { michael@0: // Ignore load events from subframes. michael@0: if (event.target != content.document) { michael@0: return; michael@0: } michael@0: michael@0: // If we're in the process of restoring, this load may signal michael@0: // the end of the restoration. michael@0: let epoch = gContentRestore.getRestoreEpoch(); michael@0: if (!epoch) { michael@0: return; michael@0: } michael@0: michael@0: // Restore the form data and scroll position. michael@0: gContentRestore.restoreDocument(); michael@0: michael@0: // Ask SessionStore.jsm to trigger SSTabRestored. michael@0: sendAsyncMessage("SessionStore:restoreDocumentComplete", {epoch: epoch}); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Listens for and handles messages sent by the session store service. michael@0: */ michael@0: let MessageListener = { michael@0: michael@0: MESSAGES: [ michael@0: "SessionStore:restoreHistory", michael@0: "SessionStore:restoreTabContent", michael@0: "SessionStore:resetRestore", michael@0: ], michael@0: michael@0: init: function () { michael@0: this.MESSAGES.forEach(m => addMessageListener(m, this)); michael@0: }, michael@0: michael@0: receiveMessage: function ({name, data}) { michael@0: switch (name) { michael@0: case "SessionStore:restoreHistory": michael@0: let reloadCallback = () => { michael@0: // Inform SessionStore.jsm about the reload. It will send michael@0: // restoreTabContent in response. michael@0: sendAsyncMessage("SessionStore:reloadPendingTab", {epoch: data.epoch}); michael@0: }; michael@0: gContentRestore.restoreHistory(data.epoch, data.tabData, reloadCallback); michael@0: michael@0: // When restoreHistory finishes, we send a synchronous message to michael@0: // SessionStore.jsm so that it can run SSTabRestoring. Users of michael@0: // SSTabRestoring seem to get confused if chrome and content are out of michael@0: // sync about the state of the restore (particularly regarding michael@0: // docShell.currentURI). Using a synchronous message is the easiest way michael@0: // to temporarily synchronize them. michael@0: sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch: data.epoch}); michael@0: break; michael@0: case "SessionStore:restoreTabContent": michael@0: let epoch = gContentRestore.getRestoreEpoch(); michael@0: let finishCallback = () => { michael@0: // Tell SessionStore.jsm that it may want to restore some more tabs, michael@0: // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. michael@0: sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch: epoch}); michael@0: }; michael@0: michael@0: // We need to pass the value of didStartLoad back to SessionStore.jsm. michael@0: let didStartLoad = gContentRestore.restoreTabContent(finishCallback); michael@0: michael@0: sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch: epoch}); michael@0: michael@0: if (!didStartLoad) { michael@0: // Pretend that the load succeeded so that event handlers fire correctly. michael@0: sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch: epoch}); michael@0: sendAsyncMessage("SessionStore:restoreDocumentComplete", {epoch: epoch}); michael@0: } michael@0: break; michael@0: case "SessionStore:resetRestore": michael@0: gContentRestore.resetRestore(); michael@0: break; michael@0: default: michael@0: debug("received unknown message '" + name + "'"); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * On initialization, this handler gets sent to the parent process as a CPOW. michael@0: * The parent will use it only to flush pending data from the frame script michael@0: * when needed, i.e. when closing a tab, closing a window, shutting down, etc. michael@0: * michael@0: * This will hopefully not be needed in the future once we have async APIs for michael@0: * closing windows and tabs. michael@0: */ michael@0: let SyncHandler = { michael@0: init: function () { michael@0: // Send this object as a CPOW to chrome. In single-process mode, michael@0: // the synchronous send ensures that the handler object is michael@0: // available in SessionStore.jsm immediately upon loading michael@0: // content-sessionStore.js. michael@0: sendSyncMessage("SessionStore:setupSyncHandler", {}, {handler: this}); michael@0: }, michael@0: michael@0: /** michael@0: * This function is used to make the tab process flush all data that michael@0: * hasn't been sent to the parent process, yet. michael@0: * michael@0: * @param id (int) michael@0: * A unique id that represents the last message received by the chrome michael@0: * process before flushing. We will use this to determine data that michael@0: * would be lost when data has been sent asynchronously shortly michael@0: * before flushing synchronously. michael@0: */ michael@0: flush: function (id) { michael@0: MessageQueue.flush(id); michael@0: }, michael@0: michael@0: /** michael@0: * DO NOT USE - DEBUGGING / TESTING ONLY michael@0: * michael@0: * This function is used to simulate certain situations where race conditions michael@0: * can occur by sending data shortly before flushing synchronously. michael@0: */ michael@0: flushAsync: function () { michael@0: MessageQueue.flushAsync(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Listens for changes to the session history. Whenever the user navigates michael@0: * we will collect URLs and everything belonging to session history. michael@0: * michael@0: * Causes a SessionStore:update message to be sent that contains the current michael@0: * session history. michael@0: * michael@0: * Example: michael@0: * {entries: [{url: "about:mozilla", ...}, ...], index: 1} michael@0: */ michael@0: let SessionHistoryListener = { michael@0: init: function () { michael@0: // The frame tree observer is needed to handle navigating away from michael@0: // an about page. Currently nsISHistoryListener does not have michael@0: // OnHistoryNewEntry() called for about pages because the history entry is michael@0: // modified to point at the new page. Once Bug 981900 lands the frame tree michael@0: // observer can be removed. michael@0: gFrameTree.addObserver(this); michael@0: michael@0: // By adding the SHistoryListener immediately, we will unfortunately be michael@0: // notified of every history entry as the tab is restored. We don't bother michael@0: // waiting to add the listener later because these notifications are cheap. michael@0: // We will likely only collect once since we are batching collection on michael@0: // a delay. michael@0: docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory. michael@0: addSHistoryListener(this); michael@0: michael@0: // Collect data if we start with a non-empty shistory. michael@0: if (!SessionHistory.isEmpty(docShell)) { michael@0: this.collect(); michael@0: } michael@0: }, michael@0: michael@0: uninit: function () { michael@0: let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; michael@0: if (sessionHistory) { michael@0: sessionHistory.removeSHistoryListener(this); michael@0: } michael@0: }, michael@0: michael@0: collect: function () { michael@0: if (docShell) { michael@0: MessageQueue.push("history", () => SessionHistory.collect(docShell)); michael@0: } michael@0: }, michael@0: michael@0: onFrameTreeCollected: function () { michael@0: this.collect(); michael@0: }, michael@0: michael@0: onFrameTreeReset: function () { michael@0: this.collect(); michael@0: }, michael@0: michael@0: OnHistoryNewEntry: function (newURI) { michael@0: this.collect(); michael@0: }, michael@0: michael@0: OnHistoryGoBack: function (backURI) { michael@0: this.collect(); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryGoForward: function (forwardURI) { michael@0: this.collect(); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryGotoIndex: function (index, gotoURI) { michael@0: this.collect(); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryPurge: function (numEntries) { michael@0: this.collect(); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryReload: function (reloadURI, reloadFlags) { michael@0: this.collect(); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryReplaceEntry: function (index) { michael@0: this.collect(); michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsISHistoryListener, michael@0: Ci.nsISupportsWeakReference michael@0: ]) michael@0: }; michael@0: michael@0: /** michael@0: * Listens for scroll position changes. Whenever the user scrolls the top-most michael@0: * frame we update the scroll position and will restore it when requested. michael@0: * michael@0: * Causes a SessionStore:update message to be sent that contains the current michael@0: * scroll positions as a tree of strings. If no frame of the whole frame tree michael@0: * is scrolled this will return null so that we don't tack a property onto michael@0: * the tabData object in the parent process. michael@0: * michael@0: * Example: michael@0: * {scroll: "100,100", children: [null, null, {scroll: "200,200"}]} michael@0: */ michael@0: let ScrollPositionListener = { michael@0: init: function () { michael@0: addEventListener("scroll", this); michael@0: gFrameTree.addObserver(this); michael@0: }, michael@0: michael@0: handleEvent: function (event) { michael@0: let frame = event.target && event.target.defaultView; michael@0: michael@0: // Don't collect scroll data for frames created at or after the load event michael@0: // as SessionStore can't restore scroll data for those. michael@0: if (frame && gFrameTree.contains(frame)) { michael@0: MessageQueue.push("scroll", () => this.collect()); michael@0: } michael@0: }, michael@0: michael@0: onFrameTreeCollected: function () { michael@0: MessageQueue.push("scroll", () => this.collect()); michael@0: }, michael@0: michael@0: onFrameTreeReset: function () { michael@0: MessageQueue.push("scroll", () => null); michael@0: }, michael@0: michael@0: collect: function () { michael@0: return gFrameTree.map(ScrollPosition.collect); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Listens for changes to input elements. Whenever the value of an input michael@0: * element changes we will re-collect data for the current frame tree and send michael@0: * a message to the parent process. michael@0: * michael@0: * Causes a SessionStore:update message to be sent that contains the form data michael@0: * for all reachable frames. michael@0: * michael@0: * Example: michael@0: * { michael@0: * formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}}, michael@0: * children: [ michael@0: * null, michael@0: * {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}} michael@0: * ] michael@0: * } michael@0: */ michael@0: let FormDataListener = { michael@0: init: function () { michael@0: addEventListener("input", this, true); michael@0: addEventListener("change", this, true); michael@0: gFrameTree.addObserver(this); michael@0: }, michael@0: michael@0: handleEvent: function (event) { michael@0: let frame = event.target && michael@0: event.target.ownerDocument && michael@0: event.target.ownerDocument.defaultView; michael@0: michael@0: // Don't collect form data for frames created at or after the load event michael@0: // as SessionStore can't restore form data for those. michael@0: if (frame && gFrameTree.contains(frame)) { michael@0: MessageQueue.push("formdata", () => this.collect()); michael@0: } michael@0: }, michael@0: michael@0: onFrameTreeReset: function () { michael@0: MessageQueue.push("formdata", () => null); michael@0: }, michael@0: michael@0: collect: function () { michael@0: return gFrameTree.map(FormData.collect); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Listens for changes to the page style. Whenever a different page style is michael@0: * selected or author styles are enabled/disabled we send a message with the michael@0: * currently applied style to the chrome process. michael@0: * michael@0: * Causes a SessionStore:update message to be sent that contains the currently michael@0: * selected pageStyle for all reachable frames. michael@0: * michael@0: * Example: michael@0: * {pageStyle: "Dusk", children: [null, {pageStyle: "Mozilla"}]} michael@0: */ michael@0: let PageStyleListener = { michael@0: init: function () { michael@0: Services.obs.addObserver(this, "author-style-disabled-changed", false); michael@0: Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false); michael@0: gFrameTree.addObserver(this); michael@0: }, michael@0: michael@0: uninit: function () { michael@0: Services.obs.removeObserver(this, "author-style-disabled-changed"); michael@0: Services.obs.removeObserver(this, "style-sheet-applicable-state-changed"); michael@0: }, michael@0: michael@0: observe: function (subject, topic) { michael@0: let frame = subject.defaultView; michael@0: michael@0: if (frame && gFrameTree.contains(frame)) { michael@0: MessageQueue.push("pageStyle", () => this.collect()); michael@0: } michael@0: }, michael@0: michael@0: collect: function () { michael@0: return PageStyle.collect(docShell, gFrameTree); michael@0: }, michael@0: michael@0: onFrameTreeCollected: function () { michael@0: MessageQueue.push("pageStyle", () => this.collect()); michael@0: }, michael@0: michael@0: onFrameTreeReset: function () { michael@0: MessageQueue.push("pageStyle", () => null); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Listens for changes to docShell capabilities. Whenever a new load is started michael@0: * we need to re-check the list of capabilities and send message when it has michael@0: * changed. michael@0: * michael@0: * Causes a SessionStore:update message to be sent that contains the currently michael@0: * disabled docShell capabilities (all nsIDocShell.allow* properties set to michael@0: * false) as a string - i.e. capability names separate by commas. michael@0: */ michael@0: let DocShellCapabilitiesListener = { michael@0: /** michael@0: * This field is used to compare the last docShell capabilities to the ones michael@0: * that have just been collected. If nothing changed we won't send a message. michael@0: */ michael@0: _latestCapabilities: "", michael@0: michael@0: init: function () { michael@0: gFrameTree.addObserver(this); michael@0: }, michael@0: michael@0: /** michael@0: * onFrameTreeReset() is called as soon as we start loading a page. michael@0: */ michael@0: onFrameTreeReset: function() { michael@0: // The order of docShell capabilities cannot change while we're running michael@0: // so calling join() without sorting before is totally sufficient. michael@0: let caps = DocShellCapabilities.collect(docShell).join(","); michael@0: michael@0: // Send new data only when the capability list changes. michael@0: if (caps != this._latestCapabilities) { michael@0: this._latestCapabilities = caps; michael@0: MessageQueue.push("disallow", () => caps || null); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Listens for changes to the DOMSessionStorage. Whenever new keys are added, michael@0: * existing ones removed or changed, or the storage is cleared we will send a michael@0: * message to the parent process containing up-to-date sessionStorage data. michael@0: * michael@0: * Causes a SessionStore:update message to be sent that contains the current michael@0: * DOMSessionStorage contents. The data is a nested object using host names michael@0: * as keys and per-host DOMSessionStorage data as values. michael@0: */ michael@0: let SessionStorageListener = { michael@0: init: function () { michael@0: addEventListener("MozStorageChanged", this); michael@0: Services.obs.addObserver(this, "browser:purge-domain-data", false); michael@0: gFrameTree.addObserver(this); michael@0: }, michael@0: michael@0: uninit: function () { michael@0: Services.obs.removeObserver(this, "browser:purge-domain-data"); michael@0: }, michael@0: michael@0: handleEvent: function (event) { michael@0: // Ignore events triggered by localStorage or globalStorage changes. michael@0: if (gFrameTree.contains(event.target) && isSessionStorageEvent(event)) { michael@0: this.collect(); michael@0: } michael@0: }, michael@0: michael@0: observe: function () { michael@0: // Collect data on the next tick so that any other observer michael@0: // that needs to purge data can do its work first. michael@0: setTimeout(() => this.collect(), 0); michael@0: }, michael@0: michael@0: collect: function () { michael@0: if (docShell) { michael@0: MessageQueue.push("storage", () => SessionStorage.collect(docShell, gFrameTree)); michael@0: } michael@0: }, michael@0: michael@0: onFrameTreeCollected: function () { michael@0: this.collect(); michael@0: }, michael@0: michael@0: onFrameTreeReset: function () { michael@0: this.collect(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Listen for changes to the privacy status of the tab. michael@0: * By definition, tabs start in non-private mode. michael@0: * michael@0: * Causes a SessionStore:update message to be sent for michael@0: * field "isPrivate". This message contains michael@0: * |true| if the tab is now private michael@0: * |null| if the tab is now public - the field is therefore michael@0: * not saved. michael@0: */ michael@0: let PrivacyListener = { michael@0: init: function() { michael@0: docShell.addWeakPrivacyTransitionObserver(this); michael@0: michael@0: // Check that value at startup as it might have michael@0: // been set before the frame script was loaded. michael@0: if (docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) { michael@0: MessageQueue.push("isPrivate", () => true); michael@0: } michael@0: }, michael@0: michael@0: // Ci.nsIPrivacyTransitionObserver michael@0: privateModeChanged: function(enabled) { michael@0: MessageQueue.push("isPrivate", () => enabled || null); michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivacyTransitionObserver, michael@0: Ci.nsISupportsWeakReference]) michael@0: }; michael@0: michael@0: /** michael@0: * A message queue that takes collected data and will take care of sending it michael@0: * to the chrome process. It allows flushing using synchronous messages and michael@0: * takes care of any race conditions that might occur because of that. Changes michael@0: * will be batched if they're pushed in quick succession to avoid a message michael@0: * flood. michael@0: */ michael@0: let MessageQueue = { michael@0: /** michael@0: * A unique, monotonically increasing ID used for outgoing messages. This is michael@0: * important to make it possible to reuse tabs and allow sync flushes before michael@0: * data could be destroyed. michael@0: */ michael@0: _id: 1, michael@0: michael@0: /** michael@0: * A map (string -> lazy fn) holding lazy closures of all queued data michael@0: * collection routines. These functions will return data collected from the michael@0: * docShell. michael@0: */ michael@0: _data: new Map(), michael@0: michael@0: /** michael@0: * A map holding the |this._id| value for every type of data back when it michael@0: * was pushed onto the queue. We will use those IDs to find the data to send michael@0: * and flush. michael@0: */ michael@0: _lastUpdated: new Map(), michael@0: michael@0: /** michael@0: * The delay (in ms) used to delay sending changes after data has been michael@0: * invalidated. michael@0: */ michael@0: BATCH_DELAY_MS: 1000, michael@0: michael@0: /** michael@0: * The current timeout ID, null if there is no queue data. We use timeouts michael@0: * to damp a flood of data changes and send lots of changes as one batch. michael@0: */ michael@0: _timeout: null, michael@0: michael@0: /** michael@0: * Pushes a given |value| onto the queue. The given |key| represents the type michael@0: * of data that is stored and can override data that has been queued before michael@0: * but has not been sent to the parent process, yet. michael@0: * michael@0: * @param key (string) michael@0: * A unique identifier specific to the type of data this is passed. michael@0: * @param fn (function) michael@0: * A function that returns the value that will be sent to the parent michael@0: * process. michael@0: */ michael@0: push: function (key, fn) { michael@0: this._data.set(key, createLazy(fn)); michael@0: this._lastUpdated.set(key, this._id); michael@0: michael@0: if (!this._timeout) { michael@0: // Wait a little before sending the message to batch multiple changes. michael@0: this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sends queued data to the chrome process. michael@0: * michael@0: * @param options (object) michael@0: * {id: 123} to override the update ID used to accumulate data to send. michael@0: * {sync: true} to send data to the parent process synchronously. michael@0: */ michael@0: send: function (options = {}) { michael@0: // Looks like we have been called off a timeout after the tab has been michael@0: // closed. The docShell is gone now and we can just return here as there michael@0: // is nothing to do. michael@0: if (!docShell) { michael@0: return; michael@0: } michael@0: michael@0: if (this._timeout) { michael@0: clearTimeout(this._timeout); michael@0: this._timeout = null; michael@0: } michael@0: michael@0: let sync = options && options.sync; michael@0: let startID = (options && options.id) || this._id; michael@0: michael@0: // We use sendRpcMessage in the sync case because we may have been called michael@0: // through a CPOW. RPC messages are the only synchronous messages that the michael@0: // child is allowed to send to the parent while it is handling a CPOW michael@0: // request. michael@0: let sendMessage = sync ? sendRpcMessage : sendAsyncMessage; michael@0: michael@0: let durationMs = Date.now(); michael@0: michael@0: let data = {}; michael@0: for (let [key, id] of this._lastUpdated) { michael@0: // There is no data for the given key anymore because michael@0: // the parent process already marked it as received. michael@0: if (!this._data.has(key)) { michael@0: continue; michael@0: } michael@0: michael@0: if (startID > id) { michael@0: // If the |id| passed by the parent process is higher than the one michael@0: // stored in |_lastUpdated| for the given key we know that the parent michael@0: // received all necessary data and we can remove it from the map. michael@0: this._data.delete(key); michael@0: continue; michael@0: } michael@0: michael@0: data[key] = this._data.get(key)(); michael@0: } michael@0: michael@0: durationMs = Date.now() - durationMs; michael@0: let telemetry = { michael@0: FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS: durationMs michael@0: } michael@0: michael@0: // Send all data to the parent process. michael@0: sendMessage("SessionStore:update", { michael@0: id: this._id, michael@0: data: data, michael@0: telemetry: telemetry michael@0: }); michael@0: michael@0: // Increase our unique message ID. michael@0: this._id++; michael@0: }, michael@0: michael@0: /** michael@0: * This function is used to make the message queue flush all queue data that michael@0: * hasn't been sent to the parent process, yet. michael@0: * michael@0: * @param id (int) michael@0: * A unique id that represents the latest message received by the michael@0: * chrome process. We can use this to determine which messages have not michael@0: * yet been received because they are still stuck in the event queue. michael@0: */ michael@0: flush: function (id) { michael@0: // It's important to always send data, even if there is nothing to flush. michael@0: // The update message will be received by the parent process that can then michael@0: // update its last received update ID to ignore stale messages. michael@0: this.send({id: id + 1, sync: true}); michael@0: michael@0: this._data.clear(); michael@0: this._lastUpdated.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * DO NOT USE - DEBUGGING / TESTING ONLY michael@0: * michael@0: * This function is used to simulate certain situations where race conditions michael@0: * can occur by sending data shortly before flushing synchronously. michael@0: */ michael@0: flushAsync: function () { michael@0: if (!Services.prefs.getBoolPref("browser.sessionstore.debug")) { michael@0: throw new Error("flushAsync() must be used for testing, only."); michael@0: } michael@0: michael@0: this.send(); michael@0: } michael@0: }; michael@0: michael@0: EventListener.init(); michael@0: MessageListener.init(); michael@0: FormDataListener.init(); michael@0: SyncHandler.init(); michael@0: PageStyleListener.init(); michael@0: SessionHistoryListener.init(); michael@0: SessionStorageListener.init(); michael@0: ScrollPositionListener.init(); michael@0: DocShellCapabilitiesListener.init(); michael@0: PrivacyListener.init(); michael@0: michael@0: addEventListener("unload", () => { michael@0: // Remove all registered nsIObservers. michael@0: PageStyleListener.uninit(); michael@0: SessionStorageListener.uninit(); michael@0: SessionHistoryListener.uninit(); michael@0: michael@0: // We don't need to take care of any gFrameTree observers as the gFrameTree michael@0: // will die with the content script. The same goes for the privacy transition michael@0: // observer that will die with the docShell when the tab is closed. michael@0: });