summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/TabStateFlusher.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/sessionstore/TabStateFlusher.sys.mjs')
-rw-r--r--browser/components/sessionstore/TabStateFlusher.sys.mjs234
1 files changed, 234 insertions, 0 deletions
diff --git a/browser/components/sessionstore/TabStateFlusher.sys.mjs b/browser/components/sessionstore/TabStateFlusher.sys.mjs
new file mode 100644
index 0000000000..e391abc970
--- /dev/null
+++ b/browser/components/sessionstore/TabStateFlusher.sys.mjs
@@ -0,0 +1,234 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+
+/**
+ * A module that enables async flushes. Updates from frame scripts are
+ * throttled to be sent only once per second. If an action wants a tab's latest
+ * state without waiting for a second then it can request an async flush and
+ * wait until the frame scripts reported back. At this point the parent has the
+ * latest data and the action can continue.
+ */
+export var TabStateFlusher = Object.freeze({
+ /**
+ * Requests an async flush for the given browser. Returns a promise that will
+ * resolve when we heard back from the content process and the parent has
+ * all the latest data.
+ */
+ flush(browser) {
+ return TabStateFlusherInternal.flush(browser);
+ },
+
+ /**
+ * Requests an async flush for all browsers of a given window. Returns a Promise
+ * that will resolve when we've heard back from all browsers.
+ */
+ flushWindow(window) {
+ return TabStateFlusherInternal.flushWindow(window);
+ },
+
+ /**
+ * Resolves the flush request with the given flush ID.
+ *
+ * @param browser (<xul:browser>)
+ * The browser for which the flush is being resolved.
+ * @param flushID (int)
+ * The ID of the flush that was sent to the browser.
+ * @param success (bool, optional)
+ * Whether or not the flush succeeded.
+ * @param message (string, optional)
+ * An error message that will be sent to the Console in the
+ * event that a flush failed.
+ */
+ resolve(browser, flushID, success = true, message = "") {
+ TabStateFlusherInternal.resolve(browser, flushID, success, message);
+ },
+
+ /**
+ * Resolves all active flush requests for a given browser. This should be
+ * used when the content process crashed or the final update message was
+ * seen. In those cases we can't guarantee to ever hear back from the frame
+ * script so we just resolve all requests instead of discarding them.
+ *
+ * @param browser (<xul:browser>)
+ * The browser for which all flushes are being resolved.
+ * @param success (bool, optional)
+ * Whether or not the flushes succeeded.
+ * @param message (string, optional)
+ * An error message that will be sent to the Console in the
+ * event that the flushes failed.
+ */
+ resolveAll(browser, success = true, message = "") {
+ TabStateFlusherInternal.resolveAll(browser, success, message);
+ },
+});
+
+var TabStateFlusherInternal = {
+ // Stores the last request ID.
+ _lastRequestID: 0,
+
+ // A map storing all active requests per browser. A request is a
+ // triple of a map containing all flush requests, a promise that
+ // resolve when a request for a browser is canceled, and the
+ // function to call to cancel a reqeust.
+ _requests: new WeakMap(),
+
+ initEntry(entry) {
+ entry.perBrowserRequests = new Map();
+ entry.cancelPromise = new Promise(resolve => {
+ entry.cancel = resolve;
+ }).then(result => {
+ TabStateFlusherInternal.initEntry(entry);
+ return result;
+ });
+
+ return entry;
+ },
+
+ /**
+ * Requests an async flush for the given browser. Returns a promise that will
+ * resolve when we heard back from the content process and the parent has
+ * all the latest data.
+ */
+ flush(browser) {
+ let id = ++this._lastRequestID;
+ let nativePromise = Promise.resolve();
+ if (browser && browser.frameLoader) {
+ /*
+ Request native listener to flush the tabState.
+ Resolves when flush is complete.
+ */
+ nativePromise = browser.frameLoader.requestTabStateFlush();
+ }
+
+ if (!Services.appinfo.sessionHistoryInParent) {
+ /*
+ In the event that we have to trigger a process switch and thus change
+ browser remoteness, session store needs to register and track the new
+ browser window loaded and to have message manager listener registered
+ ** before ** TabStateFlusher send "SessionStore:flush" message. This fixes
+ the race where we send the message before the message listener is
+ registered for it.
+ */
+ lazy.SessionStore.ensureInitialized(browser.ownerGlobal);
+
+ let mm = browser.messageManager;
+ mm.sendAsyncMessage("SessionStore:flush", {
+ id,
+ epoch: lazy.SessionStore.getCurrentEpoch(browser),
+ });
+ }
+
+ // Retrieve active requests for given browser.
+ let permanentKey = browser.permanentKey;
+ let request = this._requests.get(permanentKey);
+ if (!request) {
+ // If we don't have any requests for this browser, create a new
+ // entry for browser.
+ request = this.initEntry({});
+ this._requests.set(permanentKey, request);
+ }
+
+ // Non-SHIP flushes resolve this after the "SessionStore:update" message. We
+ // don't use that message for SHIP, so it's fine to resolve the request
+ // immediately after the native promise resolves, since SessionStore will
+ // have processed all updates from this browser by that point.
+ let requestPromise = Promise.resolve();
+ if (!Services.appinfo.sessionHistoryInParent) {
+ requestPromise = new Promise(resolve => {
+ // Store resolve() so that we can resolve the promise later.
+ request.perBrowserRequests.set(id, resolve);
+ });
+ }
+
+ return Promise.race([
+ nativePromise.then(_ => requestPromise),
+ request.cancelPromise,
+ ]);
+ },
+
+ /**
+ * Requests an async flush for all non-lazy browsers of a given window.
+ * Returns a Promise that will resolve when we've heard back from all browsers.
+ */
+ flushWindow(window) {
+ let promises = [];
+ for (let browser of window.gBrowser.browsers) {
+ if (window.gBrowser.getTabForBrowser(browser).linkedPanel) {
+ promises.push(this.flush(browser));
+ }
+ }
+ return Promise.all(promises);
+ },
+
+ /**
+ * Resolves the flush request with the given flush ID.
+ *
+ * @param browser (<xul:browser>)
+ * The browser for which the flush is being resolved.
+ * @param flushID (int)
+ * The ID of the flush that was sent to the browser.
+ * @param success (bool, optional)
+ * Whether or not the flush succeeded.
+ * @param message (string, optional)
+ * An error message that will be sent to the Console in the
+ * event that a flush failed.
+ */
+ resolve(browser, flushID, success = true, message = "") {
+ // Nothing to do if there are no pending flushes for the given browser.
+ if (!this._requests.has(browser.permanentKey)) {
+ return;
+ }
+
+ // Retrieve active requests for given browser.
+ let { perBrowserRequests } = this._requests.get(browser.permanentKey);
+ if (!perBrowserRequests.has(flushID)) {
+ return;
+ }
+
+ if (!success) {
+ console.error("Failed to flush browser: ", message);
+ }
+
+ // Resolve the request with the given id.
+ let resolve = perBrowserRequests.get(flushID);
+ perBrowserRequests.delete(flushID);
+ resolve(success);
+ },
+
+ /**
+ * Resolves all active flush requests for a given browser. This should be
+ * used when the content process crashed or the final update message was
+ * seen. In those cases we can't guarantee to ever hear back from the frame
+ * script so we just resolve all requests instead of discarding them.
+ *
+ * @param browser (<xul:browser>)
+ * The browser for which all flushes are being resolved.
+ * @param success (bool, optional)
+ * Whether or not the flushes succeeded.
+ * @param message (string, optional)
+ * An error message that will be sent to the Console in the
+ * event that the flushes failed.
+ */
+ resolveAll(browser, success = true, message = "") {
+ // Nothing to do if there are no pending flushes for the given browser.
+ if (!this._requests.has(browser.permanentKey)) {
+ return;
+ }
+
+ // Retrieve the cancel function for a given browser.
+ let { cancel } = this._requests.get(browser.permanentKey);
+
+ if (!success) {
+ console.error("Failed to flush browser: ", message);
+ }
+
+ // Resolve all requests.
+ cancel(success);
+ },
+};