/* 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/. */ /** * Helper class to coordinate Runtime and Page events. * Events have to be sent in the following order: * - Runtime.executionContextDestroyed * - Page.frameNavigated * - Runtime.executionContextCreated * * This class also handles the special case of Pages going from/to the BF cache. * When you navigate to a new URL, the previous document may be stored in the BF Cache. * All its asynchronous operations are frozen (XHR, timeouts, ...) and a `pagehide` event * is fired for this document. We then navigate to the new URL. * If the user navigates back to the previous page, the page is resurected from the * cache. A `pageshow` event is fired and its asynchronous operations are resumed. * * When a page is in the BF Cache, we should consider it as frozen and shouldn't try * to execute any javascript. So that the ExecutionContext should be considered as * being destroyed and the document navigated. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", executeSoon: "chrome://remote/content/shared/Sync.sys.mjs", }); export class ContextObserver { constructor(chromeEventHandler) { this.chromeEventHandler = chromeEventHandler; lazy.EventEmitter.decorate(this); this._fissionEnabled = Services.appinfo.fissionAutostart; this.chromeEventHandler.addEventListener("DOMWindowCreated", this, { mozSystemGroup: true, }); // Listen for pageshow and pagehide to track pages going in/out to/from the BF Cache this.chromeEventHandler.addEventListener("pageshow", this, { mozSystemGroup: true, }); this.chromeEventHandler.addEventListener("pagehide", this, { mozSystemGroup: true, }); Services.obs.addObserver(this, "document-element-inserted"); Services.obs.addObserver(this, "inner-window-destroyed"); // With Fission disabled the `DOMWindowCreated` event is fired too late. // Use the `webnavigation-create` notification instead. if (!this._fissionEnabled) { Services.obs.addObserver(this, "webnavigation-create"); } Services.obs.addObserver(this, "webnavigation-destroy"); } destructor() { this.chromeEventHandler.removeEventListener("DOMWindowCreated", this, { mozSystemGroup: true, }); this.chromeEventHandler.removeEventListener("pageshow", this, { mozSystemGroup: true, }); this.chromeEventHandler.removeEventListener("pagehide", this, { mozSystemGroup: true, }); Services.obs.removeObserver(this, "document-element-inserted"); Services.obs.removeObserver(this, "inner-window-destroyed"); if (!this._fissionEnabled) { Services.obs.removeObserver(this, "webnavigation-create"); } Services.obs.removeObserver(this, "webnavigation-destroy"); } handleEvent({ type, target, persisted }) { const window = target.defaultView; const frameId = window.browsingContext.id; const id = window.windowGlobalChild.innerWindowId; switch (type) { case "DOMWindowCreated": // Do not pass `id` here as that's the new document ID instead of the old one // that is destroyed. Instead, pass the frameId and let the listener figure out // what ExecutionContext(s) to destroy. this.emit("context-destroyed", { frameId }); // With Fission enabled the frame is attached early enough so that // expected network requests and responses are handles afterward. // Otherwise send the event when `webnavigation-create` is received. if (this._fissionEnabled) { this.emit("frame-attached", { frameId, window }); } break; case "pageshow": // `persisted` is true when this is about a page being resurected from BF Cache if (!persisted) { return; } // XXX(ochameau) we might have to emit FrameNavigate here to properly handle BF Cache // scenario in Page domain events this.emit("context-created", { windowId: id, window }); this.emit("script-loaded", { windowId: id, window }); break; case "pagehide": // `persisted` is true when this is about a page being frozen into BF Cache if (!persisted) { return; } this.emit("context-destroyed", { windowId: id }); break; } } observe(subject, topic, data) { switch (topic) { case "document-element-inserted": const window = subject.defaultView; // Ignore events without a window and those from other tabs if ( !window || window.docShell.chromeEventHandler !== this.chromeEventHandler ) { return; } // Send when the document gets attached to the window, and its location // is available. this.emit("frame-navigated", { frameId: window.browsingContext.id, window, }); const id = window.windowGlobalChild.innerWindowId; this.emit("context-created", { windowId: id, window }); // Delay script-loaded to allow context cleanup to happen first lazy.executeSoon(() => { this.emit("script-loaded", { windowId: id, window }); }); break; case "inner-window-destroyed": const windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; this.emit("context-destroyed", { windowId }); break; case "webnavigation-create": subject.QueryInterface(Ci.nsIDocShell); this.onDocShellCreated(subject); break; case "webnavigation-destroy": subject.QueryInterface(Ci.nsIDocShell); this.onDocShellDestroyed(subject); break; } } onDocShellCreated(docShell) { this.emit("frame-attached", { frameId: docShell.browsingContext.id, window: docShell.browsingContext.window, }); } onDocShellDestroyed(docShell) { this.emit("frame-detached", { frameId: docShell.browsingContext.id, }); } }