diff options
Diffstat (limited to 'browser/components/sessionstore/ContentSessionStore.sys.mjs')
-rw-r--r-- | browser/components/sessionstore/ContentSessionStore.sys.mjs | 686 |
1 files changed, 0 insertions, 686 deletions
diff --git a/browser/components/sessionstore/ContentSessionStore.sys.mjs b/browser/components/sessionstore/ContentSessionStore.sys.mjs deleted file mode 100644 index 61aa911f0b..0000000000 --- a/browser/components/sessionstore/ContentSessionStore.sys.mjs +++ /dev/null @@ -1,686 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; -import { - clearTimeout, - setTimeoutWithTarget, -} from "resource://gre/modules/Timer.sys.mjs"; - -const lazy = {}; - -ChromeUtils.defineESModuleGetters(lazy, { - ContentRestore: "resource:///modules/sessionstore/ContentRestore.sys.mjs", - SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs", -}); - -// This pref controls whether or not we send updates to the parent on a timeout -// or not, and should only be used for tests or debugging. -const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates"; - -const PREF_INTERVAL = "browser.sessionstore.interval"; - -const kNoIndex = Number.MAX_SAFE_INTEGER; -const kLastIndex = Number.MAX_SAFE_INTEGER - 1; - -class Handler { - constructor(store) { - this.store = store; - } - - get contentRestore() { - return this.store.contentRestore; - } - - get contentRestoreInitialized() { - return this.store.contentRestoreInitialized; - } - - get mm() { - return this.store.mm; - } - - get messageQueue() { - return this.store.messageQueue; - } -} - -/** - * Listens for and handles content events that we need for the - * session store service to be notified of state changes in content. - */ -class EventListener extends Handler { - constructor(store) { - super(store); - - SessionStoreUtils.addDynamicFrameFilteredListener( - this.mm, - "load", - this, - true - ); - } - - handleEvent(event) { - let { content } = this.mm; - - // Ignore load events from subframes. - if (event.target != content.document) { - return; - } - - if (content.document.documentURI.startsWith("about:reader")) { - if ( - event.type == "load" && - !content.document.body.classList.contains("loaded") - ) { - // Don't restore the scroll position of an about:reader page at this - // point; listen for the custom event dispatched from AboutReader.sys.mjs. - content.addEventListener("AboutReaderContentReady", this); - return; - } - - content.removeEventListener("AboutReaderContentReady", this); - } - - if (this.contentRestoreInitialized) { - // Restore the form data and scroll position. - this.contentRestore.restoreDocument(); - } - } -} - -/** - * Listens for changes to the session history. Whenever the user navigates - * we will collect URLs and everything belonging to session history. - * - * Causes a SessionStore:update message to be sent that contains the current - * session history. - * - * Example: - * {entries: [{url: "about:mozilla", ...}, ...], index: 1} - */ -class SessionHistoryListener extends Handler { - constructor(store) { - super(store); - - this._fromIdx = kNoIndex; - - // By adding the SHistoryListener immediately, we will unfortunately be - // notified of every history entry as the tab is restored. We don't bother - // waiting to add the listener later because these notifications are cheap. - // We will likely only collect once since we are batching collection on - // a delay. - this.mm.docShell - .QueryInterface(Ci.nsIWebNavigation) - .sessionHistory.legacySHistory.addSHistoryListener(this); // OK in non-geckoview - - let webProgress = this.mm.docShell - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - - webProgress.addProgressListener( - this, - Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT - ); - - // Collect data if we start with a non-empty shistory. - if (!lazy.SessionHistory.isEmpty(this.mm.docShell)) { - this.collect(); - // When a tab is detached from the window, for the new window there is a - // new SessionHistoryListener created. Normally it is empty at this point - // but in a test env. the initial about:blank might have a children in which - // case we fire off a history message here with about:blank in it. If we - // don't do it ASAP then there is going to be a browser swap and the parent - // will be all confused by that message. - this.store.messageQueue.send(); - } - - // Listen for page title changes. - this.mm.addEventListener("DOMTitleChanged", this); - } - - get mm() { - return this.store.mm; - } - - uninit() { - let sessionHistory = this.mm.docShell.QueryInterface( - Ci.nsIWebNavigation - ).sessionHistory; - if (sessionHistory) { - sessionHistory.legacySHistory.removeSHistoryListener(this); // OK in non-geckoview - } - } - - collect() { - // We want to send down a historychange even for full collects in case our - // session history is a partial session history, in which case we don't have - // enough information for a full update. collectFrom(-1) tells the collect - // function to collect all data avaliable in this process. - if (this.mm.docShell) { - this.collectFrom(-1); - } - } - - // History can grow relatively big with the nested elements, so if we don't have to, we - // don't want to send the entire history all the time. For a simple optimization - // we keep track of the smallest index from after any change has occured and we just send - // the elements from that index. If something more complicated happens we just clear it - // and send the entire history. We always send the additional info like the current selected - // index (so for going back and forth between history entries we set the index to kLastIndex - // if nothing else changed send an empty array and the additonal info like the selected index) - collectFrom(idx) { - if (this._fromIdx <= idx) { - // If we already know that we need to update history fromn index N we can ignore any changes - // tha happened with an element with index larger than N. - // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything - // here, and in case of navigation in the history back and forth we use kLastIndex which ignores - // only the subsequent navigations, but not any new elements added. - return; - } - - this._fromIdx = idx; - this.store.messageQueue.push("historychange", () => { - if (this._fromIdx === kNoIndex) { - return null; - } - - let history = lazy.SessionHistory.collect( - this.mm.docShell, - this._fromIdx - ); - this._fromIdx = kNoIndex; - return history; - }); - } - - handleEvent(event) { - this.collect(); - } - - OnHistoryNewEntry(newURI, oldIndex) { - // Collect the current entry as well, to make sure to collect any changes - // that were made to the entry while the document was active. - this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1); - } - - OnHistoryGotoIndex() { - // We ought to collect the previously current entry as well, see bug 1350567. - this.collectFrom(kLastIndex); - } - - OnHistoryPurge() { - this.collect(); - } - - OnHistoryReload() { - this.collect(); - return true; - } - - OnHistoryReplaceEntry() { - this.collect(); - } - - /** - * @see nsIWebProgressListener.onStateChange - */ - onStateChange(webProgress, request, stateFlags, status) { - // Ignore state changes for subframes because we're only interested in the - // top-document starting or stopping its load. - if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) { - return; - } - - // onStateChange will be fired when loading the initial about:blank URI for - // a browser, which we don't actually care about. This is particularly for - // the case of unrestored background tabs, where the content has not yet - // been restored: we don't want to accidentally send any updates to the - // parent when the about:blank placeholder page has loaded. - if (!this.mm.docShell.hasLoadedNonBlankURI) { - return; - } - - if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { - this.collect(); - } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { - this.collect(); - } - } -} -SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([ - "nsIWebProgressListener", - "nsISHistoryListener", - "nsISupportsWeakReference", -]); - -/** - * A message queue that takes collected data and will take care of sending it - * to the chrome process. It allows flushing using synchronous messages and - * takes care of any race conditions that might occur because of that. Changes - * will be batched if they're pushed in quick succession to avoid a message - * flood. - */ -class MessageQueue extends Handler { - constructor(store) { - super(store); - - /** - * A map (string -> lazy fn) holding lazy closures of all queued data - * collection routines. These functions will return data collected from the - * docShell. - */ - this._data = new Map(); - - /** - * The delay (in ms) used to delay sending changes after data has been - * invalidated. - */ - this.BATCH_DELAY_MS = 1000; - - /** - * The minimum idle period (in ms) we need for sending data to chrome process. - */ - this.NEEDED_IDLE_PERIOD_MS = 5; - - /** - * Timeout for waiting an idle period to send data. We will set this from - * the pref "browser.sessionstore.interval". - */ - this._timeoutWaitIdlePeriodMs = null; - - /** - * The current timeout ID, null if there is no queue data. We use timeouts - * to damp a flood of data changes and send lots of changes as one batch. - */ - this._timeout = null; - - /** - * Whether or not sending batched messages on a timer is disabled. This should - * only be used for debugging or testing. If you need to access this value, - * you should probably use the timeoutDisabled getter. - */ - this._timeoutDisabled = false; - - /** - * True if there is already a send pending idle dispatch, set to prevent - * scheduling more than one. If false there may or may not be one scheduled. - */ - this._idleScheduled = false; - - this.timeoutDisabled = Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF); - this._timeoutWaitIdlePeriodMs = Services.prefs.getIntPref(PREF_INTERVAL); - - Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this); - Services.prefs.addObserver(PREF_INTERVAL, this); - } - - /** - * True if batched messages are not being fired on a timer. This should only - * ever be true when debugging or during tests. - */ - get timeoutDisabled() { - return this._timeoutDisabled; - } - - /** - * Disables sending batched messages on a timer. Also cancels any pending - * timers. - */ - set timeoutDisabled(val) { - this._timeoutDisabled = val; - - if (val && this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - } - } - - uninit() { - Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this); - Services.prefs.removeObserver(PREF_INTERVAL, this); - this.cleanupTimers(); - } - - /** - * Cleanup pending idle callback and timer. - */ - cleanupTimers() { - this._idleScheduled = false; - if (this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - } - } - - observe(subject, topic, data) { - if (topic == "nsPref:changed") { - switch (data) { - case TIMEOUT_DISABLED_PREF: - this.timeoutDisabled = Services.prefs.getBoolPref( - TIMEOUT_DISABLED_PREF - ); - break; - case PREF_INTERVAL: - this._timeoutWaitIdlePeriodMs = - Services.prefs.getIntPref(PREF_INTERVAL); - break; - default: - console.error("received unknown message '" + data + "'"); - break; - } - } - } - - /** - * Pushes a given |value| onto the queue. The given |key| represents the type - * of data that is stored and can override data that has been queued before - * but has not been sent to the parent process, yet. - * - * @param key (string) - * A unique identifier specific to the type of data this is passed. - * @param fn (function) - * A function that returns the value that will be sent to the parent - * process. - */ - push(key, fn) { - this._data.set(key, fn); - - if (!this._timeout && !this._timeoutDisabled) { - // Wait a little before sending the message to batch multiple changes. - this._timeout = setTimeoutWithTarget( - () => this.sendWhenIdle(), - this.BATCH_DELAY_MS, - this.mm.tabEventTarget - ); - } - } - - /** - * Sends queued data when the remaining idle time is enough or waiting too - * long; otherwise, request an idle time again. If the |deadline| is not - * given, this function is going to schedule the first request. - * - * @param deadline (object) - * An IdleDeadline object passed by idleDispatch(). - */ - sendWhenIdle(deadline) { - if (!this.mm.content) { - // The frameloader is being torn down. Nothing more to do. - return; - } - - if (deadline) { - if ( - deadline.didTimeout || - deadline.timeRemaining() > this.NEEDED_IDLE_PERIOD_MS - ) { - this.send(); - return; - } - } else if (this._idleScheduled) { - // Bail out if there's a pending run. - return; - } - ChromeUtils.idleDispatch(deadline_ => this.sendWhenIdle(deadline_), { - timeout: this._timeoutWaitIdlePeriodMs, - }); - this._idleScheduled = true; - } - - /** - * Sends queued data to the chrome process. - * - * @param options (object) - * {flushID: 123} to specify that this is a flush - * {isFinal: true} to signal this is the final message sent on unload - */ - send(options = {}) { - // Looks like we have been called off a timeout after the tab has been - // closed. The docShell is gone now and we can just return here as there - // is nothing to do. - if (!this.mm.docShell) { - return; - } - - this.cleanupTimers(); - - let flushID = (options && options.flushID) || 0; - let histID = "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_MS"; - - let data = {}; - for (let [key, func] of this._data) { - if (key != "isPrivate") { - TelemetryStopwatch.startKeyed(histID, key); - } - - let value = func(); - - if (key != "isPrivate") { - TelemetryStopwatch.finishKeyed(histID, key); - } - - if (value || (key != "storagechange" && key != "historychange")) { - data[key] = value; - } - } - - this._data.clear(); - - try { - // Send all data to the parent process. - this.mm.sendAsyncMessage("SessionStore:update", { - data, - flushID, - isFinal: options.isFinal || false, - epoch: this.store.epoch, - }); - } catch (ex) { - if (ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) { - Services.telemetry - .getHistogramById("FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM") - .add(1); - this.mm.sendAsyncMessage("SessionStore:error"); - } - } - } -} - -/** - * Listens for and handles messages sent by the session store service. - */ -const MESSAGES = [ - "SessionStore:restoreHistory", - "SessionStore:restoreTabContent", - "SessionStore:resetRestore", - "SessionStore:flush", - "SessionStore:prepareForProcessChange", -]; - -export class ContentSessionStore { - constructor(mm) { - if (Services.appinfo.sessionHistoryInParent) { - throw new Error("This frame script should not be loaded for SHIP"); - } - - this.mm = mm; - this.messageQueue = new MessageQueue(this); - - this.epoch = 0; - - this.contentRestoreInitialized = false; - - this.handlers = [ - this.messageQueue, - new EventListener(this), - new SessionHistoryListener(this), - ]; - - XPCOMUtils.defineLazyGetter(this, "contentRestore", () => { - this.contentRestoreInitialized = true; - return new lazy.ContentRestore(mm); - }); - - MESSAGES.forEach(m => mm.addMessageListener(m, this)); - - mm.addEventListener("unload", this); - } - - receiveMessage({ name, data }) { - // The docShell might be gone. Don't process messages, - // that will just lead to errors anyway. - if (!this.mm.docShell) { - return; - } - - // A fresh tab always starts with epoch=0. The parent has the ability to - // override that to signal a new era in this tab's life. This enables it - // to ignore async messages that were already sent but not yet received - // and would otherwise confuse the internal tab state. - if (data && data.epoch && data.epoch != this.epoch) { - this.epoch = data.epoch; - } - - switch (name) { - case "SessionStore:restoreHistory": - this.restoreHistory(data); - break; - case "SessionStore:restoreTabContent": - this.restoreTabContent(data); - break; - case "SessionStore:resetRestore": - this.contentRestore.resetRestore(); - break; - case "SessionStore:flush": - this.flush(data); - break; - case "SessionStore:prepareForProcessChange": - // During normal in-process navigations, the DocShell would take - // care of automatically persisting layout history state to record - // scroll positions on the nsSHEntry. Unfortunately, process switching - // is not a normal navigation, so for now we do this ourselves. This - // is a workaround until session history state finally lives in the - // parent process. - this.mm.docShell.persistLayoutHistoryState(); - break; - default: - console.error("received unknown message '" + name + "'"); - break; - } - } - - // non-SHIP only - restoreHistory(data) { - let { epoch, tabData, loadArguments, isRemotenessUpdate } = data; - - this.contentRestore.restoreHistory(tabData, loadArguments, { - // Note: The callbacks passed here will only be used when a load starts - // that was not initiated by sessionstore itself. This can happen when - // some code calls browser.loadURI() or browser.reload() on a pending - // browser/tab. - - onLoadStarted: () => { - // Notify the parent that the tab is no longer pending. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", { - epoch, - }); - }, - - onLoadFinished: () => { - // Tell SessionStore.sys.mjs that it may want to restore some more tabs, - // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { - epoch, - }); - }, - }); - - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { - // For non-remote tabs, when restoreHistory finishes, we send a synchronous - // message to SessionStore.sys.mjs so that it can run SSTabRestoring. Users of - // SSTabRestoring seem to get confused if chrome and content are out of - // sync about the state of the restore (particularly regarding - // docShell.currentURI). Using a synchronous message is the easiest way - // to temporarily synchronize them. - // - // For remote tabs, because all nsIWebProgress notifications are sent - // asynchronously using messages, we get the same-order guarantees of the - // message manager, and can use an async message. - this.mm.sendSyncMessage("SessionStore:restoreHistoryComplete", { - epoch, - isRemotenessUpdate, - }); - } else { - this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", { - epoch, - isRemotenessUpdate, - }); - } - } - - restoreTabContent({ loadArguments, isRemotenessUpdate, reason }) { - let epoch = this.epoch; - - // We need to pass the value of didStartLoad back to SessionStore.sys.mjs. - let didStartLoad = this.contentRestore.restoreTabContent( - loadArguments, - isRemotenessUpdate, - () => { - // Tell SessionStore.sys.mjs that it may want to restore some more tabs, - // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { - epoch, - isRemotenessUpdate, - }); - } - ); - - this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", { - epoch, - isRemotenessUpdate, - reason, - }); - - if (!didStartLoad) { - // Pretend that the load succeeded so that event handlers fire correctly. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { - epoch, - isRemotenessUpdate, - }); - } - } - - flush({ id }) { - // Flush the message queue, send the latest updates. - this.messageQueue.send({ flushID: id }); - } - - handleEvent(event) { - if (event.type == "unload") { - this.onUnload(); - } - } - - onUnload() { - // Upon frameLoader destruction, send a final update message to - // the parent and flush all data currently held in the child. - this.messageQueue.send({ isFinal: true }); - - for (let handler of this.handlers) { - if (handler.uninit) { - handler.uninit(); - } - } - - if (this.contentRestoreInitialized) { - // Remove progress listeners. - this.contentRestore.resetRestore(); - } - - // We don't need to take care of any StateChangeNotifier observers as they - // will die with the content script. The same goes for the privacy transition - // observer that will die with the docShell when the tab is closed. - } -} |