tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SessionStore.sys.mjs (297557B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 // Current version of the format used by Session Restore.
      6 const FORMAT_VERSION = 1;
      7 
      8 const PERSIST_SESSIONS = Services.prefs.getBoolPref(
      9  "browser.sessionstore.persist_closed_tabs_between_sessions"
     10 );
     11 const TAB_CUSTOM_VALUES = new WeakMap();
     12 const TAB_LAZY_STATES = new WeakMap();
     13 const TAB_STATE_NEEDS_RESTORE = 1;
     14 const TAB_STATE_RESTORING = 2;
     15 const TAB_STATE_FOR_BROWSER = new WeakMap();
     16 const WINDOW_RESTORE_IDS = new WeakMap();
     17 const WINDOW_RESTORE_ZINDICES = new WeakMap();
     18 const WINDOW_SHOWING_PROMISES = new Map();
     19 const WINDOW_FLUSHING_PROMISES = new Map();
     20 
     21 // A new window has just been restored. At this stage, tabs are generally
     22 // not restored.
     23 const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
     24 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
     25 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
     26 const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
     27 const NOTIFY_LAST_SESSION_RE_ENABLED = "sessionstore-last-session-re-enable";
     28 const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
     29 const NOTIFY_INITIATING_MANUAL_RESTORE =
     30  "sessionstore-initiating-manual-restore";
     31 const NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
     32 const NOTIFY_SAVED_TAB_GROUPS_CHANGED = "sessionstore-saved-tab-groups-changed";
     33 
     34 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
     35 const NOTIFY_DOMWINDOWCLOSED_HANDLED =
     36  "sessionstore-debug-domwindowclosed-handled"; // WARNING: debug-only
     37 
     38 const NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush";
     39 
     40 // Maximum number of tabs to restore simultaneously. Previously controlled by
     41 // the browser.sessionstore.max_concurrent_tabs pref.
     42 const MAX_CONCURRENT_TAB_RESTORES = 3;
     43 
     44 // Minimum amount (in CSS px) by which we allow window edges to be off-screen
     45 // when restoring a window, before we override the saved position to pull the
     46 // window back within the available screen area.
     47 const MIN_SCREEN_EDGE_SLOP = 8;
     48 
     49 // global notifications observed
     50 const OBSERVING = [
     51  "browser-window-before-show",
     52  "domwindowclosed",
     53  "quit-application-granted",
     54  "browser-lastwindow-close-granted",
     55  "quit-application",
     56  "browser:purge-session-history",
     57  "browser:purge-session-history-for-domain",
     58  "idle-daily",
     59  "clear-origin-attributes-data",
     60  "browsing-context-did-set-embedder",
     61  "browsing-context-discarded",
     62  "browser-shutdown-tabstate-updated",
     63 ];
     64 
     65 // XUL Window properties to (re)store
     66 // Restored in restoreDimensions()
     67 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
     68 
     69 const CHROME_FLAGS_MAP = [
     70  [Ci.nsIWebBrowserChrome.CHROME_TITLEBAR, "titlebar"],
     71  [Ci.nsIWebBrowserChrome.CHROME_WINDOW_CLOSE, "close"],
     72  [Ci.nsIWebBrowserChrome.CHROME_TOOLBAR, "toolbar"],
     73  [Ci.nsIWebBrowserChrome.CHROME_LOCATIONBAR, "location"],
     74  [Ci.nsIWebBrowserChrome.CHROME_PERSONAL_TOOLBAR, "personalbar"],
     75  [Ci.nsIWebBrowserChrome.CHROME_STATUSBAR, "status"],
     76  [Ci.nsIWebBrowserChrome.CHROME_MENUBAR, "menubar"],
     77  [Ci.nsIWebBrowserChrome.CHROME_WINDOW_RESIZE, "resizable"],
     78  [Ci.nsIWebBrowserChrome.CHROME_WINDOW_MINIMIZE, "minimizable"],
     79  [Ci.nsIWebBrowserChrome.CHROME_SCROLLBARS, "", "scrollbars=0"],
     80  [Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, "private"],
     81  [Ci.nsIWebBrowserChrome.CHROME_NON_PRIVATE_WINDOW, "non-private"],
     82  // Do not inherit remoteness and fissionness from the previous session.
     83  //[Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW, "remote", "non-remote"],
     84  //[Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW, "fission", "non-fission"],
     85  // "chrome" and "suppressanimation" are always set.
     86  //[Ci.nsIWebBrowserChrome.CHROME_SUPPRESS_ANIMATION, "suppressanimation"],
     87  [Ci.nsIWebBrowserChrome.CHROME_ALWAYS_ON_TOP, "alwaysontop"],
     88  //[Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME, "chrome", "chrome=0"],
     89  [Ci.nsIWebBrowserChrome.CHROME_EXTRA, "extrachrome"],
     90  [Ci.nsIWebBrowserChrome.CHROME_CENTER_SCREEN, "centerscreen"],
     91  [Ci.nsIWebBrowserChrome.CHROME_DEPENDENT, "dependent"],
     92  [Ci.nsIWebBrowserChrome.CHROME_MODAL, "modal"],
     93  [Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG, "dialog", "dialog=0"],
     94 ];
     95 
     96 // Hideable window features to (re)store
     97 // Restored in restoreWindowFeatures()
     98 const WINDOW_HIDEABLE_FEATURES = [
     99  "menubar",
    100  "toolbar",
    101  "locationbar",
    102  "personalbar",
    103  "statusbar",
    104  "scrollbars",
    105 ];
    106 
    107 const WINDOW_OPEN_FEATURES_MAP = {
    108  locationbar: "location",
    109  statusbar: "status",
    110 };
    111 
    112 // These are tab events that we listen to.
    113 const TAB_EVENTS = [
    114  "TabOpen",
    115  "TabBrowserInserted",
    116  "TabClose",
    117  "TabSelect",
    118  "TabShow",
    119  "TabHide",
    120  "TabPinned",
    121  "TabUnpinned",
    122  "TabGroupCreate",
    123  "TabGroupRemoveRequested",
    124  "TabGroupRemoved",
    125  "TabGrouped",
    126  "TabUngrouped",
    127  "TabGroupCollapse",
    128  "TabGroupExpand",
    129  "TabSplitViewActivate",
    130 ];
    131 
    132 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    133 
    134 /**
    135 * When calling restoreTabContent, we can supply a reason why
    136 * the content is being restored. These are those reasons.
    137 */
    138 const RESTORE_TAB_CONTENT_REASON = {
    139  /**
    140   * SET_STATE:
    141   * We're restoring this tab's content because we're setting
    142   * state inside this browser tab, probably because the user
    143   * has asked us to restore a tab (or window, or entire session).
    144   */
    145  SET_STATE: 0,
    146  /**
    147   * NAVIGATE_AND_RESTORE:
    148   * We're restoring this tab's content because a navigation caused
    149   * us to do a remoteness-flip.
    150   */
    151  NAVIGATE_AND_RESTORE: 1,
    152 };
    153 
    154 // 'browser.startup.page' preference value to resume the previous session.
    155 const BROWSER_STARTUP_RESUME_SESSION = 3;
    156 
    157 // Used by SessionHistoryListener.
    158 const kNoIndex = Number.MAX_SAFE_INTEGER;
    159 const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
    160 
    161 import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
    162 
    163 import { TabMetrics } from "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs";
    164 import { TelemetryTimestamps } from "resource://gre/modules/TelemetryTimestamps.sys.mjs";
    165 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
    166 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
    167 import { GlobalState } from "resource:///modules/sessionstore/GlobalState.sys.mjs";
    168 
    169 const lazy = {};
    170 
    171 XPCOMUtils.defineLazyServiceGetters(lazy, {
    172  gScreenManager: ["@mozilla.org/gfx/screenmanager;1", Ci.nsIScreenManager],
    173 });
    174 
    175 ChromeUtils.defineESModuleGetters(lazy, {
    176  AIWindow:
    177    "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
    178  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
    179  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
    180  DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
    181  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
    182  HomePage: "resource:///modules/HomePage.sys.mjs",
    183  JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
    184  PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs",
    185  sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
    186  RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
    187  SessionCookies: "resource:///modules/sessionstore/SessionCookies.sys.mjs",
    188  SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
    189  SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
    190  SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
    191  SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
    192  SessionStoreHelper:
    193    "resource://gre/modules/sessionstore/SessionStoreHelper.sys.mjs",
    194  TabAttributes: "resource:///modules/sessionstore/TabAttributes.sys.mjs",
    195  TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
    196  TabGroupState: "resource:///modules/sessionstore/TabGroupState.sys.mjs",
    197  TabState: "resource:///modules/sessionstore/TabState.sys.mjs",
    198  TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
    199  TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
    200  setTimeout: "resource://gre/modules/Timer.sys.mjs",
    201 });
    202 
    203 ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
    204  return Services.io.newURI("about:blank");
    205 });
    206 
    207 XPCOMUtils.defineLazyPreferenceGetter(
    208  lazy,
    209  "gRestoreWindowsToVirtualDesktop",
    210  "browser.sessionstore.restore_windows_to_virtual_desktop"
    211 );
    212 
    213 /**
    214 * |true| if we are in debug mode, |false| otherwise.
    215 * Debug mode is controlled by preference browser.sessionstore.debug
    216 */
    217 var gDebuggingEnabled = false;
    218 
    219 /**
    220 * @namespace SessionStore
    221 */
    222 export var SessionStore = {
    223  get logger() {
    224    return SessionStoreInternal._log;
    225  },
    226  get promiseInitialized() {
    227    return SessionStoreInternal.promiseInitialized;
    228  },
    229 
    230  get promiseAllWindowsRestored() {
    231    return SessionStoreInternal.promiseAllWindowsRestored;
    232  },
    233 
    234  get canRestoreLastSession() {
    235    return SessionStoreInternal.canRestoreLastSession;
    236  },
    237 
    238  set canRestoreLastSession(val) {
    239    SessionStoreInternal.canRestoreLastSession = val;
    240  },
    241 
    242  get lastClosedObjectType() {
    243    return SessionStoreInternal.lastClosedObjectType;
    244  },
    245 
    246  get lastClosedActions() {
    247    return [...SessionStoreInternal._lastClosedActions];
    248  },
    249 
    250  get LAST_ACTION_CLOSED_TAB() {
    251    return SessionStoreInternal._LAST_ACTION_CLOSED_TAB;
    252  },
    253 
    254  get LAST_ACTION_CLOSED_WINDOW() {
    255    return SessionStoreInternal._LAST_ACTION_CLOSED_WINDOW;
    256  },
    257 
    258  get savedGroups() {
    259    return SessionStoreInternal._savedGroups;
    260  },
    261 
    262  get willAutoRestore() {
    263    return SessionStoreInternal.willAutoRestore;
    264  },
    265 
    266  get shouldRestoreLastSession() {
    267    return SessionStoreInternal._shouldRestoreLastSession;
    268  },
    269 
    270  init: function ss_init() {
    271    SessionStoreInternal.init();
    272  },
    273 
    274  /**
    275   * Get the collection of all matching windows tracked by SessionStore
    276   *
    277   * @param {Window | object} [aWindowOrOptions] Optionally an options object or a window to used to determine if we're filtering for private or non-private windows
    278   * @param {boolean} [aWindowOrOptions.private] Determine if we should filter for private or non-private windows
    279   */
    280  getWindows(aWindowOrOptions) {
    281    return SessionStoreInternal.getWindows(aWindowOrOptions);
    282  },
    283 
    284  /**
    285   * Get window a given closed tab belongs to
    286   *
    287   * @param {integer} aClosedId The closedId of the tab whose window we want to find
    288   * @param {boolean} [aIncludePrivate] Optionally include private windows when searching for the closed tab
    289   */
    290  getWindowForTabClosedId(aClosedId, aIncludePrivate) {
    291    return SessionStoreInternal.getWindowForTabClosedId(
    292      aClosedId,
    293      aIncludePrivate
    294    );
    295  },
    296 
    297  getBrowserState: function ss_getBrowserState() {
    298    return SessionStoreInternal.getBrowserState();
    299  },
    300 
    301  setBrowserState: function ss_setBrowserState(aState) {
    302    SessionStoreInternal.setBrowserState(aState);
    303  },
    304 
    305  getWindowState: function ss_getWindowState(aWindow) {
    306    return SessionStoreInternal.getWindowState(aWindow);
    307  },
    308 
    309  setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) {
    310    SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite);
    311  },
    312 
    313  getTabState: function ss_getTabState(aTab) {
    314    return SessionStoreInternal.getTabState(aTab);
    315  },
    316 
    317  setTabState: function ss_setTabState(aTab, aState) {
    318    SessionStoreInternal.setTabState(aTab, aState);
    319  },
    320 
    321  // Return whether a tab is restoring.
    322  isTabRestoring(aTab) {
    323    return TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser);
    324  },
    325 
    326  getInternalObjectState(obj) {
    327    return SessionStoreInternal.getInternalObjectState(obj);
    328  },
    329 
    330  duplicateTab: function ss_duplicateTab(
    331    aWindow,
    332    aTab,
    333    aDelta = 0,
    334    aRestoreImmediately = true,
    335    aOptions = {}
    336  ) {
    337    return SessionStoreInternal.duplicateTab(
    338      aWindow,
    339      aTab,
    340      aDelta,
    341      aRestoreImmediately,
    342      aOptions
    343    );
    344  },
    345 
    346  /**
    347   * How many tabs were last closed. If multiple tabs were selected and closed together,
    348   * we'll return that number. Normally the count is 1, or 0 if no tabs have been
    349   * recently closed in this window.
    350   *
    351   * @returns the number of tabs that were last closed.
    352   */
    353  getLastClosedTabCount(aWindow) {
    354    return SessionStoreInternal.getLastClosedTabCount(aWindow);
    355  },
    356 
    357  resetLastClosedTabCount(aWindow) {
    358    SessionStoreInternal.resetLastClosedTabCount(aWindow);
    359  },
    360 
    361  /**
    362   * Get the number of closed tabs associated with a specific window
    363   *
    364   * @param {Window} aWindow
    365   */
    366  getClosedTabCountForWindow: function ss_getClosedTabCountForWindow(aWindow) {
    367    return SessionStoreInternal.getClosedTabCountForWindow(aWindow);
    368  },
    369 
    370  /**
    371   * Get the number of closed tabs associated with all matching windows
    372   *
    373   * @param {Window | object} [aOptions]
    374   *        Either a DOMWindow (see aOptions.sourceWindow) or an object with properties
    375            to identify which closed tabs to include in the count.
    376   * @param {Window} aOptions.sourceWindow
    377            A browser window used to identity privateness.
    378            When closedTabsFromAllWindows is false, we only count closed tabs assocated with this window.
    379   * @param {boolean} [aOptions.private = false]
    380            Explicit indicator to constrain tab count to only private or non-private windows,
    381   * @param {boolean} [aOptions.closedTabsFromAllWindows]
    382            Override the value of the closedTabsFromAllWindows preference.
    383   * @param {boolean} [aOptions.closedTabsFromClosedWindows]
    384            Override the value of the closedTabsFromClosedWindows preference.
    385   */
    386  getClosedTabCount: function ss_getClosedTabCount(aOptions) {
    387    return SessionStoreInternal.getClosedTabCount(aOptions);
    388  },
    389 
    390  /**
    391   * Get the number of closed tabs from recently closed window
    392   *
    393   * This is normally only relevant in a non-private window context, as we don't
    394   * keep data from closed private windows.
    395   */
    396  getClosedTabCountFromClosedWindows:
    397    function ss_getClosedTabCountFromClosedWindows() {
    398      return SessionStoreInternal.getClosedTabCountFromClosedWindows();
    399    },
    400 
    401  /**
    402   * Get the closed tab data associated with this window
    403   *
    404   * @param {Window} aWindow
    405   */
    406  getClosedTabDataForWindow: function ss_getClosedTabDataForWindow(aWindow) {
    407    return SessionStoreInternal.getClosedTabDataForWindow(aWindow);
    408  },
    409 
    410  /**
    411   * Get the closed tab data associated with all matching windows
    412   *
    413   * @param {Window | object} [aOptions]
    414   *        Either a DOMWindow (see aOptions.sourceWindow) or an object with properties
    415            to identify which closed tabs to get data from
    416   * @param {Window} aOptions.sourceWindow
    417            A browser window used to identity privateness.
    418            When closedTabsFromAllWindows is false, we only include closed tabs assocated with this window.
    419   * @param {boolean} [aOptions.private = false]
    420            Explicit indicator to constrain tab data to only private or non-private windows,
    421   * @param {boolean} [aOptions.closedTabsFromAllWindows]
    422            Override the value of the closedTabsFromAllWindows preference.
    423   * @param {boolean} [aOptions.closedTabsFromClosedWindows]
    424            Override the value of the closedTabsFromClosedWindows preference.
    425   */
    426  getClosedTabData: function ss_getClosedTabData(aOptions) {
    427    return SessionStoreInternal.getClosedTabData(aOptions);
    428  },
    429 
    430  /**
    431   * Get the closed tab data associated with all closed windows
    432   *
    433   * @returns an un-sorted array of tabData for closed tabs from closed windows
    434   */
    435  getClosedTabDataFromClosedWindows:
    436    function ss_getClosedTabDataFromClosedWindows() {
    437      return SessionStoreInternal.getClosedTabDataFromClosedWindows();
    438    },
    439 
    440  /**
    441   * Get the closed tab group data associated with all matching windows
    442   *
    443   * @param {Window|object} aOptions
    444   *        Either a DOMWindow (see aOptions.sourceWindow) or an object with properties
    445            to identify the window source of the closed tab groups
    446   * @param {Window} [aOptions.sourceWindow]
    447            A browser window used to identity privateness.
    448            When closedTabsFromAllWindows is false, we only include closed tab groups assocated with this window.
    449   * @param {boolean} [aOptions.private = false]
    450            Explicit indicator to constrain tab group data to only private or non-private windows,
    451   * @param {boolean} [aOptions.closedTabsFromAllWindows]
    452            Override the value of the closedTabsFromAllWindows preference.
    453   * @param {boolean} [aOptions.closedTabsFromClosedWindows]
    454            Override the value of the closedTabsFromClosedWindows preference.
    455   * @returns {ClosedTabGroupStateData[]}
    456   */
    457  getClosedTabGroups: function ss_getClosedTabGroups(aOptions) {
    458    return SessionStoreInternal.getClosedTabGroups(aOptions);
    459  },
    460 
    461  /**
    462   * Get the last closed tab ID associated with a specific window
    463   *
    464   * @param {Window} aWindow
    465   */
    466  getLastClosedTabGroupId(window) {
    467    return SessionStoreInternal.getLastClosedTabGroupId(window);
    468  },
    469 
    470  /**
    471   * Re-open a closed tab
    472   *
    473   * @param {Window | object} aSource
    474   *        Either a DOMWindow or an object with properties to resolve to the window
    475   *        the tab was previously open in.
    476   * @param {string} aSource.sourceWindowId
    477            A SessionStore window id used to look up the window where the tab was closed
    478   * @param {number} aSource.sourceClosedId
    479            The closedId used to look up the closed window where the tab was closed
    480   * @param {Integer} [aIndex = 0]
    481   *        The index of the tab in the closedTabs array (via SessionStore.getClosedTabData), where 0 is most recent.
    482   * @param {Window} [aTargetWindow = aWindow] Optional window to open the tab into, defaults to current (topWindow).
    483   * @returns a reference to the reopened tab.
    484   */
    485  undoCloseTab: function ss_undoCloseTab(aSource, aIndex, aTargetWindow) {
    486    return SessionStoreInternal.undoCloseTab(aSource, aIndex, aTargetWindow);
    487  },
    488 
    489  /**
    490   * Re-open a tab from a closed window, which corresponds to the closedId
    491   *
    492   * @param {Window | object} aSource
    493   *        Either a DOMWindow or an object with properties to resolve to the window
    494   *        the tab was previously open in.
    495   * @param {string} aSource.sourceWindowId
    496            A SessionStore window id used to look up the window where the tab was closed
    497   * @param {number} aSource.sourceClosedId
    498            The closedId used to look up the closed window where the tab was closed
    499   * @param {integer} aClosedId
    500   *        The closedId of the tab or window
    501   * @param {Window} [aTargetWindow = aWindow] Optional window to open the tab into, defaults to current (topWindow).
    502   * @returns a reference to the reopened tab.
    503   */
    504  undoClosedTabFromClosedWindow: function ss_undoClosedTabFromClosedWindow(
    505    aSource,
    506    aClosedId,
    507    aTargetWindow
    508  ) {
    509    return SessionStoreInternal.undoClosedTabFromClosedWindow(
    510      aSource,
    511      aClosedId,
    512      aTargetWindow
    513    );
    514  },
    515 
    516  /**
    517   * Forget a closed tab associated with a given window
    518   * Removes the record at the given index so it cannot be un-closed or appear
    519   * in a list of recently-closed tabs
    520   *
    521   * @param {Window | object} aSource
    522   *        Either a DOMWindow or an object with properties to resolve to the window
    523   *        the tab was previously open in.
    524   * @param {string} aSource.sourceWindowId
    525            A SessionStore window id used to look up the window where the tab was closed
    526   * @param {number} aSource.sourceClosedId
    527            The closedId used to look up the closed window where the tab was closed
    528   * @param {Integer} [aIndex = 0]
    529   *        The index into the window's list of closed tabs
    530   * @throws {InvalidArgumentError} if the window is not tracked by SessionStore, or index is out of bounds
    531   */
    532  forgetClosedTab: function ss_forgetClosedTab(aSource, aIndex) {
    533    return SessionStoreInternal.forgetClosedTab(aSource, aIndex);
    534  },
    535 
    536  /**
    537   * Forget a closed tab group associated with a given window
    538   * Removes the record at the given index so it cannot be un-closed or appear
    539   * in a list of recently-closed tabs
    540   *
    541   * @param {Window | object} aSource
    542   *        Either a DOMWindow or an object with properties to resolve to the window
    543   *        the tab was previously open in.
    544   * @param {string} aSource.sourceWindowId
    545            A SessionStore window id used to look up the window where the tab group was closed
    546   * @param {number} aSource.sourceClosedId
    547            The closedId used to look up the closed window where the tab group was closed
    548   * @param {string} tabGroupId
    549   *        The tab group ID of the closed tab group
    550   * @throws {InvalidArgumentError}
    551   *        if the window or tab group is not tracked by SessionStore
    552   */
    553  forgetClosedTabGroup: function ss_forgetClosedTabGroup(aSource, tabGroupId) {
    554    return SessionStoreInternal.forgetClosedTabGroup(aSource, tabGroupId);
    555  },
    556 
    557  /**
    558   * Forget a closed tab that corresponds to the closedId
    559   * Removes the record with this closedId so it cannot be un-closed or appear
    560   * in a list of recently-closed tabs
    561   *
    562   * @param {integer} aClosedId
    563   *        The closedId of the tab
    564   * @param {Window | object} aSourceOptions
    565   *        Either a DOMWindow or an object with properties to resolve to the window
    566   *        the tab was previously open in.
    567   * @param {boolean} [aSourceOptions.includePrivate = true]
    568            If no other means of resolving a source window is given, this flag is used to
    569            constrain a search across all open window's closed tabs.
    570   * @param {string} aSourceOptions.sourceWindowId
    571            A SessionStore window id used to look up the window where the tab was closed
    572   * @param {number} aSourceOptions.sourceClosedId
    573            The closedId used to look up the closed window where the tab was closed
    574   * @throws {InvalidArgumentError} if the closedId doesnt match a closed tab in any window
    575   */
    576  forgetClosedTabById: function ss_forgetClosedTabById(
    577    aClosedId,
    578    aSourceOptions
    579  ) {
    580    SessionStoreInternal.forgetClosedTabById(aClosedId, aSourceOptions);
    581  },
    582 
    583  /**
    584   * Forget a closed window.
    585   * Removes the record with this closedId so it cannot be un-closed or appear
    586   * in a list of recently-closed windows
    587   *
    588   * @param {integer} aClosedId
    589   *        The closedId of the window
    590   * @throws {InvalidArgumentError} if the closedId doesnt match a closed window
    591   */
    592  forgetClosedWindowById: function ss_forgetClosedWindowById(aClosedId) {
    593    SessionStoreInternal.forgetClosedWindowById(aClosedId);
    594  },
    595 
    596  /**
    597   * Look up the object type ("tab" or "window") for a given closedId
    598   *
    599   * @param {integer} aClosedId
    600   */
    601  getObjectTypeForClosedId(aClosedId) {
    602    return SessionStoreInternal.getObjectTypeForClosedId(aClosedId);
    603  },
    604 
    605  /**
    606   * Look up a window tracked by SessionStore by its id
    607   *
    608   * @param {string} aSessionStoreId
    609   */
    610  getWindowById: function ss_getWindowById(aSessionStoreId) {
    611    return SessionStoreInternal.getWindowById(aSessionStoreId);
    612  },
    613 
    614  getClosedWindowCount: function ss_getClosedWindowCount() {
    615    return SessionStoreInternal.getClosedWindowCount();
    616  },
    617 
    618  // this should only be used by one caller (currently restoreLastClosedTabOrWindowOrSession in browser.js)
    619  popLastClosedAction: function ss_popLastClosedAction() {
    620    return SessionStoreInternal._lastClosedActions.pop();
    621  },
    622 
    623  // for testing purposes
    624  resetLastClosedActions: function ss_resetLastClosedActions() {
    625    SessionStoreInternal._lastClosedActions = [];
    626  },
    627 
    628  getClosedWindowData: function ss_getClosedWindowData() {
    629    return SessionStoreInternal.getClosedWindowData();
    630  },
    631 
    632  maybeDontRestoreTabs(aWindow) {
    633    SessionStoreInternal.maybeDontRestoreTabs(aWindow);
    634  },
    635 
    636  undoCloseWindow: function ss_undoCloseWindow(aIndex) {
    637    return SessionStoreInternal.undoCloseWindow(aIndex);
    638  },
    639 
    640  forgetClosedWindow: function ss_forgetClosedWindow(aIndex) {
    641    return SessionStoreInternal.forgetClosedWindow(aIndex);
    642  },
    643 
    644  getCustomWindowValue(aWindow, aKey) {
    645    return SessionStoreInternal.getCustomWindowValue(aWindow, aKey);
    646  },
    647 
    648  setCustomWindowValue(aWindow, aKey, aStringValue) {
    649    SessionStoreInternal.setCustomWindowValue(aWindow, aKey, aStringValue);
    650  },
    651 
    652  deleteCustomWindowValue(aWindow, aKey) {
    653    SessionStoreInternal.deleteCustomWindowValue(aWindow, aKey);
    654  },
    655 
    656  getCustomTabValue(aTab, aKey) {
    657    return SessionStoreInternal.getCustomTabValue(aTab, aKey);
    658  },
    659 
    660  setCustomTabValue(aTab, aKey, aStringValue) {
    661    SessionStoreInternal.setCustomTabValue(aTab, aKey, aStringValue);
    662  },
    663 
    664  deleteCustomTabValue(aTab, aKey) {
    665    SessionStoreInternal.deleteCustomTabValue(aTab, aKey);
    666  },
    667 
    668  getLazyTabValue(aTab, aKey) {
    669    return SessionStoreInternal.getLazyTabValue(aTab, aKey);
    670  },
    671 
    672  getCustomGlobalValue(aKey) {
    673    return SessionStoreInternal.getCustomGlobalValue(aKey);
    674  },
    675 
    676  setCustomGlobalValue(aKey, aStringValue) {
    677    SessionStoreInternal.setCustomGlobalValue(aKey, aStringValue);
    678  },
    679 
    680  deleteCustomGlobalValue(aKey) {
    681    SessionStoreInternal.deleteCustomGlobalValue(aKey);
    682  },
    683 
    684  restoreLastSession: function ss_restoreLastSession() {
    685    SessionStoreInternal.restoreLastSession();
    686  },
    687 
    688  speculativeConnectOnTabHover(tab) {
    689    SessionStoreInternal.speculativeConnectOnTabHover(tab);
    690  },
    691 
    692  getCurrentState(aUpdateAll) {
    693    return SessionStoreInternal.getCurrentState(aUpdateAll);
    694  },
    695 
    696  reviveCrashedTab(aTab) {
    697    return SessionStoreInternal.reviveCrashedTab(aTab);
    698  },
    699 
    700  reviveAllCrashedTabs() {
    701    return SessionStoreInternal.reviveAllCrashedTabs();
    702  },
    703 
    704  updateSessionStoreFromTablistener(
    705    aBrowser,
    706    aBrowsingContext,
    707    aPermanentKey,
    708    aData,
    709    aForStorage
    710  ) {
    711    return SessionStoreInternal.updateSessionStoreFromTablistener(
    712      aBrowser,
    713      aBrowsingContext,
    714      aPermanentKey,
    715      aData,
    716      aForStorage
    717    );
    718  },
    719 
    720  getSessionHistory(tab, updatedCallback) {
    721    return SessionStoreInternal.getSessionHistory(tab, updatedCallback);
    722  },
    723 
    724  /**
    725   * Re-open a tab or window which corresponds to the closedId
    726   *
    727   * @param {integer} aClosedId
    728   *        The closedId of the tab or window
    729   * @param {boolean} [aIncludePrivate = true]
    730   *        Whether to match the aClosedId to only closed private tabs/windows or non-private
    731   * @param {Window} [aTargetWindow]
    732   *        When aClosedId is for a closed tab, which window to re-open the tab into.
    733   *        Defaults to current (topWindow).
    734   *
    735   * @returns a tab or window object
    736   */
    737  undoCloseById(aClosedId, aIncludePrivate, aTargetWindow) {
    738    return SessionStoreInternal.undoCloseById(
    739      aClosedId,
    740      aIncludePrivate,
    741      aTargetWindow
    742    );
    743  },
    744 
    745  resetBrowserToLazyState(tab) {
    746    return SessionStoreInternal.resetBrowserToLazyState(tab);
    747  },
    748 
    749  maybeExitCrashedState(browser) {
    750    SessionStoreInternal.maybeExitCrashedState(browser);
    751  },
    752 
    753  isBrowserInCrashedSet(browser) {
    754    return SessionStoreInternal.isBrowserInCrashedSet(browser);
    755  },
    756 
    757  // this is used for testing purposes
    758  resetNextClosedId() {
    759    SessionStoreInternal._nextClosedId = 0;
    760  },
    761 
    762  /**
    763   * Ensures that session store has registered and started tracking a given window.
    764   *
    765   * @param window
    766   *        Window reference
    767   */
    768  ensureInitialized(window) {
    769    if (SessionStoreInternal._sessionInitialized && !window.__SSi) {
    770      /*
    771        We need to check that __SSi is not defined on the window so that if
    772        onLoad function is in the middle of executing we don't enter the function
    773        again and try to redeclare the ContentSessionStore script.
    774       */
    775      SessionStoreInternal.onLoad(window);
    776    }
    777  },
    778 
    779  getCurrentEpoch(browser) {
    780    return SessionStoreInternal.getCurrentEpoch(browser.permanentKey);
    781  },
    782 
    783  /**
    784   * Determines whether the passed version number is compatible with
    785   * the current version number of the SessionStore.
    786   *
    787   * @param version The format and version of the file, as an array, e.g.
    788   * ["sessionrestore", 1]
    789   */
    790  isFormatVersionCompatible(version) {
    791    if (!version) {
    792      return false;
    793    }
    794    if (!Array.isArray(version)) {
    795      // Improper format.
    796      return false;
    797    }
    798    if (version[0] != "sessionrestore") {
    799      // Not a Session Restore file.
    800      return false;
    801    }
    802    let number = Number.parseFloat(version[1]);
    803    if (Number.isNaN(number)) {
    804      return false;
    805    }
    806    return number <= FORMAT_VERSION;
    807  },
    808 
    809  /**
    810   * Filters out not worth-saving tabs from a given browser state object.
    811   *
    812   * @param aState (object)
    813   *        The browser state for which we remove worth-saving tabs.
    814   *        The given object will be modified.
    815   */
    816  keepOnlyWorthSavingTabs(aState) {
    817    let closedWindowShouldRestore = null;
    818    for (let i = aState.windows.length - 1; i >= 0; i--) {
    819      let win = aState.windows[i];
    820      for (let j = win.tabs.length - 1; j >= 0; j--) {
    821        let tab = win.tabs[j];
    822        if (!SessionStoreInternal._shouldSaveTab(tab)) {
    823          win.tabs.splice(j, 1);
    824          if (win.selected > j) {
    825            win.selected--;
    826          }
    827        }
    828      }
    829 
    830      // If it's the last window (and no closedWindow that will restore), keep the window state with no tabs.
    831      if (
    832        !win.tabs.length &&
    833        (aState.windows.length > 1 ||
    834          closedWindowShouldRestore ||
    835          (closedWindowShouldRestore == null &&
    836            (closedWindowShouldRestore = aState._closedWindows.some(
    837              w => w._shouldRestore
    838            ))))
    839      ) {
    840        aState.windows.splice(i, 1);
    841        if (aState.selectedWindow > i) {
    842          aState.selectedWindow--;
    843        }
    844      }
    845    }
    846  },
    847 
    848  /**
    849   * Clear session store data for a given private browsing window.
    850   *
    851   * @param {ChromeWindow} win - Open private browsing window to clear data for.
    852   */
    853  purgeDataForPrivateWindow(win) {
    854    return SessionStoreInternal.purgeDataForPrivateWindow(win);
    855  },
    856 
    857  /**
    858   * Add a tab group to the session's saved group list.
    859   *
    860   * @param {MozTabbrowserTabGroup} tabGroup - The group to save
    861   */
    862  addSavedTabGroup(tabGroup) {
    863    return SessionStoreInternal.addSavedTabGroup(tabGroup);
    864  },
    865 
    866  /**
    867   * Add tabs to an existing saved tab group.
    868   *
    869   * @param {string} tabGroupId - The ID of the group to save to
    870   * @param {MozTabbrowserTab[]} tabs - The list of tabs to add to the group
    871   * @param {TabMetricsContext} [metricsContext]
    872   *   Optional context to record for metrics purposes.
    873   * @returns {SavedTabGroupStateData}
    874   */
    875  addTabsToSavedGroup(tabGroupId, tabs, metricsContext) {
    876    return SessionStoreInternal.addTabsToSavedGroup(
    877      tabGroupId,
    878      tabs,
    879      metricsContext
    880    );
    881  },
    882 
    883  /**
    884   * Retrieve the tab group state of a saved tab group by ID.
    885   *
    886   * @param {string} tabGroupId
    887   * @returns {SavedTabGroupStateData|undefined}
    888   */
    889  getSavedTabGroup(tabGroupId) {
    890    return SessionStoreInternal.getSavedTabGroup(tabGroupId);
    891  },
    892 
    893  /**
    894   * Returns all tab groups that were saved in this session.
    895   *
    896   * @returns {SavedTabGroupStateData[]}
    897   */
    898  getSavedTabGroups() {
    899    return SessionStoreInternal.getSavedTabGroups();
    900  },
    901 
    902  /**
    903   * Remove a tab group from the session's saved tab group list.
    904   *
    905   * @param {string} tabGroupId
    906   *   The ID of the tab group to remove
    907   */
    908  forgetSavedTabGroup(tabGroupId) {
    909    return SessionStoreInternal.forgetSavedTabGroup(tabGroupId);
    910  },
    911 
    912  /**
    913   * Re-open a closed tab group
    914   *
    915   * @param {Window | object} source
    916   *        Either a DOMWindow or an object with properties to resolve to the window
    917   *        the tab was previously open in.
    918   * @param {string} source.sourceWindowId
    919            A SessionStore window id used to look up the window where the tab was closed.
    920   * @param {number} source.sourceClosedId
    921            The closedId used to look up the closed window where the tab was closed.
    922   * @param {string} tabGroupId
    923   *        The unique ID of the group to restore.
    924   * @param {Window} [targetWindow] defaults to the top window if not specified.
    925   * @returns {MozTabbrowserTabGroup}
    926   *   a reference to the restored tab group in a browser window.
    927   */
    928  undoCloseTabGroup(source, tabGroupId, targetWindow) {
    929    return SessionStoreInternal.undoCloseTabGroup(
    930      source,
    931      tabGroupId,
    932      targetWindow
    933    );
    934  },
    935 
    936  /**
    937   * Re-open a saved tab group.
    938   * Note that this method does not require passing a window source, as saved
    939   * tab groups are independent of windows.
    940   * Attempting to open a saved tab group in a private window will raise an error.
    941   *
    942   * @param {string} tabGroupId
    943   *        The unique ID of the group to restore.
    944   * @param {Window} [targetWindow] defaults to the top window if not specified.
    945   * @returns {MozTabbrowserTabGroup}
    946   *   a reference to the restored tab group in a browser window.
    947   */
    948  openSavedTabGroup(
    949    tabGroupId,
    950    targetWindow,
    951    { source = TabMetrics.METRIC_SOURCE.UNKNOWN } = {}
    952  ) {
    953    let isVerticalMode = targetWindow.gBrowser.tabContainer.verticalMode;
    954    Glean.tabgroup.reopen.record({
    955      id: tabGroupId,
    956      source,
    957      layout: isVerticalMode
    958        ? TabMetrics.METRIC_TABS_LAYOUT.VERTICAL
    959        : TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL,
    960      type: TabMetrics.METRIC_REOPEN_TYPE.SAVED,
    961    });
    962    if (source == TabMetrics.METRIC_SOURCE.SUGGEST) {
    963      Glean.tabgroup.groupInteractions.open_suggest.add(1);
    964    } else if (source == TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU) {
    965      Glean.tabgroup.groupInteractions.open_tabmenu.add(1);
    966    } else if (source == TabMetrics.METRIC_SOURCE.RECENT_TABS) {
    967      Glean.tabgroup.groupInteractions.open_recent.add(1);
    968    }
    969 
    970    return SessionStoreInternal.openSavedTabGroup(tabGroupId, targetWindow);
    971  },
    972 
    973  /**
    974   * Determine whether a list of tabs should be considered saveable.
    975   * A list of tabs is considered saveable if any of the tabs in the list
    976   * are worth saving.
    977   *
    978   * This is used to determine if a tab group should be saved, or if any active
    979   * tabs in a selection are eligible to be added to an existing saved group.
    980   *
    981   * @param {MozTabbrowserTab[]} tabs - the list of tabs to check
    982   * @returns {boolean} true if any of the tabs are saveable.
    983   */
    984  shouldSaveTabsToGroup(tabs) {
    985    return SessionStoreInternal.shouldSaveTabsToGroup(tabs);
    986  },
    987 
    988  /**
    989   * Convert tab state into a saved group tab state. Used to convert a
    990   * closed tab group into a saved tab group.
    991   *
    992   * @param {TabState} tabState closed tab state
    993   */
    994  formatTabStateForSavedGroup(tab) {
    995    return SessionStoreInternal._formatTabStateForSavedGroup(tab);
    996  },
    997 
    998  /**
    999   * Validates that a state object matches the schema
   1000   * defined in browser/components/sessionstore/session.schema.json
   1001   *
   1002   * @param {object} [state] State object to validate. If not provided,
   1003   *   will validate the current session state.
   1004   * @returns {Promise} A promise which resolves to a validation result object
   1005   */
   1006  validateState(state) {
   1007    return SessionStoreInternal.validateState(state);
   1008  },
   1009 };
   1010 
   1011 // Freeze the SessionStore object. We don't want anyone to modify it.
   1012 Object.freeze(SessionStore);
   1013 
   1014 /**
   1015 * @namespace SessionStoreInternal
   1016 *
   1017 * @description Internal implementations and helpers for the public SessionStore methods
   1018 */
   1019 var SessionStoreInternal = {
   1020  QueryInterface: ChromeUtils.generateQI([
   1021    "nsIObserver",
   1022    "nsISupportsWeakReference",
   1023  ]),
   1024 
   1025  _globalState: new GlobalState(),
   1026 
   1027  // A counter to be used to generate a unique ID for each closed tab or window.
   1028  _nextClosedId: 0,
   1029 
   1030  // During the initial restore and setBrowserState calls tracks the number of
   1031  // windows yet to be restored
   1032  _restoreCount: -1,
   1033 
   1034  // For each <browser> element, records the SHistoryListener.
   1035  _browserSHistoryListener: new WeakMap(),
   1036 
   1037  // Tracks the various listeners that are used throughout the restore.
   1038  _restoreListeners: new WeakMap(),
   1039 
   1040  // Records the promise created in _restoreHistory, which is used to track
   1041  // the completion of the first phase of the restore.
   1042  _tabStateRestorePromises: new WeakMap(),
   1043 
   1044  // The history data needed to be restored in the parent.
   1045  _tabStateToRestore: new WeakMap(),
   1046 
   1047  // For each <browser> element, records the current epoch.
   1048  _browserEpochs: new WeakMap(),
   1049 
   1050  // Any browsers that fires the oop-browser-crashed event gets stored in
   1051  // here - that way we know which browsers to ignore messages from (until
   1052  // they get restored).
   1053  _crashedBrowsers: new WeakSet(),
   1054 
   1055  // A map (xul:browser -> FrameLoader) that maps a browser to the last
   1056  // associated frameLoader we heard about.
   1057  _lastKnownFrameLoader: new WeakMap(),
   1058 
   1059  // A map (xul:browser -> object) that maps a browser associated with a
   1060  // recently closed tab to all its necessary state information we need to
   1061  // properly handle final update message.
   1062  _closingTabMap: new WeakMap(),
   1063 
   1064  // A map (xul:browser -> object) that maps a browser associated with a
   1065  // recently closed tab due to a window closure to the tab state information
   1066  // that is being stored in _closedWindows for that tab.
   1067  _tabClosingByWindowMap: new WeakMap(),
   1068 
   1069  // A set of window data that has the potential to be saved in the _closedWindows
   1070  // array for the session. We will remove window data from this set whenever
   1071  // forgetClosedWindow is called for the window, or when session history is
   1072  // purged, so that we don't accidentally save that data after the flush has
   1073  // completed. Closed tabs use a more complicated mechanism for this particular
   1074  // problem. When forgetClosedTab is called, the browser is removed from the
   1075  // _closingTabMap, so its data is not recorded. In the purge history case,
   1076  // the closedTabs array per window is overwritten so that once the flush is
   1077  // complete, the tab would only ever add itself to an array that SessionStore
   1078  // no longer cares about. Bug 1230636 has been filed to make the tab case
   1079  // work more like the window case, which is more explicit, and easier to
   1080  // reason about.
   1081  _saveableClosedWindowData: new WeakSet(),
   1082 
   1083  // whether a setBrowserState call is in progress
   1084  _browserSetState: false,
   1085 
   1086  // time in milliseconds when the session was started (saved across sessions),
   1087  // defaults to now if no session was restored or timestamp doesn't exist
   1088  _sessionStartTime: Date.now(),
   1089 
   1090  /**
   1091   * states for all currently opened windows
   1092   *
   1093   * @type {{[key: WindowID]: WindowStateData}}
   1094   */
   1095  _windows: {},
   1096 
   1097  // counter for creating unique window IDs
   1098  _nextWindowID: 0,
   1099 
   1100  // states for all recently closed windows
   1101  _closedWindows: [],
   1102 
   1103  /** @type {SavedTabGroupStateData[]} states for all saved+closed tab groups */
   1104  _savedGroups: [],
   1105 
   1106  // collection of session states yet to be restored
   1107  _statesToRestore: {},
   1108 
   1109  // counts the number of crashes since the last clean start
   1110  _recentCrashes: 0,
   1111 
   1112  // whether the last window was closed and should be restored
   1113  _restoreLastWindow: false,
   1114 
   1115  // whether we should restore last session on the next launch
   1116  // of a regular Firefox window. This scenario is triggered
   1117  // when a user closes all regular Firefox windows but the session is not over
   1118  _shouldRestoreLastSession: false,
   1119 
   1120  // whether we will potentially be restoring the session
   1121  // more than once without Firefox restarting in between
   1122  _restoreWithoutRestart: false,
   1123 
   1124  // number of tabs currently restoring
   1125  _tabsRestoringCount: 0,
   1126 
   1127  /**
   1128   * @typedef {object} CloseAction
   1129   * @property {string} type
   1130   *   What the close action acted upon. One of either _LAST_ACTION_CLOSED_TAB or
   1131   *   _LAST_ACTION_CLOSED_WINDOW
   1132   * @property {number} closedId
   1133   *   The unique ID of the item that closed.
   1134   */
   1135 
   1136  /**
   1137   * An in-order stack of close actions for tabs and windows.
   1138   *
   1139   * @type {CloseAction[]}
   1140   */
   1141  _lastClosedActions: [],
   1142 
   1143  /**
   1144   * Removes an object from the _lastClosedActions list
   1145   *
   1146   * @param closedAction
   1147   *        Either _LAST_ACTION_CLOSED_TAB or _LAST_ACTION_CLOSED_WINDOW
   1148   * @param {integer} closedId
   1149   *        The closedId of a tab or window
   1150   */
   1151  _removeClosedAction(closedAction, closedId) {
   1152    let closedActionIndex = this._lastClosedActions.findIndex(
   1153      obj => obj.type == closedAction && obj.closedId == closedId
   1154    );
   1155 
   1156    if (closedActionIndex > -1) {
   1157      this._lastClosedActions.splice(closedActionIndex, 1);
   1158    }
   1159  },
   1160 
   1161  /**
   1162   * Add an object to the _lastClosedActions list and truncates the list if needed
   1163   *
   1164   * @param closedAction
   1165   *        Either _LAST_ACTION_CLOSED_TAB or _LAST_ACTION_CLOSED_WINDOW
   1166   * @param {integer} closedId
   1167   *        The closedId of a tab or window
   1168   */
   1169  _addClosedAction(closedAction, closedId) {
   1170    this._lastClosedActions.push({
   1171      type: closedAction,
   1172      closedId,
   1173    });
   1174    let maxLength = this._max_tabs_undo * this._max_windows_undo;
   1175 
   1176    if (this._lastClosedActions.length > maxLength) {
   1177      this._lastClosedActions = this._lastClosedActions.slice(-maxLength);
   1178    }
   1179  },
   1180 
   1181  _LAST_ACTION_CLOSED_TAB: "tab",
   1182 
   1183  _LAST_ACTION_CLOSED_WINDOW: "window",
   1184 
   1185  _log: null,
   1186 
   1187  // When starting Firefox with a single private window or web app window, this is the place
   1188  // where we keep the session we actually wanted to restore in case the user
   1189  // decides to later open a non-private window as well.
   1190  _deferredInitialState: null,
   1191 
   1192  // Keeps track of whether a notification needs to be sent that closed objects have changed.
   1193  _closedObjectsChanged: false,
   1194 
   1195  // A promise resolved once initialization is complete
   1196  _deferredInitialized: Promise.withResolvers(),
   1197 
   1198  // Whether session has been initialized
   1199  _sessionInitialized: false,
   1200 
   1201  // A promise resolved once all windows are restored.
   1202  _deferredAllWindowsRestored: Promise.withResolvers(),
   1203 
   1204  get promiseAllWindowsRestored() {
   1205    return this._deferredAllWindowsRestored.promise;
   1206  },
   1207 
   1208  // Promise that is resolved when we're ready to initialize
   1209  // and restore the session.
   1210  _promiseReadyForInitialization: null,
   1211 
   1212  // Keep busy state counters per window.
   1213  _windowBusyStates: new WeakMap(),
   1214 
   1215  /**
   1216   * A promise fulfilled once initialization is complete.
   1217   */
   1218  get promiseInitialized() {
   1219    return this._deferredInitialized.promise;
   1220  },
   1221 
   1222  get canRestoreLastSession() {
   1223    return LastSession.canRestore;
   1224  },
   1225 
   1226  set canRestoreLastSession(val) {
   1227    // Cheat a bit; only allow false.
   1228    if (!val) {
   1229      LastSession.clear();
   1230    }
   1231  },
   1232 
   1233  /**
   1234   * Returns a string describing the last closed object, either "tab" or "window".
   1235   *
   1236   * This was added to support the sessions.restore WebExtensions API.
   1237   */
   1238  get lastClosedObjectType() {
   1239    if (this._closedWindows.length) {
   1240      // Since there are closed windows, we need to check if there's a closed tab
   1241      // in one of the currently open windows that was closed after the
   1242      // last-closed window.
   1243      let tabTimestamps = [];
   1244      for (let window of Services.wm.getEnumerator("navigator:browser")) {
   1245        let windowState = this._windows[window.__SSi];
   1246        if (windowState && windowState._closedTabs[0]) {
   1247          tabTimestamps.push(windowState._closedTabs[0].closedAt);
   1248        }
   1249      }
   1250      if (
   1251        !tabTimestamps.length ||
   1252        tabTimestamps.sort((a, b) => b - a)[0] < this._closedWindows[0].closedAt
   1253      ) {
   1254        return this._LAST_ACTION_CLOSED_WINDOW;
   1255      }
   1256    }
   1257    return this._LAST_ACTION_CLOSED_TAB;
   1258  },
   1259 
   1260  /**
   1261   * Returns a boolean that determines whether the session will be automatically
   1262   * restored upon the _next_ startup or a restart.
   1263   */
   1264  get willAutoRestore() {
   1265    return (
   1266      !PrivateBrowsingUtils.permanentPrivateBrowsing &&
   1267      (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") ||
   1268        Services.prefs.getIntPref("browser.startup.page") ==
   1269          BROWSER_STARTUP_RESUME_SESSION)
   1270    );
   1271  },
   1272 
   1273  /**
   1274   * Initialize the sessionstore service.
   1275   */
   1276  init() {
   1277    if (this._initialized) {
   1278      throw new Error("SessionStore.init() must only be called once!");
   1279    }
   1280 
   1281    TelemetryTimestamps.add("sessionRestoreInitialized");
   1282    Glean.sessionRestore.startupTimeline.sessionRestoreInitialized.set(
   1283      Services.telemetry.msSinceProcessStart()
   1284    );
   1285    OBSERVING.forEach(function (aTopic) {
   1286      Services.obs.addObserver(this, aTopic, true);
   1287    }, this);
   1288 
   1289    this._initPrefs();
   1290    this._initialized = true;
   1291 
   1292    this.promiseAllWindowsRestored.finally(() => () => {
   1293      this._log.debug("promiseAllWindowsRestored finalized");
   1294    });
   1295  },
   1296 
   1297  /**
   1298   * Initialize the session using the state provided by SessionStartup
   1299   */
   1300  initSession() {
   1301    let timerId = Glean.sessionRestore.startupInitSession.start();
   1302    let state;
   1303    let ss = lazy.SessionStartup;
   1304    let willRestore = ss.willRestore();
   1305    if (willRestore || ss.sessionType == ss.DEFER_SESSION) {
   1306      state = ss.state;
   1307    }
   1308    this._log.debug(
   1309      `initSession willRestore: ${willRestore}, SessionStartup.sessionType: ${ss.sessionType}`
   1310    );
   1311 
   1312    if (state) {
   1313      try {
   1314        // If we're doing a DEFERRED session, then we want to pull pinned tabs
   1315        // out so they can be restored, and save any open groups so they are
   1316        // available to the user.
   1317        if (ss.sessionType == ss.DEFER_SESSION) {
   1318          let [iniState, remainingState] =
   1319            this._prepDataForDeferredRestore(state);
   1320          // If we have an iniState with windows, that means that we have windows
   1321          // with pinned tabs to restore. If we have an iniState with saved
   1322          // groups, we need to preserve those in the new state.
   1323          if (iniState.windows.length || iniState.savedGroups) {
   1324            state = iniState;
   1325          } else {
   1326            state = null;
   1327          }
   1328          this._log.debug(
   1329            `initSession deferred restore with ${iniState.windows.length} initial windows, ${remainingState.windows.length} remaining windows`
   1330          );
   1331 
   1332          if (remainingState.windows.length) {
   1333            LastSession.setState(remainingState);
   1334          }
   1335          Glean.browserEngagement.sessionrestoreInterstitial.deferred_restore.add(
   1336            1
   1337          );
   1338        } else {
   1339          // Get the last deferred session in case the user still wants to
   1340          // restore it
   1341          LastSession.setState(state.lastSessionState);
   1342 
   1343          let restoreAsCrashed = ss.willRestoreAsCrashed();
   1344          if (restoreAsCrashed) {
   1345            this._recentCrashes =
   1346              ((state.session && state.session.recentCrashes) || 0) + 1;
   1347            this._log.debug(
   1348              `initSession, restoreAsCrashed, crashes: ${this._recentCrashes}`
   1349            );
   1350 
   1351            // _needsRestorePage will record sessionrestore_interstitial,
   1352            // including the specific reason we decided we needed to show
   1353            // about:sessionrestore, if that's what we do.
   1354            if (this._needsRestorePage(state, this._recentCrashes)) {
   1355              // replace the crashed session with a restore-page-only session
   1356              let url = "about:sessionrestore";
   1357              let formdata = { id: { sessionData: state }, url };
   1358              let entry = {
   1359                url,
   1360                triggeringPrincipal_base64:
   1361                  lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
   1362              };
   1363              state = { windows: [{ tabs: [{ entries: [entry], formdata }] }] };
   1364              this._log.debug("initSession, will show about:sessionrestore");
   1365            } else if (
   1366              this._hasSingleTabWithURL(state.windows, "about:welcomeback")
   1367            ) {
   1368              this._log.debug("initSession, will show about:welcomeback");
   1369              Glean.browserEngagement.sessionrestoreInterstitial.shown_only_about_welcomeback.add(
   1370                1
   1371              );
   1372              // On a single about:welcomeback URL that crashed, replace about:welcomeback
   1373              // with about:sessionrestore, to make clear to the user that we crashed.
   1374              state.windows[0].tabs[0].entries[0].url = "about:sessionrestore";
   1375              state.windows[0].tabs[0].entries[0].triggeringPrincipal_base64 =
   1376                lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
   1377            } else {
   1378              restoreAsCrashed = false;
   1379            }
   1380          }
   1381 
   1382          // If we didn't use about:sessionrestore, record that:
   1383          if (!restoreAsCrashed) {
   1384            Glean.browserEngagement.sessionrestoreInterstitial.autorestore.add(
   1385              1
   1386            );
   1387            this._log.debug("initSession, will autorestore");
   1388            this._removeExplicitlyClosedTabs(state);
   1389          }
   1390 
   1391          // Update the session start time using the restored session state.
   1392          this._updateSessionStartTime(state);
   1393 
   1394          if (state.windows.length) {
   1395            // Make sure that at least the first window doesn't have anything hidden.
   1396            delete state.windows[0].hidden;
   1397            // Since nothing is hidden in the first window, it cannot be a popup.
   1398            delete state.windows[0].isPopup;
   1399            // We don't want to minimize and then open a window at startup.
   1400            if (state.windows[0].sizemode == "minimized") {
   1401              state.windows[0].sizemode = "normal";
   1402            }
   1403          }
   1404 
   1405          // clear any lastSessionWindowID attributes since those don't matter
   1406          // during normal restore
   1407          state.windows.forEach(function (aWindow) {
   1408            delete aWindow.__lastSessionWindowID;
   1409          });
   1410        }
   1411 
   1412        // clear _maybeDontRestoreTabs because we have restored (or not)
   1413        // windows and so they don't matter
   1414        state?.windows?.forEach(win => delete win._maybeDontRestoreTabs);
   1415        state?._closedWindows?.forEach(win => delete win._maybeDontRestoreTabs);
   1416 
   1417        this._savedGroups = state?.savedGroups ?? [];
   1418      } catch (ex) {
   1419        this._log.error("The session file is invalid: ", ex);
   1420      }
   1421    }
   1422 
   1423    // at this point, we've as good as resumed the session, so we can
   1424    // clear the resume_session_once flag, if it's set
   1425    if (
   1426      !lazy.RunState.isQuitting &&
   1427      this._prefBranch.getBoolPref("sessionstore.resume_session_once")
   1428    ) {
   1429      this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
   1430    }
   1431 
   1432    Glean.sessionRestore.startupInitSession.stopAndAccumulate(timerId);
   1433    return state;
   1434  },
   1435 
   1436  /**
   1437   * When initializing session, if we are restoring the last session at startup,
   1438   * close open tabs or close windows marked _maybeDontRestoreTabs (if they were closed
   1439   * by closing remaining tabs).
   1440   * See bug 490136
   1441   */
   1442  _removeExplicitlyClosedTabs(state) {
   1443    // Don't restore tabs that has been explicitly closed
   1444    for (let i = 0; i < state.windows.length; ) {
   1445      const winData = state.windows[i];
   1446      if (winData._maybeDontRestoreTabs) {
   1447        if (state.windows.length == 1) {
   1448          // it's the last window, we just want to close tabs
   1449          let j = 0;
   1450          // reset close group (we don't want to append tabs to existing group close).
   1451          winData._lastClosedTabGroupCount = -1;
   1452          while (winData.tabs.length) {
   1453            const tabState = winData.tabs.pop();
   1454 
   1455            // Ensure the index is in bounds.
   1456            let activeIndex = (tabState.index || tabState.entries.length) - 1;
   1457            activeIndex = Math.min(activeIndex, tabState.entries.length - 1);
   1458            activeIndex = Math.max(activeIndex, 0);
   1459 
   1460            let title = "";
   1461            if (activeIndex in tabState.entries) {
   1462              title =
   1463                tabState.entries[activeIndex].title ||
   1464                tabState.entries[activeIndex].url;
   1465            }
   1466 
   1467            const tabData = {
   1468              state: tabState,
   1469              title,
   1470              image: tabState.image,
   1471              pos: j++,
   1472              closedAt: Date.now(),
   1473              closedInGroup: true,
   1474            };
   1475            if (this._shouldSaveTabState(tabState)) {
   1476              this.saveClosedTabData(winData, winData._closedTabs, tabData);
   1477            }
   1478          }
   1479        } else {
   1480          // We can remove the window since it doesn't have any
   1481          // tabs that we should restore and it's not the only window
   1482          if (winData.tabs.some(this._shouldSaveTabState)) {
   1483            winData.closedAt = Date.now();
   1484            state._closedWindows.unshift(winData);
   1485          }
   1486          state.windows.splice(i, 1);
   1487          continue; // we don't want to increment the index
   1488        }
   1489      }
   1490      i++;
   1491    }
   1492  },
   1493 
   1494  _initPrefs() {
   1495    this._prefBranch = Services.prefs.getBranch("browser.");
   1496 
   1497    gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
   1498 
   1499    Services.prefs.addObserver("browser.sessionstore.debug", () => {
   1500      gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
   1501    });
   1502 
   1503    this._log = lazy.sessionStoreLogger;
   1504 
   1505    this._max_tabs_undo = this._prefBranch.getIntPref(
   1506      "sessionstore.max_tabs_undo"
   1507    );
   1508    this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
   1509 
   1510    this._closedTabsFromAllWindowsEnabled = this._prefBranch.getBoolPref(
   1511      "sessionstore.closedTabsFromAllWindows"
   1512    );
   1513    this._prefBranch.addObserver(
   1514      "sessionstore.closedTabsFromAllWindows",
   1515      this,
   1516      true
   1517    );
   1518 
   1519    this._closedTabsFromClosedWindowsEnabled = this._prefBranch.getBoolPref(
   1520      "sessionstore.closedTabsFromClosedWindows"
   1521    );
   1522    this._prefBranch.addObserver(
   1523      "sessionstore.closedTabsFromClosedWindows",
   1524      this,
   1525      true
   1526    );
   1527 
   1528    this._max_windows_undo = this._prefBranch.getIntPref(
   1529      "sessionstore.max_windows_undo"
   1530    );
   1531    this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
   1532 
   1533    this._restore_on_demand = this._prefBranch.getBoolPref(
   1534      "sessionstore.restore_on_demand"
   1535    );
   1536    this._prefBranch.addObserver("sessionstore.restore_on_demand", this, true);
   1537  },
   1538 
   1539  /**
   1540   * Called on application shutdown, after notifications:
   1541   * quit-application-granted, quit-application
   1542   */
   1543  _uninit: function ssi_uninit() {
   1544    if (!this._initialized) {
   1545      throw new Error("SessionStore is not initialized.");
   1546    }
   1547 
   1548    // Prepare to close the session file and write the last state.
   1549    lazy.RunState.setClosing();
   1550 
   1551    // save all data for session resuming
   1552    if (this._sessionInitialized) {
   1553      lazy.SessionSaver.run();
   1554    }
   1555 
   1556    // clear out priority queue in case it's still holding refs
   1557    TabRestoreQueue.reset();
   1558 
   1559    // Make sure to cancel pending saves.
   1560    lazy.SessionSaver.cancel();
   1561  },
   1562 
   1563  /**
   1564   * Handle notifications
   1565   */
   1566  observe: function ssi_observe(aSubject, aTopic, aData) {
   1567    switch (aTopic) {
   1568      case "browser-window-before-show": // catch new windows
   1569        this.onBeforeBrowserWindowShown(aSubject);
   1570        break;
   1571      case "domwindowclosed": // catch closed windows
   1572        this.onClose(aSubject).then(() => {
   1573          this._notifyOfClosedObjectsChange();
   1574        });
   1575        if (gDebuggingEnabled) {
   1576          Services.obs.notifyObservers(null, NOTIFY_DOMWINDOWCLOSED_HANDLED);
   1577        }
   1578        break;
   1579      case "quit-application-granted": {
   1580        let syncShutdown = aData == "syncShutdown";
   1581        this.onQuitApplicationGranted(syncShutdown);
   1582        break;
   1583      }
   1584      case "browser-lastwindow-close-granted":
   1585        this.onLastWindowCloseGranted();
   1586        break;
   1587      case "quit-application":
   1588        this.onQuitApplication(aData);
   1589        break;
   1590      case "browser:purge-session-history": // catch sanitization
   1591        this.onPurgeSessionHistory();
   1592        this._notifyOfClosedObjectsChange();
   1593        break;
   1594      case "browser:purge-session-history-for-domain":
   1595        this.onPurgeDomainData(aData);
   1596        this._notifyOfClosedObjectsChange();
   1597        break;
   1598      case "nsPref:changed": // catch pref changes
   1599        this.onPrefChange(aData);
   1600        this._notifyOfClosedObjectsChange();
   1601        break;
   1602      case "idle-daily":
   1603        this.onIdleDaily();
   1604        this._notifyOfClosedObjectsChange();
   1605        break;
   1606      case "clear-origin-attributes-data": {
   1607        let userContextId = 0;
   1608        try {
   1609          userContextId = JSON.parse(aData).userContextId;
   1610        } catch (e) {}
   1611        if (userContextId) {
   1612          this._forgetTabsWithUserContextId(userContextId);
   1613        }
   1614        break;
   1615      }
   1616      case "browsing-context-did-set-embedder":
   1617        if (aSubject === aSubject.top && aSubject.isContent) {
   1618          const permanentKey = aSubject.embedderElement?.permanentKey;
   1619          if (permanentKey) {
   1620            this.maybeRecreateSHistoryListener(permanentKey, aSubject);
   1621          }
   1622        }
   1623        break;
   1624      case "browsing-context-discarded": {
   1625        let permanentKey = aSubject?.embedderElement?.permanentKey;
   1626        if (permanentKey) {
   1627          this._browserSHistoryListener.get(permanentKey)?.unregister();
   1628        }
   1629        break;
   1630      }
   1631      case "browser-shutdown-tabstate-updated":
   1632        this.onFinalTabStateUpdateComplete(aSubject);
   1633        this._notifyOfClosedObjectsChange();
   1634        break;
   1635    }
   1636  },
   1637 
   1638  getOrCreateSHistoryListener(permanentKey, browsingContext) {
   1639    if (!permanentKey || browsingContext !== browsingContext.top) {
   1640      return null;
   1641    }
   1642 
   1643    const listener = this._browserSHistoryListener.get(permanentKey);
   1644    if (listener) {
   1645      return listener;
   1646    }
   1647 
   1648    return this.createSHistoryListener(permanentKey, browsingContext, false);
   1649  },
   1650 
   1651  maybeRecreateSHistoryListener(permanentKey, browsingContext) {
   1652    const listener = this._browserSHistoryListener.get(permanentKey);
   1653    if (!listener || listener._browserId != browsingContext.browserId) {
   1654      listener?.unregister(permanentKey);
   1655      this.createSHistoryListener(permanentKey, browsingContext, true);
   1656    }
   1657  },
   1658 
   1659  createSHistoryListener(permanentKey, browsingContext, collectImmediately) {
   1660    class SHistoryListener {
   1661      constructor() {
   1662        this.QueryInterface = ChromeUtils.generateQI([
   1663          "nsISHistoryListener",
   1664          "nsISupportsWeakReference",
   1665        ]);
   1666 
   1667        this._browserId = browsingContext.browserId;
   1668        this._fromIndex = kNoIndex;
   1669      }
   1670 
   1671      unregister() {
   1672        let bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId);
   1673        bc?.sessionHistory?.removeSHistoryListener(this);
   1674        SessionStoreInternal._browserSHistoryListener.delete(permanentKey);
   1675      }
   1676 
   1677      collect(
   1678        permanentKey, // eslint-disable-line no-shadow
   1679        browsingContext, // eslint-disable-line no-shadow
   1680        { collectFull = true, writeToCache = false }
   1681      ) {
   1682        // Don't bother doing anything if we haven't seen any navigations.
   1683        if (!collectFull && this._fromIndex === kNoIndex) {
   1684          return null;
   1685        }
   1686 
   1687        let timerId = Glean.sessionRestore.collectSessionHistory.start();
   1688 
   1689        let fromIndex = collectFull ? -1 : this._fromIndex;
   1690        this._fromIndex = kNoIndex;
   1691 
   1692        let historychange = lazy.SessionHistory.collectFromParent(
   1693          browsingContext.currentURI?.spec,
   1694          true, // Bug 1704574
   1695          browsingContext.sessionHistory,
   1696          fromIndex
   1697        );
   1698 
   1699        if (writeToCache) {
   1700          let win =
   1701            browsingContext.embedderElement?.ownerGlobal ||
   1702            browsingContext.currentWindowGlobal?.browsingContext?.window;
   1703 
   1704          SessionStoreInternal.onTabStateUpdate(permanentKey, win, {
   1705            data: { historychange },
   1706          });
   1707        }
   1708 
   1709        Glean.sessionRestore.collectSessionHistory.stopAndAccumulate(timerId);
   1710 
   1711        return historychange;
   1712      }
   1713 
   1714      collectFrom(index) {
   1715        if (this._fromIndex <= index) {
   1716          // If we already know that we need to update history from index N we
   1717          // can ignore any changes that happened with an element with index
   1718          // larger than N.
   1719          //
   1720          // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which
   1721          // means we don't ignore anything here, and in case of navigation in
   1722          // the history back and forth cases we use kLastIndex which ignores
   1723          // only the subsequent navigations, but not any new elements added.
   1724          return;
   1725        }
   1726 
   1727        let bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId);
   1728        if (bc?.embedderElement?.frameLoader) {
   1729          this._fromIndex = index;
   1730 
   1731          // Queue a tab state update on the |browser.sessionstore.interval|
   1732          // timer. We'll call this.collect() when we receive the update.
   1733          bc.embedderElement.frameLoader.requestSHistoryUpdate();
   1734        }
   1735      }
   1736 
   1737      OnHistoryNewEntry(newURI, oldIndex) {
   1738        // We use oldIndex - 1 to collect the current entry as well. This makes
   1739        // sure to collect any changes that were made to the entry while the
   1740        // document was active.
   1741        this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1);
   1742      }
   1743      OnHistoryGotoIndex() {
   1744        this.collectFrom(kLastIndex);
   1745      }
   1746      OnHistoryPurge() {
   1747        this.collectFrom(-1);
   1748      }
   1749      OnHistoryReload() {
   1750        this.collectFrom(-1);
   1751        return true;
   1752      }
   1753      OnHistoryReplaceEntry() {
   1754        this.collectFrom(-1);
   1755      }
   1756    }
   1757 
   1758    let sessionHistory = browsingContext.sessionHistory;
   1759    if (!sessionHistory) {
   1760      return null;
   1761    }
   1762 
   1763    const listener = new SHistoryListener();
   1764    sessionHistory.addSHistoryListener(listener);
   1765    this._browserSHistoryListener.set(permanentKey, listener);
   1766 
   1767    let isAboutBlank = browsingContext.currentURI?.spec === "about:blank";
   1768 
   1769    if (collectImmediately && (!isAboutBlank || sessionHistory.count !== 0)) {
   1770      listener.collect(permanentKey, browsingContext, { writeToCache: true });
   1771    }
   1772 
   1773    return listener;
   1774  },
   1775 
   1776  onTabStateUpdate(permanentKey, win, update) {
   1777    // Ignore messages from <browser> elements that have crashed
   1778    // and not yet been revived.
   1779    if (this._crashedBrowsers.has(permanentKey)) {
   1780      return;
   1781    }
   1782 
   1783    lazy.TabState.update(permanentKey, update);
   1784    this.saveStateDelayed(win);
   1785 
   1786    // Handle any updates sent by the child after the tab was closed. This
   1787    // might be the final update as sent by the "unload" handler but also
   1788    // any async update message that was sent before the child unloaded.
   1789    let closedTab = this._closingTabMap.get(permanentKey);
   1790    if (closedTab) {
   1791      // Update the closed tab's state. This will be reflected in its
   1792      // window's list of closed tabs as that refers to the same object.
   1793      lazy.TabState.copyFromCache(permanentKey, closedTab.tabData.state);
   1794    }
   1795  },
   1796 
   1797  onFinalTabStateUpdateComplete(browser) {
   1798    let permanentKey = browser.permanentKey;
   1799    if (
   1800      this._closingTabMap.has(permanentKey) &&
   1801      !this._crashedBrowsers.has(permanentKey)
   1802    ) {
   1803      let { winData, closedTabs, tabData } =
   1804        this._closingTabMap.get(permanentKey);
   1805 
   1806      // We expect no further updates.
   1807      this._closingTabMap.delete(permanentKey);
   1808 
   1809      // The tab state no longer needs this reference.
   1810      delete tabData.permanentKey;
   1811 
   1812      // Determine whether the tab state is worth saving.
   1813      let shouldSave = this._shouldSaveTabState(tabData.state);
   1814      let index = closedTabs.indexOf(tabData);
   1815 
   1816      if (shouldSave && index == -1) {
   1817        // If the tab state is worth saving and we didn't push it onto
   1818        // the list of closed tabs when it was closed (because we deemed
   1819        // the state not worth saving) then add it to the window's list
   1820        // of closed tabs now.
   1821        this.saveClosedTabData(winData, closedTabs, tabData);
   1822      } else if (!shouldSave && index > -1) {
   1823        // Remove from the list of closed tabs. The update messages sent
   1824        // after the tab was closed changed enough state so that we no
   1825        // longer consider its data interesting enough to keep around.
   1826        this.removeClosedTabData(winData, closedTabs, index);
   1827      }
   1828 
   1829      this._cleanupOrphanedClosedGroups(winData);
   1830    }
   1831 
   1832    // If this the final message we need to resolve all pending flush
   1833    // requests for the given browser as they might have been sent too
   1834    // late and will never respond. If they have been sent shortly after
   1835    // switching a browser's remoteness there isn't too much data to skip.
   1836    lazy.TabStateFlusher.resolveAll(browser);
   1837 
   1838    this._browserSHistoryListener.get(permanentKey)?.unregister();
   1839    this._restoreListeners.get(permanentKey)?.unregister();
   1840 
   1841    Services.obs.notifyObservers(browser, NOTIFY_BROWSER_SHUTDOWN_FLUSH);
   1842  },
   1843 
   1844  updateSessionStoreFromTablistener(
   1845    browser,
   1846    browsingContext,
   1847    permanentKey,
   1848    update,
   1849    forStorage = false
   1850  ) {
   1851    permanentKey = browser?.permanentKey ?? permanentKey;
   1852    if (!permanentKey) {
   1853      return;
   1854    }
   1855 
   1856    // Ignore sessionStore update from previous epochs
   1857    if (!this.isCurrentEpoch(permanentKey, update.epoch)) {
   1858      return;
   1859    }
   1860 
   1861    if (browsingContext.isReplaced) {
   1862      return;
   1863    }
   1864 
   1865    let listener = this.getOrCreateSHistoryListener(
   1866      permanentKey,
   1867      browsingContext
   1868    );
   1869 
   1870    if (listener) {
   1871      let historychange =
   1872        // If it is not the scheduled update (tab closed, window closed etc),
   1873        // try to store the loading non-web-controlled page opened in _blank
   1874        // first.
   1875        (forStorage &&
   1876          lazy.SessionHistory.collectNonWebControlledBlankLoadingSession(
   1877            browsingContext
   1878          )) ||
   1879        listener.collect(permanentKey, browsingContext, {
   1880          collectFull: !!update.sHistoryNeeded,
   1881          writeToCache: false,
   1882        });
   1883 
   1884      if (historychange) {
   1885        update.data.historychange = historychange;
   1886      }
   1887    }
   1888 
   1889    let win =
   1890      browser?.ownerGlobal ??
   1891      browsingContext.currentWindowGlobal?.browsingContext?.window;
   1892 
   1893    this.onTabStateUpdate(permanentKey, win, update);
   1894  },
   1895 
   1896  /* ........ Window Event Handlers .............. */
   1897 
   1898  /**
   1899   * Implement EventListener for handling various window and tab events
   1900   */
   1901  handleEvent: function ssi_handleEvent(aEvent) {
   1902    let win = aEvent.currentTarget.ownerGlobal;
   1903    let target = aEvent.originalTarget;
   1904    switch (aEvent.type) {
   1905      case "TabOpen":
   1906        this.onTabAdd(win);
   1907        if (aEvent.detail.adoptedTab) {
   1908          this.moveCustomTabValue(aEvent.detail.adoptedTab, target);
   1909        }
   1910        break;
   1911      case "TabBrowserInserted":
   1912        this.onTabBrowserInserted(win, target);
   1913        break;
   1914      case "TabClose":
   1915        // `adoptedBy` will be set if the tab was closed because it is being
   1916        // moved to a new window.
   1917        if (aEvent.detail.adoptedBy) {
   1918          this.moveCustomTabValue(target, aEvent.detail.adoptedBy);
   1919          this.onMoveToNewWindow(
   1920            target.linkedBrowser,
   1921            aEvent.detail.adoptedBy.linkedBrowser
   1922          );
   1923        } else if (!aEvent.detail.skipSessionStore) {
   1924          // `skipSessionStore` is set by tab close callers to indicate that we
   1925          // shouldn't record the closed tab.
   1926          this.onTabClose(win, target);
   1927        }
   1928        this.onTabRemove(win, target);
   1929        this._notifyOfClosedObjectsChange();
   1930        break;
   1931      case "TabSelect":
   1932        this.onTabSelect(win);
   1933        break;
   1934      case "TabShow":
   1935        this.onTabShow(win, target);
   1936        break;
   1937      case "TabHide":
   1938        this.onTabHide(win, target);
   1939        break;
   1940      case "TabPinned":
   1941      case "TabUnpinned":
   1942      case "SwapDocShells":
   1943        this.saveStateDelayed(win);
   1944        break;
   1945      case "TabGroupCreate":
   1946      case "TabGroupRemoved":
   1947      case "TabGrouped":
   1948      case "TabUngrouped":
   1949      case "TabGroupCollapse":
   1950      case "TabGroupExpand":
   1951        this.saveStateDelayed(win);
   1952        break;
   1953      case "TabGroupRemoveRequested":
   1954        if (!aEvent.detail?.skipSessionStore) {
   1955          this.onTabGroupRemoveRequested(win, target);
   1956          this._notifyOfClosedObjectsChange();
   1957        }
   1958        break;
   1959      case "TabSplitViewActivate":
   1960        for (const tab of aEvent.detail.tabs) {
   1961          this.maybeRestoreTabContent(tab);
   1962        }
   1963        break;
   1964      case "oop-browser-crashed":
   1965      case "oop-browser-buildid-mismatch":
   1966        if (aEvent.isTopFrame) {
   1967          this.onBrowserCrashed(target);
   1968        }
   1969        break;
   1970      case "XULFrameLoaderCreated":
   1971        if (
   1972          target.namespaceURI == XUL_NS &&
   1973          target.localName == "browser" &&
   1974          target.frameLoader &&
   1975          target.permanentKey
   1976        ) {
   1977          this._lastKnownFrameLoader.set(
   1978            target.permanentKey,
   1979            target.frameLoader
   1980          );
   1981          this.resetEpoch(target.permanentKey, target.frameLoader);
   1982        }
   1983        break;
   1984      default:
   1985        throw new Error(`unhandled event ${aEvent.type}?`);
   1986    }
   1987    this._clearRestoringWindows();
   1988  },
   1989 
   1990  /**
   1991   * Generate a unique window identifier
   1992   *
   1993   * @return string
   1994   *         A unique string to identify a window
   1995   */
   1996  _generateWindowID: function ssi_generateWindowID() {
   1997    return "window" + this._nextWindowID++;
   1998  },
   1999 
   2000  /**
   2001   * Registers and tracks a given window.
   2002   *
   2003   * @param aWindow
   2004   *        Window reference
   2005   */
   2006  onLoad(aWindow) {
   2007    // return if window has already been initialized
   2008    if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi]) {
   2009      return;
   2010    }
   2011 
   2012    // ignore windows opened while shutting down
   2013    if (lazy.RunState.isQuitting) {
   2014      return;
   2015    }
   2016 
   2017    // Assign the window a unique identifier we can use to reference
   2018    // internal data about the window.
   2019    aWindow.__SSi = this._generateWindowID();
   2020 
   2021    // and create its data object
   2022    this._windows[aWindow.__SSi] = {
   2023      tabs: [],
   2024      groups: [],
   2025      closedGroups: [],
   2026      selected: 0,
   2027      _closedTabs: [],
   2028      // NOTE: this naming refers to the number of tabs in a *multiselection*, not in a tab group.
   2029      // This naming was chosen before the introduction of tab groups proper.
   2030      // TODO: choose more distinct naming in bug1928424
   2031      _lastClosedTabGroupCount: -1,
   2032      lastClosedTabGroupId: null,
   2033      busy: false,
   2034      chromeFlags: aWindow.docShell.treeOwner
   2035        .QueryInterface(Ci.nsIInterfaceRequestor)
   2036        .getInterface(Ci.nsIAppWindow).chromeFlags,
   2037    };
   2038 
   2039    if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
   2040      this._windows[aWindow.__SSi].isPrivate = true;
   2041    }
   2042    if (!this._isWindowLoaded(aWindow)) {
   2043      this._windows[aWindow.__SSi]._restoring = true;
   2044    }
   2045    if (!aWindow.toolbar.visible) {
   2046      this._windows[aWindow.__SSi].isPopup = true;
   2047    }
   2048 
   2049    if (aWindow.document.documentElement.hasAttribute("taskbartab")) {
   2050      this._windows[aWindow.__SSi].isTaskbarTab = true;
   2051    }
   2052 
   2053    if (lazy.AIWindow.isAIWindowActiveAndEnabled(aWindow)) {
   2054      this._windows[aWindow.__SSi].isAIWindow = true;
   2055    }
   2056 
   2057    let tabbrowser = aWindow.gBrowser;
   2058 
   2059    // add tab change listeners to all already existing tabs
   2060    for (let i = 0; i < tabbrowser.tabs.length; i++) {
   2061      this.onTabBrowserInserted(aWindow, tabbrowser.tabs[i]);
   2062    }
   2063    // notification of tab add/remove/selection/show/hide
   2064    TAB_EVENTS.forEach(function (aEvent) {
   2065      tabbrowser.tabContainer.addEventListener(aEvent, this, true);
   2066    }, this);
   2067 
   2068    // Keep track of a browser's latest frameLoader.
   2069    aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this);
   2070  },
   2071 
   2072  /**
   2073   * Initializes a given window.
   2074   *
   2075   * Windows are registered as soon as they are created but we need to wait for
   2076   * the session file to load, and the initial window's delayed startup to
   2077   * finish before initializing a window, i.e. restoring data into it.
   2078   *
   2079   * @param aWindow
   2080   *        Window reference
   2081   * @param aInitialState
   2082   *        The initial state to be loaded after startup (optional)
   2083   */
   2084  initializeWindow(aWindow, aInitialState = null) {
   2085    let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
   2086    let isTaskbarTab = this._windows[aWindow.__SSi].isTaskbarTab;
   2087    // A regular window is not a private window, taskbar tab window, or popup window
   2088    let isRegularWindow =
   2089      !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible;
   2090 
   2091    // perform additional initialization when the first window is loading
   2092    if (lazy.RunState.isStopped) {
   2093      lazy.RunState.setRunning();
   2094 
   2095      // restore a crashed session resp. resume the last session if requested
   2096      if (aInitialState) {
   2097        // Don't write to disk right after startup. Set the last time we wrote
   2098        // to disk to NOW() to enforce a full interval before the next write.
   2099        lazy.SessionSaver.updateLastSaveTime();
   2100 
   2101        if (isPrivateWindow || isTaskbarTab) {
   2102          this._log.debug(
   2103            "initializeWindow, the window is private or a web app. Saving SessionStartup.state for possibly restoring later"
   2104          );
   2105          // We're starting with a single private window. Save the state we
   2106          // actually wanted to restore so that we can do it later in case
   2107          // the user opens another, non-private window.
   2108          this._deferredInitialState = lazy.SessionStartup.state;
   2109 
   2110          // Nothing to restore now, notify observers things are complete.
   2111          Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
   2112          Services.obs.notifyObservers(
   2113            null,
   2114            "sessionstore-one-or-no-tab-restored"
   2115          );
   2116          this._deferredAllWindowsRestored.resolve();
   2117        } else {
   2118          TelemetryTimestamps.add("sessionRestoreRestoring");
   2119          Glean.sessionRestore.startupTimeline.sessionRestoreRestoring.set(
   2120            Services.telemetry.msSinceProcessStart()
   2121          );
   2122          this._restoreCount = aInitialState.windows
   2123            ? aInitialState.windows.length
   2124            : 0;
   2125 
   2126          // global data must be restored before restoreWindow is called so that
   2127          // it happens before observers are notified
   2128          this._globalState.setFromState(aInitialState);
   2129 
   2130          // Restore session cookies before loading any tabs.
   2131          lazy.SessionCookies.restore(aInitialState.cookies || []);
   2132 
   2133          let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
   2134          let options = { firstWindow: true, overwriteTabs: overwrite };
   2135          this.restoreWindows(aWindow, aInitialState, options);
   2136        }
   2137      } else {
   2138        // Nothing to restore, notify observers things are complete.
   2139        Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
   2140        Services.obs.notifyObservers(
   2141          null,
   2142          "sessionstore-one-or-no-tab-restored"
   2143        );
   2144        this._deferredAllWindowsRestored.resolve();
   2145      }
   2146      // this window was opened by _openWindowWithState
   2147    } else if (!this._isWindowLoaded(aWindow)) {
   2148      // We want to restore windows after all windows have opened (since bug
   2149      // 1034036), so bail out here.
   2150      return;
   2151      // The user opened another window that is not a popup, private window, or web app,
   2152      // after starting up with a single private or web app window.
   2153      // Let's restore the session we actually wanted to restore at startup.
   2154    } else if (this._deferredInitialState && isRegularWindow) {
   2155      // Only restore the deferred session if SessionStartup indicates we should
   2156      // restore (e.g., crash recovery or user preference to restore sessions).
   2157      // This prevents incorrect session restoration when a private window was
   2158      // opened first followed by a normal window. See Bug 1938752.
   2159      if (lazy.SessionStartup.willRestore()) {
   2160        // global data must be restored before restoreWindow is called so that
   2161        // it happens before observers are notified
   2162        this._globalState.setFromState(this._deferredInitialState);
   2163        this._restoreCount = this._deferredInitialState.windows
   2164          ? this._deferredInitialState.windows.length
   2165          : 0;
   2166        this.restoreWindows(aWindow, this._deferredInitialState, {
   2167          firstWindow: true,
   2168        });
   2169      }
   2170      this._deferredInitialState = null;
   2171    } else if (
   2172      this._restoreLastWindow &&
   2173      aWindow.toolbar.visible &&
   2174      this._closedWindows.length &&
   2175      !isPrivateWindow
   2176    ) {
   2177      // default to the most-recently closed window
   2178      // don't use popup windows
   2179      let closedWindowState = null;
   2180      let closedWindowIndex;
   2181      for (let i = 0; i < this._closedWindows.length; i++) {
   2182        // Take the first non-popup, point our object at it, and break out.
   2183        if (!this._closedWindows[i].isPopup) {
   2184          closedWindowState = this._closedWindows[i];
   2185          closedWindowIndex = i;
   2186          break;
   2187        }
   2188      }
   2189 
   2190      if (closedWindowState) {
   2191        let newWindowState;
   2192        if (
   2193          AppConstants.platform == "macosx" ||
   2194          !lazy.SessionStartup.willRestore()
   2195        ) {
   2196          // We want to split the window up into pinned tabs and unpinned tabs.
   2197          // Pinned tabs should be restored. If there are any remaining tabs,
   2198          // they should be added back to _closedWindows.
   2199          // We'll cheat a little bit and reuse _prepDataForDeferredRestore
   2200          // even though it wasn't built exactly for this.
   2201          let [appTabsState, normalTabsState] =
   2202            this._prepDataForDeferredRestore({
   2203              windows: [closedWindowState],
   2204            });
   2205 
   2206          // These are our pinned tabs and sidebar attributes, which we should restore
   2207          if (appTabsState.windows.length) {
   2208            newWindowState = appTabsState.windows[0];
   2209            delete newWindowState.__lastSessionWindowID;
   2210          }
   2211 
   2212          // In case there were no unpinned tabs, remove the window from _closedWindows
   2213          if (!normalTabsState.windows.length) {
   2214            this._removeClosedWindow(closedWindowIndex);
   2215            // Or update _closedWindows with the modified state
   2216          } else {
   2217            delete normalTabsState.windows[0].__lastSessionWindowID;
   2218            this._closedWindows[closedWindowIndex] = normalTabsState.windows[0];
   2219          }
   2220        } else {
   2221          // If we're just restoring the window, make sure it gets removed from
   2222          // _closedWindows.
   2223          this._removeClosedWindow(closedWindowIndex);
   2224          newWindowState = closedWindowState;
   2225          delete newWindowState.hidden;
   2226        }
   2227 
   2228        if (newWindowState) {
   2229          // Ensure that the window state isn't hidden
   2230          this._restoreCount = 1;
   2231          let state = { windows: [newWindowState] };
   2232          let options = { overwriteTabs: this._isCmdLineEmpty(aWindow, state) };
   2233          this.restoreWindow(aWindow, newWindowState, options);
   2234        }
   2235      }
   2236      // we actually restored the session just now.
   2237      this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
   2238    }
   2239    // This is a taskbar-tab specific scenario. If an user closes
   2240    // all regular Firefox windows except for taskbar tabs and has
   2241    // auto restore on startup enabled, _shouldRestoreLastSession
   2242    // will be set to true. We should then restore when a
   2243    // regular Firefox window is opened.
   2244    else if (
   2245      Services.prefs.getBoolPref("browser.taskbarTabs.enabled", false) &&
   2246      this._shouldRestoreLastSession &&
   2247      isRegularWindow
   2248    ) {
   2249      let lastSessionState = LastSession.getState();
   2250      this._globalState.setFromState(lastSessionState);
   2251      lazy.SessionCookies.restore(lastSessionState.cookies || []);
   2252      this.restoreWindows(aWindow, lastSessionState, {
   2253        firstWindow: true,
   2254      });
   2255      this._shouldRestoreLastSession = false;
   2256    }
   2257 
   2258    if (this._restoreLastWindow && aWindow.toolbar.visible) {
   2259      // always reset (if not a popup window)
   2260      // we don't want to restore a window directly after, for example,
   2261      // undoCloseWindow was executed.
   2262      this._restoreLastWindow = false;
   2263    }
   2264  },
   2265 
   2266  /**
   2267   * Called right before a new browser window is shown.
   2268   *
   2269   * @param aWindow
   2270   *        Window reference
   2271   */
   2272  onBeforeBrowserWindowShown(aWindow) {
   2273    // Do not track Document Picture-in-Picture windows since these are
   2274    // ephemeral and tied to a specific tab's browser document.
   2275    if (aWindow.browsingContext.isDocumentPiP) {
   2276      return;
   2277    }
   2278 
   2279    // Register the window.
   2280    this.onLoad(aWindow);
   2281 
   2282    // Some are waiting for this window to be shown, which is now, so let's resolve
   2283    // the deferred operation.
   2284    let deferred = WINDOW_SHOWING_PROMISES.get(aWindow);
   2285    if (deferred) {
   2286      deferred.resolve(aWindow);
   2287      WINDOW_SHOWING_PROMISES.delete(aWindow);
   2288    }
   2289 
   2290    // Just call initializeWindow() directly if we're initialized already.
   2291    if (this._sessionInitialized) {
   2292      this._log.debug(
   2293        "onBeforeBrowserWindowShown, session already initialized, initializing window"
   2294      );
   2295      this.initializeWindow(aWindow);
   2296      return;
   2297    }
   2298 
   2299    // The very first window that is opened creates a promise that is then
   2300    // re-used by all subsequent windows. The promise will be used to tell
   2301    // when we're ready for initialization.
   2302    if (!this._promiseReadyForInitialization) {
   2303      // Wait for the given window's delayed startup to be finished.
   2304      let promise = new Promise(resolve => {
   2305        Services.obs.addObserver(function obs(subject, topic) {
   2306          if (aWindow == subject) {
   2307            Services.obs.removeObserver(obs, topic);
   2308            resolve();
   2309          }
   2310        }, "browser-delayed-startup-finished");
   2311      });
   2312 
   2313      // We are ready for initialization as soon as the session file has been
   2314      // read from disk and the initial window's delayed startup has finished.
   2315      this._promiseReadyForInitialization = Promise.all([
   2316        promise,
   2317        lazy.SessionStartup.onceInitialized,
   2318      ]);
   2319    }
   2320 
   2321    // We can't call this.onLoad since initialization
   2322    // hasn't completed, so we'll wait until it is done.
   2323    // Even if additional windows are opened and wait
   2324    // for initialization as well, the first opened
   2325    // window should execute first, and this.onLoad
   2326    // will be called with the initialState.
   2327    this._promiseReadyForInitialization
   2328      .then(() => {
   2329        if (aWindow.closed) {
   2330          this._log.debug(
   2331            "When _promiseReadyForInitialization resolved, the window was closed"
   2332          );
   2333          return;
   2334        }
   2335 
   2336        if (this._sessionInitialized) {
   2337          this.initializeWindow(aWindow);
   2338        } else {
   2339          let initialState = this.initSession();
   2340          this._sessionInitialized = true;
   2341 
   2342          if (initialState) {
   2343            Services.obs.notifyObservers(null, NOTIFY_RESTORING_ON_STARTUP);
   2344          }
   2345          let timerId = Glean.sessionRestore.startupOnloadInitialWindow.start();
   2346          this.initializeWindow(aWindow, initialState);
   2347          Glean.sessionRestore.startupOnloadInitialWindow.stopAndAccumulate(
   2348            timerId
   2349          );
   2350 
   2351          // Let everyone know we're done.
   2352          this._deferredInitialized.resolve();
   2353        }
   2354      })
   2355      .catch(ex => {
   2356        this._log.error(
   2357          "Exception when handling _promiseReadyForInitialization resolution:",
   2358          ex
   2359        );
   2360      });
   2361  },
   2362 
   2363  /**
   2364   * On window close...
   2365   * - remove event listeners from tabs
   2366   * - save all window data
   2367   *
   2368   * @param aWindow
   2369   *        Window reference
   2370   *
   2371   * @returns a Promise
   2372   */
   2373  onClose: function ssi_onClose(aWindow) {
   2374    let completionPromise = Promise.resolve();
   2375    // this window was about to be restored - conserve its original data, if any
   2376    let isFullyLoaded = this._isWindowLoaded(aWindow);
   2377    if (!isFullyLoaded) {
   2378      if (!aWindow.__SSi) {
   2379        aWindow.__SSi = this._generateWindowID();
   2380      }
   2381 
   2382      let restoreID = WINDOW_RESTORE_IDS.get(aWindow);
   2383      this._windows[aWindow.__SSi] =
   2384        this._statesToRestore[restoreID].windows[0];
   2385      delete this._statesToRestore[restoreID];
   2386      WINDOW_RESTORE_IDS.delete(aWindow);
   2387    }
   2388 
   2389    // ignore windows not tracked by SessionStore
   2390    if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
   2391      return completionPromise;
   2392    }
   2393 
   2394    // notify that the session store will stop tracking this window so that
   2395    // extensions can store any data about this window in session store before
   2396    // that's not possible anymore
   2397    let event = aWindow.document.createEvent("Events");
   2398    event.initEvent("SSWindowClosing", true, false);
   2399    aWindow.dispatchEvent(event);
   2400 
   2401    if (this.windowToFocus && this.windowToFocus == aWindow) {
   2402      delete this.windowToFocus;
   2403    }
   2404 
   2405    var tabbrowser = aWindow.gBrowser;
   2406 
   2407    let browsers = Array.from(tabbrowser.browsers);
   2408 
   2409    TAB_EVENTS.forEach(function (aEvent) {
   2410      tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
   2411    }, this);
   2412 
   2413    aWindow.gBrowser.removeEventListener("XULFrameLoaderCreated", this);
   2414 
   2415    let winData = this._windows[aWindow.__SSi];
   2416 
   2417    // Collect window data only when *not* closed during shutdown.
   2418    if (lazy.RunState.isRunning) {
   2419      // Grab the most recent window data. The tab data will be updated
   2420      // once we finish flushing all of the messages from the tabs.
   2421      let tabMap = this._collectWindowData(aWindow);
   2422 
   2423      for (let [tab, tabData] of tabMap) {
   2424        let permanentKey = tab.linkedBrowser.permanentKey;
   2425        this._tabClosingByWindowMap.set(permanentKey, tabData);
   2426      }
   2427 
   2428      if (isFullyLoaded && !winData.title) {
   2429        winData.title =
   2430          tabbrowser.selectedBrowser.contentTitle ||
   2431          tabbrowser.selectedTab.label;
   2432      }
   2433 
   2434      if (AppConstants.platform != "macosx") {
   2435        // Until we decide otherwise elsewhere, this window is part of a series
   2436        // of closing windows to quit.
   2437        winData._shouldRestore = true;
   2438      }
   2439 
   2440      // Store the window's close date to figure out when each individual tab
   2441      // was closed. This timestamp should allow re-arranging data based on how
   2442      // recently something was closed.
   2443      winData.closedAt = Date.now();
   2444 
   2445      // we don't want to save the busy state
   2446      delete winData.busy;
   2447 
   2448      // When closing windows one after the other until Firefox quits, we
   2449      // will move those closed in series back to the "open windows" bucket
   2450      // before writing to disk. If however there is only a single window
   2451      // with tabs we deem not worth saving then we might end up with a
   2452      // random closed or even a pop-up window re-opened. To prevent that
   2453      // we explicitly allow saving an "empty" window state.
   2454      let isLastWindow = this.isLastRestorableWindow();
   2455 
   2456      let isLastRegularWindow =
   2457        Object.values(this._windows).filter(
   2458          wData => !wData.isPrivate && !wData.isTaskbarTab
   2459        ).length == 1;
   2460      this._log.debug(
   2461        `onClose, closing window isLastRegularWindow? ${isLastRegularWindow}`
   2462      );
   2463 
   2464      let taskbarTabsRemains = Object.values(this._windows).some(
   2465        wData => wData.isTaskbarTab
   2466      );
   2467 
   2468      // Closing the last regular Firefox window with
   2469      // at least one taskbar tab window still active.
   2470      // The session is considered over and we need to restore
   2471      // the next time a non-private, non-taskbar-tab window
   2472      // is opened.
   2473      if (
   2474        Services.prefs.getBoolPref("browser.taskbarTabs.enabled", false) &&
   2475        isLastRegularWindow &&
   2476        !winData.isTaskbarTab &&
   2477        !winData.isPrivate &&
   2478        taskbarTabsRemains
   2479      ) {
   2480        // If the setting is enabled, Firefox should auto-restore
   2481        // the next time a regular window is opened
   2482        if (this.willAutoRestore) {
   2483          this._shouldRestoreLastSession = true;
   2484          // Otherwise, we want "restore last session" button
   2485          // to be avaliable in the hamburger menu
   2486        } else {
   2487          Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_RE_ENABLED);
   2488        }
   2489 
   2490        let savedState = this.getCurrentState(true);
   2491        lazy.PrivacyFilter.filterPrivateWindowsAndTabs(savedState);
   2492        LastSession.setState(savedState);
   2493        this._restoreWithoutRestart = true;
   2494      }
   2495 
   2496      // clear this window from the list, since it has definitely been closed.
   2497      delete this._windows[aWindow.__SSi];
   2498 
   2499      // This window has the potential to be saved in the _closedWindows
   2500      // array (maybeSaveClosedWindows gets the final call on that).
   2501      this._saveableClosedWindowData.add(winData);
   2502 
   2503      // Now we have to figure out if this window is worth saving in the _closedWindows
   2504      // Object.
   2505      //
   2506      // We're about to flush the tabs from this window, but it's possible that we
   2507      // might never hear back from the content process(es) in time before the user
   2508      // chooses to restore the closed window. So we do the following:
   2509      //
   2510      // 1) Use the tab state cache to determine synchronously if the window is
   2511      //    worth stashing in _closedWindows.
   2512      // 2) Flush the window.
   2513      // 3) When the flush is complete, revisit our decision to store the window
   2514      //    in _closedWindows, and add/remove as necessary.
   2515      if (!winData.isPrivate && !winData.isTaskbarTab) {
   2516        this.maybeSaveClosedWindow(winData, isLastWindow);
   2517      }
   2518 
   2519      completionPromise = lazy.TabStateFlusher.flushWindow(aWindow).then(() => {
   2520        // At this point, aWindow is closed! You should probably not try to
   2521        // access any DOM elements from aWindow within this callback unless
   2522        // you're holding on to them in the closure.
   2523 
   2524        WINDOW_FLUSHING_PROMISES.delete(aWindow);
   2525 
   2526        for (let browser of browsers) {
   2527          if (this._tabClosingByWindowMap.has(browser.permanentKey)) {
   2528            let tabData = this._tabClosingByWindowMap.get(browser.permanentKey);
   2529            lazy.TabState.copyFromCache(browser.permanentKey, tabData);
   2530            this._tabClosingByWindowMap.delete(browser.permanentKey);
   2531          }
   2532        }
   2533 
   2534        // Save non-private windows if they have at
   2535        // least one saveable tab or are the last window.
   2536        if (!winData.isPrivate && !winData.isTaskbarTab) {
   2537          this.maybeSaveClosedWindow(winData, isLastWindow);
   2538 
   2539          if (!isLastWindow && winData.closedId > -1) {
   2540            this._addClosedAction(
   2541              this._LAST_ACTION_CLOSED_WINDOW,
   2542              winData.closedId
   2543            );
   2544          }
   2545        }
   2546 
   2547        // Update the tabs data now that we've got the most
   2548        // recent information.
   2549        this.cleanUpWindow(aWindow, winData, browsers);
   2550 
   2551        // save the state without this window to disk
   2552        this.saveStateDelayed();
   2553      });
   2554 
   2555      // Here we might override a flush already in flight, but that's fine
   2556      // because `completionPromise` will always resolve after the old flush
   2557      // resolves.
   2558      WINDOW_FLUSHING_PROMISES.set(aWindow, completionPromise);
   2559    } else {
   2560      this.cleanUpWindow(aWindow, winData, browsers);
   2561    }
   2562 
   2563    for (let i = 0; i < tabbrowser.tabs.length; i++) {
   2564      this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
   2565    }
   2566 
   2567    return completionPromise;
   2568  },
   2569 
   2570  /**
   2571   * Clean up the message listeners on a window that has finally
   2572   * gone away. Call this once you're sure you don't want to hear
   2573   * from any of this windows tabs from here forward.
   2574   *
   2575   * @param aWindow
   2576   *        The browser window we're cleaning up.
   2577   * @param winData
   2578   *        The data for the window that we should hold in the
   2579   *        DyingWindowCache in case anybody is still holding a
   2580   *        reference to it.
   2581   */
   2582  cleanUpWindow(aWindow, winData, browsers) {
   2583    // Any leftover TabStateFlusher Promises need to be resolved now,
   2584    // since we're about to remove the message listeners.
   2585    for (let browser of browsers) {
   2586      lazy.TabStateFlusher.resolveAll(browser);
   2587    }
   2588 
   2589    // Cache the window state until it is completely gone.
   2590    DyingWindowCache.set(aWindow, winData);
   2591 
   2592    this._saveableClosedWindowData.delete(winData);
   2593    delete aWindow.__SSi;
   2594  },
   2595 
   2596  /**
   2597   * Decides whether or not a closed window should be put into the
   2598   * _closedWindows Object. This might be called multiple times per
   2599   * window, and will do the right thing of moving the window data
   2600   * in or out of _closedWindows if the winData indicates that our
   2601   * need for saving it has changed.
   2602   *
   2603   * @param winData
   2604   *        The data for the closed window that we might save.
   2605   * @param isLastWindow
   2606   *        Whether or not the window being closed is the last
   2607   *        browser window. Callers of this function should pass
   2608   *        in the value of SessionStoreInternal.atLastWindow for
   2609   *        this argument, and pass in the same value if they happen
   2610   *        to call this method again asynchronously (for example, after
   2611   *        a window flush).
   2612   */
   2613  maybeSaveClosedWindow(winData, isLastWindow) {
   2614    // Make sure SessionStore is still running, and make sure that we
   2615    // haven't chosen to forget this window.
   2616    if (
   2617      lazy.RunState.isRunning &&
   2618      this._saveableClosedWindowData.has(winData)
   2619    ) {
   2620      // Determine whether the window has any tabs worth saving.
   2621      // Note: We currently ignore the possibility of useful _closedTabs here.
   2622      // A window with 0 worth-keeping open tabs will not have its state saved, and
   2623      // any _closedTabs will be lost.
   2624      let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState);
   2625 
   2626      // Note that we might already have this window stored in
   2627      // _closedWindows from a previous call to this function.
   2628      let winIndex = this._closedWindows.indexOf(winData);
   2629      let alreadyStored = winIndex != -1;
   2630      // If sidebar command is truthy, i.e. sidebar is open, store sidebar settings
   2631      let shouldStore = hasSaveableTabs || isLastWindow;
   2632 
   2633      if (shouldStore && !alreadyStored) {
   2634        let index = this._closedWindows.findIndex(win => {
   2635          return win.closedAt < winData.closedAt;
   2636        });
   2637 
   2638        // If we found no window closed before our
   2639        // window then just append it to the list.
   2640        if (index == -1) {
   2641          index = this._closedWindows.length;
   2642        }
   2643 
   2644        // About to save the closed window, add a unique ID.
   2645        winData.closedId = this._nextClosedId++;
   2646 
   2647        // Insert winData at the right position.
   2648        this._closedWindows.splice(index, 0, winData);
   2649        this._capClosedWindows();
   2650        this._saveOpenTabGroupsOnClose(winData);
   2651        this._closedObjectsChanged = true;
   2652        this._log.debug(
   2653          `Saved closed window:${winData.closedId} with ${winData.tabs.length} open tabs, ${winData._closedTabs.length} closed tabs`
   2654        );
   2655 
   2656        // The first time we close a window, ensure it can be restored from the
   2657        // hidden window.
   2658        if (
   2659          AppConstants.platform == "macosx" &&
   2660          this._closedWindows.length == 1
   2661        ) {
   2662          // Fake a popupshowing event so shortcuts work:
   2663          let window = Services.appShell.hiddenDOMWindow;
   2664          let historyMenu = window.document.getElementById("history-menu");
   2665          let evt = new window.CustomEvent("popupshowing", { bubbles: true });
   2666          historyMenu.menupopup.dispatchEvent(evt);
   2667        }
   2668      } else if (!shouldStore) {
   2669        if (
   2670          winData._closedTabs.length &&
   2671          this._closedTabsFromAllWindowsEnabled
   2672        ) {
   2673          // we are going to lose closed tabs, so any observers should be notified
   2674          this._closedObjectsChanged = true;
   2675        }
   2676        if (alreadyStored) {
   2677          this._removeClosedWindow(winIndex);
   2678          return;
   2679        }
   2680        this._log.warn(
   2681          `Discarding window:${winData.closedId} with 0 saveable tabs and ${winData._closedTabs.length} closed tabs`
   2682        );
   2683      }
   2684    }
   2685  },
   2686 
   2687  /**
   2688   * If there are any open tab groups in this closing window, move those
   2689   * tab groups to the list of saved tab groups so that the user doesn't
   2690   * lose them.
   2691   *
   2692   * The normal API for saving a tab group is `this.addSavedTabGroup`.
   2693   * `this.addSavedTabGroup` relies on a MozTabbrowserTabGroup DOM element
   2694   * and relies on passing the tab group's MozTabbrowserTab DOM elements to
   2695   * `this.maybeSaveClosedTab`. Since this method might be dealing with a closed
   2696   * window that has no DOM, this method has a separate but similar
   2697   * implementation to `this.addSavedTabGroup` and `this.maybeSaveClosedTab`.
   2698   *
   2699   * @param {WindowStateData} closedWinData
   2700   * @returns {void}
   2701   */
   2702  _saveOpenTabGroupsOnClose(closedWinData) {
   2703    /** @type Map<string, SavedTabGroupStateData> */
   2704    let newlySavedTabGroups = new Map();
   2705    // Convert any open tab groups into saved tab groups in place
   2706    closedWinData.groups = closedWinData.groups.map(tabGroupState =>
   2707      lazy.TabGroupState.savedInClosedWindow(
   2708        tabGroupState,
   2709        closedWinData.closedId
   2710      )
   2711    );
   2712    for (let tabGroupState of closedWinData.groups) {
   2713      if (!tabGroupState.saveOnWindowClose) {
   2714        continue;
   2715      }
   2716      newlySavedTabGroups.set(tabGroupState.id, tabGroupState);
   2717    }
   2718    for (let tIndex = 0; tIndex < closedWinData.tabs.length; tIndex++) {
   2719      let tabState = closedWinData.tabs[tIndex];
   2720      if (!tabState.groupId) {
   2721        continue;
   2722      }
   2723      if (!newlySavedTabGroups.has(tabState.groupId)) {
   2724        continue;
   2725      }
   2726 
   2727      if (this._shouldSaveTabState(tabState)) {
   2728        let tabData = this._formatTabStateForSavedGroup(tabState);
   2729        if (!tabData) {
   2730          continue;
   2731        }
   2732        newlySavedTabGroups.get(tabState.groupId).tabs.push(tabData);
   2733      }
   2734    }
   2735 
   2736    // Add saved tab group references to saved tab group state.
   2737    for (let tabGroupToSave of newlySavedTabGroups.values()) {
   2738      this._recordSavedTabGroupState(tabGroupToSave);
   2739    }
   2740  },
   2741 
   2742  /**
   2743   * Convert tab state into a saved group tab state. Used to convert a
   2744   * closed tab group into a saved tab group.
   2745   *
   2746   * @param {TabState} tabState closed tab state
   2747   */
   2748  _formatTabStateForSavedGroup(tabState) {
   2749    // Ensure the index is in bounds.
   2750    let activeIndex = tabState.index;
   2751    activeIndex = Math.min(activeIndex, tabState.entries.length - 1);
   2752    activeIndex = Math.max(activeIndex, 0);
   2753    if (!(activeIndex in tabState.entries)) {
   2754      return {};
   2755    }
   2756    let title =
   2757      tabState.entries[activeIndex].title || tabState.entries[activeIndex].url;
   2758    return {
   2759      state: tabState,
   2760      title,
   2761      image: tabState.image,
   2762      pos: tabState.pos,
   2763      closedAt: Date.now(),
   2764      closedId: this._nextClosedId++,
   2765    };
   2766  },
   2767 
   2768  /**
   2769   * On quit application granted
   2770   */
   2771  onQuitApplicationGranted: function ssi_onQuitApplicationGranted(
   2772    syncShutdown = false
   2773  ) {
   2774    // Collect an initial snapshot of window data before we do the flush.
   2775    let index = 0;
   2776    for (let window of this._orderedBrowserWindows) {
   2777      this._collectWindowData(window);
   2778      this._windows[window.__SSi].zIndex = ++index;
   2779    }
   2780    this._log.debug(
   2781      `onQuitApplicationGranted, shutdown of ${index} windows will be sync? ${syncShutdown}`
   2782    );
   2783    this._log.debug(
   2784      `Last session save attempt: ${Date.now() - lazy.SessionSaver.lastSaveTime}ms ago`
   2785    );
   2786 
   2787    // Now add an AsyncShutdown blocker that'll spin the event loop
   2788    // until the windows have all been flushed.
   2789 
   2790    // This progress object will track the state of async window flushing
   2791    // and will help us debug things that go wrong with our AsyncShutdown
   2792    // blocker.
   2793    let progress = { total: -1, current: -1 };
   2794 
   2795    // We're going down! Switch state so that we treat closing windows and
   2796    // tabs correctly.
   2797    lazy.RunState.setQuitting();
   2798 
   2799    if (!syncShutdown) {
   2800      // We've got some time to shut down, so let's do this properly that there
   2801      // will be a complete session available upon next startup.
   2802      // We use our own timer and spin the event loop ourselves, as we do not
   2803      // want to crash on timeout and as we need to run in response to
   2804      // "quit-application-granted", which is not yet a real shutdown phase.
   2805      //
   2806      // We end spinning once:
   2807      // 1. the flush duration exceeds 10 seconds before DELAY_CRASH_MS, or
   2808      // 2. 'oop-frameloader-crashed' (issued by BrowserParent::ActorDestroy
   2809      //    on abnormal frame shutdown) is observed, or
   2810      // 3. 'ipc:content-shutdown' (issued by ContentParent::ActorDestroy on
   2811      //    abnormal shutdown) is observed, or
   2812      // 4. flushAllWindowsAsync completes (hopefully the normal case).
   2813 
   2814      Glean.sessionRestore.shutdownType.async.add(1);
   2815 
   2816      // Set up the list of promises that will signal a complete sessionstore
   2817      // shutdown: either all data is saved, or we crashed or the message IPC
   2818      // channel went away in the meantime.
   2819      let promises = [this.flushAllWindowsAsync(progress)];
   2820 
   2821      const observeTopic = topic => {
   2822        let deferred = Promise.withResolvers();
   2823        const observer = subject => {
   2824          // Skip abort on ipc:content-shutdown if not abnormal/crashed
   2825          subject.QueryInterface(Ci.nsIPropertyBag2);
   2826          switch (topic) {
   2827            case "ipc:content-shutdown":
   2828              if (subject.get("abnormal")) {
   2829                this._log.debug(
   2830                  "Observed abnormal ipc:content-shutdown during shutdown"
   2831                );
   2832                Glean.sessionRestore.shutdownFlushAllOutcomes.abnormal_content_shutdown.add(
   2833                  1
   2834                );
   2835                deferred.resolve();
   2836              }
   2837              break;
   2838            case "oop-frameloader-crashed":
   2839              this._log.debug(`Observed topic: ${topic} during shutdown`);
   2840              Glean.sessionRestore.shutdownFlushAllOutcomes.oop_frameloader_crashed.add(
   2841                1
   2842              );
   2843              deferred.resolve();
   2844              break;
   2845          }
   2846        };
   2847        const cleanup = () => {
   2848          try {
   2849            Services.obs.removeObserver(observer, topic);
   2850          } catch (ex) {
   2851            this._log.error("Exception whilst flushing all windows", ex);
   2852          }
   2853        };
   2854        Services.obs.addObserver(observer, topic);
   2855        deferred.promise.then(cleanup, cleanup);
   2856        return deferred;
   2857      };
   2858 
   2859      // Build a list of deferred executions that require cleanup once the
   2860      // Promise race is won.
   2861      // Ensure that the timer fires earlier than the AsyncShutdown crash timer.
   2862      let waitTimeMaxMs = Math.max(
   2863        0,
   2864        lazy.AsyncShutdown.DELAY_CRASH_MS - 10000
   2865      );
   2866      let defers = [
   2867        this.looseTimer(waitTimeMaxMs),
   2868 
   2869        // FIXME: We should not be aborting *all* flushes when a single
   2870        // content process crashes here.
   2871        observeTopic("oop-frameloader-crashed"),
   2872        observeTopic("ipc:content-shutdown"),
   2873      ];
   2874      // Add these monitors to the list of Promises to start the race.
   2875      promises.push(...defers.map(deferred => deferred.promise));
   2876 
   2877      let isDone = false;
   2878      Promise.race(promises)
   2879        .then(() => {
   2880          // When a Promise won the race, make sure we clean up the running
   2881          // monitors.
   2882          defers.forEach(deferred => deferred.reject());
   2883        })
   2884        .finally(() => {
   2885          isDone = true;
   2886        });
   2887      Services.tm.spinEventLoopUntil(
   2888        "Wait until SessionStoreInternal.flushAllWindowsAsync finishes.",
   2889        () => isDone
   2890      );
   2891    } else {
   2892      Glean.sessionRestore.shutdownType.sync.add(1);
   2893      // We have to shut down NOW, which means we only get to save whatever
   2894      // we already had cached.
   2895    }
   2896  },
   2897 
   2898  /**
   2899   * An async Task that iterates all open browser windows and flushes
   2900   * any outstanding messages from their tabs. This will also close
   2901   * all of the currently open windows while we wait for the flushes
   2902   * to complete.
   2903   *
   2904   * @param progress (Object)
   2905   *        Optional progress object that will be updated as async
   2906   *        window flushing progresses. flushAllWindowsSync will
   2907   *        write to the following properties:
   2908   *
   2909   *        total (int):
   2910   *          The total number of windows to be flushed.
   2911   *        current (int):
   2912   *          The current window that we're waiting for a flush on.
   2913   *
   2914   * @return Promise
   2915   */
   2916  async flushAllWindowsAsync(progress = {}) {
   2917    let windowPromises = new Map(WINDOW_FLUSHING_PROMISES);
   2918    WINDOW_FLUSHING_PROMISES.clear();
   2919 
   2920    // We collect flush promises and close each window immediately so that
   2921    // the user can't start changing any window state while we're waiting
   2922    // for the flushes to finish.
   2923    for (let window of this._browserWindows) {
   2924      windowPromises.set(window, lazy.TabStateFlusher.flushWindow(window));
   2925 
   2926      // We have to wait for these messages to come up from
   2927      // each window and each browser. In the meantime, hide
   2928      // the windows to improve perceived shutdown speed.
   2929      let baseWin = window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
   2930      baseWin.visibility = false;
   2931    }
   2932 
   2933    progress.total = windowPromises.size;
   2934    progress.current = 0;
   2935 
   2936    // We'll iterate through the Promise array, yielding each one, so as to
   2937    // provide useful progress information to AsyncShutdown.
   2938    for (let [win, promise] of windowPromises) {
   2939      await promise;
   2940 
   2941      // We may have already stopped tracking this window in onClose, which is
   2942      // fine as we would've collected window data there as well.
   2943      if (win.__SSi && this._windows[win.__SSi]) {
   2944        this._collectWindowData(win);
   2945      }
   2946 
   2947      progress.current++;
   2948    }
   2949 
   2950    // We must cache this because _getTopWindow will always
   2951    // return null by the time quit-application occurs.
   2952    var activeWindow = this._getTopWindow();
   2953    if (activeWindow) {
   2954      this.activeWindowSSiCache = activeWindow.__SSi || "";
   2955    }
   2956    DirtyWindows.clear();
   2957    Glean.sessionRestore.shutdownFlushAllOutcomes.complete.add(1);
   2958  },
   2959 
   2960  /**
   2961   * On last browser window close
   2962   */
   2963  onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() {
   2964    // last browser window is quitting.
   2965    // remember to restore the last window when another browser window is opened
   2966    // do not account for pref(resume_session_once) at this point, as it might be
   2967    // set by another observer getting this notice after us
   2968    this._restoreLastWindow = true;
   2969  },
   2970 
   2971  /**
   2972   * On quitting application
   2973   *
   2974   * @param aData
   2975   *        String type of quitting
   2976   */
   2977  onQuitApplication: function ssi_onQuitApplication(aData) {
   2978    if (aData == "restart" || aData == "os-restart") {
   2979      if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
   2980        if (
   2981          aData == "os-restart" &&
   2982          !this._prefBranch.getBoolPref("sessionstore.resume_session_once")
   2983        ) {
   2984          this._prefBranch.setBoolPref(
   2985            "sessionstore.resuming_after_os_restart",
   2986            true
   2987          );
   2988        }
   2989        this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
   2990      }
   2991 
   2992      // The browser:purge-session-history notification fires after the
   2993      // quit-application notification so unregister the
   2994      // browser:purge-session-history notification to prevent clearing
   2995      // session data on disk on a restart.  It is also unnecessary to
   2996      // perform any other sanitization processing on a restart as the
   2997      // browser is about to exit anyway.
   2998      Services.obs.removeObserver(this, "browser:purge-session-history");
   2999    }
   3000 
   3001    if (aData != "restart") {
   3002      // Throw away the previous session on shutdown without notification
   3003      LastSession.clear(true);
   3004    }
   3005 
   3006    this._uninit();
   3007  },
   3008 
   3009  /**
   3010   * Clear session store data for a given private browsing window.
   3011   *
   3012   * @param {ChromeWindow} win - Open private browsing window to clear data for.
   3013   */
   3014  purgeDataForPrivateWindow(win) {
   3015    // No need to clear data if already shutting down.
   3016    if (lazy.RunState.isQuitting) {
   3017      return;
   3018    }
   3019 
   3020    // Check if we have data for the given window.
   3021    let windowData = this._windows[win.__SSi];
   3022    if (!windowData) {
   3023      return;
   3024    }
   3025 
   3026    // Clear closed tab data.
   3027    if (windowData._closedTabs.length) {
   3028      // Remove all of the closed tabs data.
   3029      // This also clears out the permenentKey-mapped data for pending state updates
   3030      // and removes the tabs from from the _lastClosedActions list
   3031      while (windowData._closedTabs.length) {
   3032        this.removeClosedTabData(windowData, windowData._closedTabs, 0);
   3033      }
   3034      // Reset the closed tab list.
   3035      windowData._closedTabs = [];
   3036      windowData._lastClosedTabGroupCount = -1;
   3037      windowData.lastClosedTabGroupId = null;
   3038      this._closedObjectsChanged = true;
   3039    }
   3040 
   3041    // Clear closed tab groups
   3042    if (windowData.closedGroups.length) {
   3043      for (let closedGroup of windowData.closedGroups) {
   3044        while (closedGroup.tabs.length) {
   3045          this.removeClosedTabData(windowData, closedGroup.tabs, 0);
   3046        }
   3047      }
   3048      windowData.closedGroups = [];
   3049      this._closedObjectsChanged = true;
   3050    }
   3051  },
   3052 
   3053  /**
   3054   * On purge of session history
   3055   */
   3056  onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
   3057    lazy.SessionFile.wipe();
   3058    // If the browser is shutting down, simply return after clearing the
   3059    // session data on disk as this notification fires after the
   3060    // quit-application notification so the browser is about to exit.
   3061    if (lazy.RunState.isQuitting) {
   3062      return;
   3063    }
   3064    LastSession.clear();
   3065 
   3066    let openWindows = {};
   3067    // Collect open windows.
   3068    for (let window of this._browserWindows) {
   3069      openWindows[window.__SSi] = true;
   3070    }
   3071 
   3072    // also clear all data about closed tabs and windows
   3073    for (let ix in this._windows) {
   3074      if (ix in openWindows) {
   3075        if (this._windows[ix]._closedTabs.length) {
   3076          this._windows[ix]._closedTabs = [];
   3077          this._closedObjectsChanged = true;
   3078        }
   3079        if (this._windows[ix].closedGroups.length) {
   3080          this._windows[ix].closedGroups = [];
   3081          this._closedObjectsChanged = true;
   3082        }
   3083      } else {
   3084        delete this._windows[ix];
   3085      }
   3086    }
   3087    // also clear all data about closed windows
   3088    if (this._closedWindows.length) {
   3089      this._closedWindows = [];
   3090      this._closedObjectsChanged = true;
   3091    }
   3092    // give the tabbrowsers a chance to clear their histories first
   3093    var win = this._getTopWindow();
   3094    if (win) {
   3095      win.setTimeout(() => lazy.SessionSaver.run(), 0);
   3096    } else if (lazy.RunState.isRunning) {
   3097      lazy.SessionSaver.run();
   3098    }
   3099 
   3100    this._clearRestoringWindows();
   3101    this._saveableClosedWindowData = new WeakSet();
   3102    this._lastClosedActions = [];
   3103  },
   3104 
   3105  /**
   3106   * On purge of domain data
   3107   *
   3108   * @param {string} aDomain
   3109   *        The domain we want to purge data for
   3110   */
   3111  onPurgeDomainData: function ssi_onPurgeDomainData(aDomain) {
   3112    // does a session history entry contain a url for the given domain?
   3113    function containsDomain(aEntry) {
   3114      let host;
   3115      try {
   3116        host = Services.io.newURI(aEntry.url).host;
   3117      } catch (e) {
   3118        // The given URL probably doesn't have a host.
   3119      }
   3120      if (host && Services.eTLD.hasRootDomain(host, aDomain)) {
   3121        return true;
   3122      }
   3123      return aEntry.children && aEntry.children.some(containsDomain, this);
   3124    }
   3125    // remove all closed tabs containing a reference to the given domain
   3126    for (let ix in this._windows) {
   3127      let closedTabsLists = [
   3128        this._windows[ix]._closedTabs,
   3129        ...this._windows[ix].closedGroups.map(g => g.tabs),
   3130      ];
   3131 
   3132      for (let closedTabs of closedTabsLists) {
   3133        for (let i = closedTabs.length - 1; i >= 0; i--) {
   3134          if (closedTabs[i].state.entries.some(containsDomain, this)) {
   3135            closedTabs.splice(i, 1);
   3136            this._closedObjectsChanged = true;
   3137          }
   3138        }
   3139      }
   3140    }
   3141    // remove all open & closed tabs containing a reference to the given
   3142    // domain in closed windows
   3143    for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) {
   3144      let closedTabsLists = [
   3145        this._closedWindows[ix]._closedTabs,
   3146        ...this._closedWindows[ix].closedGroups.map(g => g.tabs),
   3147      ];
   3148      let openTabs = this._closedWindows[ix].tabs;
   3149      let openTabCount = openTabs.length;
   3150 
   3151      for (let closedTabs of closedTabsLists) {
   3152        for (let i = closedTabs.length - 1; i >= 0; i--) {
   3153          if (closedTabs[i].state.entries.some(containsDomain, this)) {
   3154            closedTabs.splice(i, 1);
   3155          }
   3156        }
   3157      }
   3158      for (let j = openTabs.length - 1; j >= 0; j--) {
   3159        if (openTabs[j].entries.some(containsDomain, this)) {
   3160          openTabs.splice(j, 1);
   3161          if (this._closedWindows[ix].selected > j) {
   3162            this._closedWindows[ix].selected--;
   3163          }
   3164        }
   3165      }
   3166      if (!openTabs.length) {
   3167        this._closedWindows.splice(ix, 1);
   3168      } else if (openTabs.length != openTabCount) {
   3169        // Adjust the window's title if we removed an open tab
   3170        let selectedTab = openTabs[this._closedWindows[ix].selected - 1];
   3171        // some duplication from restoreHistory - make sure we get the correct title
   3172        let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1;
   3173        if (activeIndex >= selectedTab.entries.length) {
   3174          activeIndex = selectedTab.entries.length - 1;
   3175        }
   3176        this._closedWindows[ix].title = selectedTab.entries[activeIndex].title;
   3177      }
   3178    }
   3179 
   3180    if (lazy.RunState.isRunning) {
   3181      lazy.SessionSaver.run();
   3182    }
   3183 
   3184    this._clearRestoringWindows();
   3185  },
   3186 
   3187  /**
   3188   * On preference change
   3189   *
   3190   * @param aData
   3191   *        String preference changed
   3192   */
   3193  onPrefChange: function ssi_onPrefChange(aData) {
   3194    switch (aData) {
   3195      // if the user decreases the max number of closed tabs they want
   3196      // preserved update our internal states to match that max
   3197      case "sessionstore.max_tabs_undo":
   3198        this._max_tabs_undo = this._prefBranch.getIntPref(
   3199          "sessionstore.max_tabs_undo"
   3200        );
   3201        for (let ix in this._windows) {
   3202          if (this._windows[ix]._closedTabs.length > this._max_tabs_undo) {
   3203            this._windows[ix]._closedTabs.splice(
   3204              this._max_tabs_undo,
   3205              this._windows[ix]._closedTabs.length
   3206            );
   3207            this._closedObjectsChanged = true;
   3208          }
   3209        }
   3210        break;
   3211      case "sessionstore.max_windows_undo":
   3212        this._max_windows_undo = this._prefBranch.getIntPref(
   3213          "sessionstore.max_windows_undo"
   3214        );
   3215        this._capClosedWindows();
   3216        break;
   3217      case "sessionstore.restore_on_demand":
   3218        this._restore_on_demand = this._prefBranch.getBoolPref(
   3219          "sessionstore.restore_on_demand"
   3220        );
   3221        break;
   3222      case "sessionstore.closedTabsFromAllWindows":
   3223        this._closedTabsFromAllWindowsEnabled = this._prefBranch.getBoolPref(
   3224          "sessionstore.closedTabsFromAllWindows"
   3225        );
   3226        this._closedObjectsChanged = true;
   3227        break;
   3228      case "sessionstore.closedTabsFromClosedWindows":
   3229        this._closedTabsFromClosedWindowsEnabled = this._prefBranch.getBoolPref(
   3230          "sessionstore.closedTabsFromClosedWindows"
   3231        );
   3232        this._closedObjectsChanged = true;
   3233        break;
   3234    }
   3235  },
   3236 
   3237  /**
   3238   * save state when new tab is added
   3239   *
   3240   * @param aWindow
   3241   *        Window reference
   3242   */
   3243  onTabAdd: function ssi_onTabAdd(aWindow) {
   3244    this.saveStateDelayed(aWindow);
   3245  },
   3246 
   3247  /**
   3248   * set up listeners for a new tab
   3249   *
   3250   * @param aWindow
   3251   *        Window reference
   3252   * @param aTab
   3253   *        Tab reference
   3254   */
   3255  onTabBrowserInserted: function ssi_onTabBrowserInserted(aWindow, aTab) {
   3256    let browser = aTab.linkedBrowser;
   3257    browser.addEventListener("SwapDocShells", this);
   3258    browser.addEventListener("oop-browser-crashed", this);
   3259    browser.addEventListener("oop-browser-buildid-mismatch", this);
   3260 
   3261    if (browser.frameLoader) {
   3262      this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
   3263    }
   3264 
   3265    // Only restore if browser has been lazy.
   3266    if (
   3267      TAB_LAZY_STATES.has(aTab) &&
   3268      !TAB_STATE_FOR_BROWSER.has(browser) &&
   3269      lazy.TabStateCache.get(browser.permanentKey)
   3270    ) {
   3271      let tabState = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
   3272      this.restoreTab(aTab, tabState);
   3273    }
   3274 
   3275    // The browser has been inserted now, so lazy data is no longer relevant.
   3276    TAB_LAZY_STATES.delete(aTab);
   3277  },
   3278 
   3279  /**
   3280   * remove listeners for a tab
   3281   *
   3282   * @param aWindow
   3283   *        Window reference
   3284   * @param aTab
   3285   *        Tab reference
   3286   * @param aNoNotification
   3287   *        bool Do not save state if we're updating an existing tab
   3288   */
   3289  onTabRemove: function ssi_onTabRemove(aWindow, aTab, aNoNotification) {
   3290    this.cleanUpRemovedBrowser(aTab);
   3291 
   3292    if (!aNoNotification) {
   3293      this.saveStateDelayed(aWindow);
   3294    }
   3295  },
   3296 
   3297  /**
   3298   * When a tab closes, collect its properties
   3299   *
   3300   * @param {Window} aWindow
   3301   *        Window reference
   3302   * @param {MozTabbrowserTab} aTab
   3303   *        Tab reference
   3304   */
   3305  onTabClose: function ssi_onTabClose(aWindow, aTab) {
   3306    // don't update our internal state if we don't have to
   3307    if (this._max_tabs_undo == 0) {
   3308      return;
   3309    }
   3310 
   3311    // Get the latest data for this tab (generally, from the cache)
   3312    let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
   3313 
   3314    // Store closed-tab data for undo.
   3315    this.maybeSaveClosedTab(aWindow, aTab, tabState);
   3316  },
   3317 
   3318  onTabGroupRemoveRequested: function ssi_onTabGroupRemoveRequested(
   3319    win,
   3320    tabGroup
   3321  ) {
   3322    // don't update our internal state if we don't have to
   3323    if (this._max_tabs_undo == 0) {
   3324      return;
   3325    }
   3326 
   3327    if (this.getSavedTabGroup(tabGroup.id)) {
   3328      // If a tab group is being removed from the tab strip but it's already
   3329      // saved, then this is a "save and close" action; the saved tab group
   3330      // should be stored in global session state rather than in this window's
   3331      // closed tab groups.
   3332      return;
   3333    }
   3334 
   3335    let closedGroups = this._windows[win.__SSi].closedGroups;
   3336    let tabGroupState = lazy.TabGroupState.closed(tabGroup, win.__SSi);
   3337    tabGroupState.tabs = this._collectClosedTabsForTabGroup(tabGroup.tabs, win);
   3338 
   3339    // TODO(jswinarton) it's unclear if updating lastClosedTabGroupCount is
   3340    // necessary when restoring tab groups — it largely depends on how we
   3341    // decide to do the restore.
   3342    // To address in bug1915174
   3343    this._windows[win.__SSi]._lastClosedTabGroupCount =
   3344      tabGroupState.tabs.length;
   3345    closedGroups.unshift(tabGroupState);
   3346    this._closedObjectsChanged = true;
   3347  },
   3348 
   3349  /**
   3350   * Collect closed tab states for a tab group that is about to be
   3351   * saved and/or closed.
   3352   *
   3353   * The `TabGroupState` module is generally responsible for collecting
   3354   * tab group state data, but the session store has additional requirements
   3355   * for closed tabs that are currently only implemented in
   3356   * `SessionStoreInternal.maybeSaveClosedTab`. This method converts the tabs
   3357   * in a tab group into the closed tab data schema format required for
   3358   * closed or saved groups.
   3359   *
   3360   * @param {MozTabbrowserTab[]} tabs
   3361   * @param {Window} win
   3362   * @param {object} [options]
   3363   * @param {string} [options.updateTabGroupId]
   3364   *         Manually set a tab group id on the the tab state for each tab.
   3365   *         This is mainly used to add closing tabs to pre-existing
   3366   *         saved groups.
   3367   * @returns {ClosedTabStateData[]}
   3368   */
   3369  _collectClosedTabsForTabGroup(tabs, win, { updateTabGroupId } = {}) {
   3370    let closedTabs = [];
   3371    tabs.forEach(tab => {
   3372      let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
   3373      if (updateTabGroupId) {
   3374        tabState.groupId = updateTabGroupId;
   3375      }
   3376      this.maybeSaveClosedTab(win, tab, tabState, {
   3377        closedTabsArray: closedTabs,
   3378        closedInTabGroup: true,
   3379      });
   3380    });
   3381    return closedTabs;
   3382  },
   3383 
   3384  /**
   3385   * Flush and copy tab state when moving a tab to a new window.
   3386   *
   3387   * @param aFromBrowser
   3388   *        Browser reference.
   3389   * @param aToBrowser
   3390   *        Browser reference.
   3391   */
   3392  onMoveToNewWindow(aFromBrowser, aToBrowser) {
   3393    lazy.TabStateFlusher.flush(aFromBrowser).then(() => {
   3394      let tabState = lazy.TabStateCache.get(aFromBrowser.permanentKey);
   3395      if (!tabState) {
   3396        throw new Error(
   3397          "Unexpected undefined tabState for onMoveToNewWindow aFromBrowser"
   3398        );
   3399      }
   3400      lazy.TabStateCache.update(aToBrowser.permanentKey, tabState);
   3401    });
   3402  },
   3403 
   3404  /**
   3405   * Save a closed tab if needed.
   3406   *
   3407   * @param {Window} aWindow
   3408   *        Window reference.
   3409   * @param {MozTabbrowserTab} aTab
   3410   *        Tab reference.
   3411   * @param {TabStateData} tabState
   3412   *        Tab state.
   3413   * @param {object} [options]
   3414   * @param {TabStateData[]} [options.closedTabsArray]
   3415   *        The array of closed tabs to save to. This could be a
   3416   *        window's _closedTabs array or the tab list of a
   3417   *        closed tab group.
   3418   * @param {boolean} [options.closedInTabGroup=false]
   3419   *        If this tab was closed due to the closing of a tab group.
   3420   */
   3421  maybeSaveClosedTab(
   3422    aWindow,
   3423    aTab,
   3424    tabState,
   3425    { closedTabsArray, closedInTabGroup = false } = {}
   3426  ) {
   3427    // Don't save private tabs
   3428    let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
   3429    if (!isPrivateWindow && tabState.isPrivate) {
   3430      return;
   3431    }
   3432    if (aTab == aWindow.FirefoxViewHandler.tab) {
   3433      return;
   3434    }
   3435 
   3436    let permanentKey = aTab.linkedBrowser.permanentKey;
   3437 
   3438    let tabData = {
   3439      permanentKey,
   3440      state: tabState,
   3441      title: aTab.label,
   3442      image: aWindow.gBrowser.getIcon(aTab),
   3443      pos: aTab._tPos,
   3444      closedAt: Date.now(),
   3445      closedInGroup: aTab._closedInMultiselection,
   3446      closedInTabGroupId: closedInTabGroup ? tabState.groupId : null,
   3447      sourceWindowId: aWindow.__SSi,
   3448    };
   3449 
   3450    let winData = this._windows[aWindow.__SSi];
   3451    let closedTabs = closedTabsArray || winData._closedTabs;
   3452 
   3453    // Determine whether the tab contains any information worth saving. Note
   3454    // that there might be pending state changes queued in the child that
   3455    // didn't reach the parent yet. If a tab is emptied before closing then we
   3456    // might still remove it from the list of closed tabs later.
   3457    if (this._shouldSaveTabState(tabState)) {
   3458      // Save the tab state, for now. We might push a valid tab out
   3459      // of the list but those cases should be extremely rare and
   3460      // do probably never occur when using the browser normally.
   3461      // (Tests or add-ons might do weird things though.)
   3462      this.saveClosedTabData(winData, closedTabs, tabData);
   3463    }
   3464 
   3465    // Remember the closed tab to properly handle any last updates included in
   3466    // the final "update" message sent by the frame script's unload handler.
   3467    this._closingTabMap.set(permanentKey, {
   3468      winData,
   3469      closedTabs,
   3470      tabData,
   3471    });
   3472  },
   3473 
   3474  /**
   3475   * Remove listeners which were added when browser was inserted and reset restoring state.
   3476   * Also re-instate lazy data and basically revert tab to its lazy browser state.
   3477   *
   3478   * @param aTab
   3479   *        Tab reference
   3480   */
   3481  resetBrowserToLazyState(aTab) {
   3482    let browser = aTab.linkedBrowser;
   3483    // Browser is already lazy so don't do anything.
   3484    if (!browser.isConnected) {
   3485      return;
   3486    }
   3487 
   3488    this.cleanUpRemovedBrowser(aTab);
   3489 
   3490    aTab.setAttribute("pending", "true");
   3491 
   3492    this._lastKnownFrameLoader.delete(browser.permanentKey);
   3493    this._crashedBrowsers.delete(browser.permanentKey);
   3494    aTab.removeAttribute("crashed");
   3495 
   3496    let { userTypedValue = null, userTypedClear = 0 } = browser;
   3497    let hasStartedLoad = browser.didStartLoadSinceLastUserTyping();
   3498 
   3499    let cacheState = lazy.TabStateCache.get(browser.permanentKey);
   3500 
   3501    // Cache the browser userTypedValue either if there is no cache state
   3502    // at all (e.g. if it was already discarded before we got to cache its state)
   3503    // or it may have been created but not including a userTypedValue (e.g.
   3504    // for a private tab we will cache `isPrivate: true` as soon as the tab
   3505    // is opened).
   3506    //
   3507    // But only if:
   3508    //
   3509    // - if there is no cache state yet (which is unfortunately required
   3510    //   for tabs discarded immediately after creation by extensions, see
   3511    //   Bug 1422588).
   3512    //
   3513    // - or the user typed value was already being loaded (otherwise the lazy
   3514    //   tab will not be restored with the expected url once activated again,
   3515    //   see Bug 1724205).
   3516    let shouldUpdateCacheState =
   3517      userTypedValue &&
   3518      (!cacheState || (hasStartedLoad && !cacheState.userTypedValue));
   3519 
   3520    if (shouldUpdateCacheState) {
   3521      // Discard was likely called before state can be cached.  Update
   3522      // the persistent tab state cache with browser information so a
   3523      // restore will be successful.  This information is necessary for
   3524      // restoreTabContent in ContentRestore.sys.mjs to work properly.
   3525      lazy.TabStateCache.update(browser.permanentKey, {
   3526        userTypedValue,
   3527        userTypedClear: 1,
   3528      });
   3529    }
   3530 
   3531    TAB_LAZY_STATES.set(aTab, {
   3532      url: browser.currentURI.spec,
   3533      title: aTab.label,
   3534      userTypedValue,
   3535      userTypedClear,
   3536    });
   3537  },
   3538 
   3539  /**
   3540   * Check if we are dealing with a crashed browser. If so, then the corresponding
   3541   * crashed tab was revived by navigating to a different page. Remove the browser
   3542   * from the list of crashed browsers to stop ignoring its messages.
   3543   *
   3544   * @param aBrowser
   3545   *        Browser reference
   3546   */
   3547  maybeExitCrashedState(aBrowser) {
   3548    let uri = aBrowser.documentURI;
   3549    if (uri?.spec?.startsWith("about:tabcrashed")) {
   3550      this._crashedBrowsers.delete(aBrowser.permanentKey);
   3551    }
   3552  },
   3553 
   3554  /**
   3555   * A debugging-only function to check if a browser is in _crashedBrowsers.
   3556   *
   3557   * @param aBrowser
   3558   *        Browser reference
   3559   */
   3560  isBrowserInCrashedSet(aBrowser) {
   3561    if (gDebuggingEnabled) {
   3562      return this._crashedBrowsers.has(aBrowser.permanentKey);
   3563    }
   3564    throw new Error(
   3565      "SessionStore.isBrowserInCrashedSet() should only be called in debug mode!"
   3566    );
   3567  },
   3568 
   3569  /**
   3570   * When a tab is removed or suspended, remove listeners and reset restoring state.
   3571   *
   3572   * @param aBrowser
   3573   *        Browser reference
   3574   */
   3575  cleanUpRemovedBrowser(aTab) {
   3576    let browser = aTab.linkedBrowser;
   3577 
   3578    browser.removeEventListener("SwapDocShells", this);
   3579    browser.removeEventListener("oop-browser-crashed", this);
   3580    browser.removeEventListener("oop-browser-buildid-mismatch", this);
   3581 
   3582    // If this tab was in the middle of restoring or still needs to be restored,
   3583    // we need to reset that state. If the tab was restoring, we will attempt to
   3584    // restore the next tab.
   3585    let previousState = TAB_STATE_FOR_BROWSER.get(browser);
   3586    if (previousState) {
   3587      this._resetTabRestoringState(aTab);
   3588      if (previousState == TAB_STATE_RESTORING) {
   3589        this.restoreNextTab();
   3590      }
   3591    }
   3592  },
   3593 
   3594  /**
   3595   * Insert a given |tabData| object into the list of |closedTabs|. We will
   3596   * determine the right insertion point based on the .closedAt properties of
   3597   * all tabs already in the list. The list will be truncated to contain a
   3598   * maximum of |this._max_tabs_undo| entries if required.
   3599   *
   3600   * @param {WindowStateData} winData
   3601   * @param {ClosedTabStateData[]} closedTabs
   3602   *   The list of closed tabs for a window or tab group.
   3603   * @param {ClosedTabStateData} tabData
   3604   *   The closed tab that should be inserted into `closedTabs`
   3605   * @param {boolean} [saveAction=true]
   3606   *   Whether or not to add an action to the closed actions stack on save.
   3607   */
   3608  saveClosedTabData(winData, closedTabs, tabData, saveAction = true) {
   3609    // Find the index of the first tab in the list
   3610    // of closed tabs that was closed before our tab.
   3611    let index = tabData.closedInTabGroupId
   3612      ? closedTabs.length
   3613      : closedTabs.findIndex(tab => {
   3614          return tab.closedAt < tabData.closedAt;
   3615        });
   3616 
   3617    // If we found no tab closed before our
   3618    // tab then just append it to the list.
   3619    if (index == -1) {
   3620      index = closedTabs.length;
   3621    }
   3622 
   3623    // About to save the closed tab, add a unique ID.
   3624    tabData.closedId = this._nextClosedId++;
   3625 
   3626    // Insert tabData at the right position.
   3627    closedTabs.splice(index, 0, tabData);
   3628    this._closedObjectsChanged = true;
   3629 
   3630    if (tabData.closedInGroup) {
   3631      if (winData._lastClosedTabGroupCount < this._max_tabs_undo) {
   3632        if (winData._lastClosedTabGroupCount < 0) {
   3633          winData._lastClosedTabGroupCount = 1;
   3634        } else {
   3635          winData._lastClosedTabGroupCount++;
   3636        }
   3637      }
   3638    } else {
   3639      winData._lastClosedTabGroupCount = -1;
   3640    }
   3641 
   3642    winData.lastClosedTabGroupId = tabData.closedInTabGroupId || null;
   3643 
   3644    if (saveAction) {
   3645      this._addClosedAction(this._LAST_ACTION_CLOSED_TAB, tabData.closedId);
   3646    }
   3647 
   3648    // Truncate the list of closed tabs, if needed. For closed tabs within tab
   3649    // groups, always keep all closed tabs because users expect tab groups to
   3650    // be intact.
   3651    if (
   3652      !tabData.closedInTabGroupId &&
   3653      closedTabs.length > this._max_tabs_undo
   3654    ) {
   3655      closedTabs.splice(this._max_tabs_undo, closedTabs.length);
   3656    }
   3657  },
   3658 
   3659  /**
   3660   * Remove the closed tab data at |index| from the list of |closedTabs|. If
   3661   * the tab's final message is still pending we will simply discard it when
   3662   * it arrives so that the tab doesn't reappear in the list.
   3663   *
   3664   * @param winData (object)
   3665   *        The data of the window.
   3666   * @param index (uint)
   3667   *        The index of the tab to remove.
   3668   * @param closedTabs (array)
   3669   *        The list of closed tabs for a window.
   3670   */
   3671  removeClosedTabData(winData, closedTabs, index) {
   3672    // Remove the given index from the list.
   3673    let [closedTab] = closedTabs.splice(index, 1);
   3674    this._closedObjectsChanged = true;
   3675 
   3676    // If the tab is part of the last closed multiselected tab set,
   3677    // we need to deduct the tab from the count.
   3678    if (index < winData._lastClosedTabGroupCount) {
   3679      winData._lastClosedTabGroupCount--;
   3680    }
   3681 
   3682    // If the closed tab's state still has a .permanentKey property then we
   3683    // haven't seen its final update message yet. Remove it from the map of
   3684    // closed tabs so that we will simply discard its last messages and will
   3685    // not add it back to the list of closed tabs again.
   3686    if (closedTab.permanentKey) {
   3687      this._closingTabMap.delete(closedTab.permanentKey);
   3688      this._tabClosingByWindowMap.delete(closedTab.permanentKey);
   3689      delete closedTab.permanentKey;
   3690    }
   3691 
   3692    this._removeClosedAction(this._LAST_ACTION_CLOSED_TAB, closedTab.closedId);
   3693 
   3694    return closedTab;
   3695  },
   3696 
   3697  /**
   3698   * When a tab is selected, save session data
   3699   *
   3700   * @param aWindow
   3701   *        Window reference
   3702   */
   3703  onTabSelect: function ssi_onTabSelect(aWindow) {
   3704    if (lazy.RunState.isRunning) {
   3705      this._windows[aWindow.__SSi].selected =
   3706        aWindow.gBrowser.tabContainer.selectedIndex;
   3707 
   3708      let tab = aWindow.gBrowser.selectedTab;
   3709      this.maybeRestoreTabContent(tab);
   3710    }
   3711  },
   3712 
   3713  maybeRestoreTabContent(tab) {
   3714    let browser = tab.linkedBrowser;
   3715 
   3716    if (TAB_STATE_FOR_BROWSER.get(browser) == TAB_STATE_NEEDS_RESTORE) {
   3717      // If BROWSER_STATE is still available for the browser and it is
   3718      // If __SS_restoreState is still on the browser and it is
   3719      // TAB_STATE_NEEDS_RESTORE, then we haven't restored this tab yet.
   3720      //
   3721      // It's possible that this tab was recently revived, and that
   3722      // we've deferred showing the tab crashed page for it (if the
   3723      // tab crashed in the background). If so, we need to re-enter
   3724      // the crashed state, since we'll be showing the tab crashed
   3725      // page.
   3726      if (lazy.TabCrashHandler.willShowCrashedTab(browser)) {
   3727        this.enterCrashedState(browser);
   3728      } else {
   3729        this.restoreTabContent(tab);
   3730      }
   3731    }
   3732  },
   3733 
   3734  onTabShow: function ssi_onTabShow(aWindow, aTab) {
   3735    // If the tab hasn't been restored yet, move it into the right bucket
   3736    if (
   3737      TAB_STATE_FOR_BROWSER.get(aTab.linkedBrowser) == TAB_STATE_NEEDS_RESTORE
   3738    ) {
   3739      TabRestoreQueue.hiddenToVisible(aTab);
   3740 
   3741      // let's kick off tab restoration again to ensure this tab gets restored
   3742      // with "restore_hidden_tabs" == false (now that it has become visible)
   3743      this.restoreNextTab();
   3744    }
   3745 
   3746    // Default delay of 2 seconds gives enough time to catch multiple TabShow
   3747    // events. This used to be due to changing groups in 'tab groups'. We
   3748    // might be able to get rid of this now?
   3749    this.saveStateDelayed(aWindow);
   3750  },
   3751 
   3752  onTabHide: function ssi_onTabHide(aWindow, aTab) {
   3753    // If the tab hasn't been restored yet, move it into the right bucket
   3754    if (
   3755      TAB_STATE_FOR_BROWSER.get(aTab.linkedBrowser) == TAB_STATE_NEEDS_RESTORE
   3756    ) {
   3757      TabRestoreQueue.visibleToHidden(aTab);
   3758    }
   3759 
   3760    // Default delay of 2 seconds gives enough time to catch multiple TabHide
   3761    // events. This used to be due to changing groups in 'tab groups'. We
   3762    // might be able to get rid of this now?
   3763    this.saveStateDelayed(aWindow);
   3764  },
   3765 
   3766  /**
   3767   * Handler for the event that is fired when a <xul:browser> crashes.
   3768   *
   3769   * @param aWindow
   3770   *        The window that the crashed browser belongs to.
   3771   * @param aBrowser
   3772   *        The <xul:browser> that is now in the crashed state.
   3773   */
   3774  onBrowserCrashed(aBrowser) {
   3775    this.enterCrashedState(aBrowser);
   3776    // The browser crashed so we might never receive flush responses.
   3777    // Resolve all pending flush requests for the crashed browser.
   3778    lazy.TabStateFlusher.resolveAll(aBrowser);
   3779  },
   3780 
   3781  /**
   3782   * Called when a browser is showing or is about to show the tab
   3783   * crashed page. This method causes SessionStore to ignore the
   3784   * tab until it's restored.
   3785   *
   3786   * @param browser
   3787   *        The <xul:browser> that is about to show the crashed page.
   3788   */
   3789  enterCrashedState(browser) {
   3790    this._crashedBrowsers.add(browser.permanentKey);
   3791 
   3792    let win = browser.ownerGlobal;
   3793 
   3794    // If we hadn't yet restored, or were still in the midst of
   3795    // restoring this browser at the time of the crash, we need
   3796    // to reset its state so that we can try to restore it again
   3797    // when the user revives the tab from the crash.
   3798    if (TAB_STATE_FOR_BROWSER.has(browser)) {
   3799      let tab = win.gBrowser.getTabForBrowser(browser);
   3800      if (tab) {
   3801        this._resetLocalTabRestoringState(tab);
   3802      }
   3803    }
   3804  },
   3805 
   3806  // Clean up data that has been closed a long time ago.
   3807  // Do not reschedule a save. This will wait for the next regular
   3808  // save.
   3809  onIdleDaily() {
   3810    // Remove old closed windows
   3811    this._cleanupOldData([this._closedWindows]);
   3812 
   3813    // Remove closed tabs of closed windows
   3814    this._cleanupOldData(
   3815      this._closedWindows.map(winData => winData._closedTabs)
   3816    );
   3817 
   3818    // Remove closed groups of closed windows
   3819    this._cleanupOldData(
   3820      this._closedWindows.map(winData => winData.closedGroups)
   3821    );
   3822 
   3823    // Remove closed tabs of open windows
   3824    this._cleanupOldData(
   3825      Object.keys(this._windows).map(key => this._windows[key]._closedTabs)
   3826    );
   3827 
   3828    // Remove closed groups of open windows
   3829    this._cleanupOldData(
   3830      Object.keys(this._windows).map(key => this._windows[key].closedGroups)
   3831    );
   3832 
   3833    this._notifyOfClosedObjectsChange();
   3834  },
   3835 
   3836  // Remove "old" data from an array
   3837  _cleanupOldData(targets) {
   3838    const TIME_TO_LIVE = this._prefBranch.getIntPref(
   3839      "sessionstore.cleanup.forget_closed_after"
   3840    );
   3841    const now = Date.now();
   3842 
   3843    for (let array of targets) {
   3844      for (let i = array.length - 1; i >= 0; --i) {
   3845        let data = array[i];
   3846        // Make sure that we have a timestamp to tell us when the target
   3847        // has been closed. If we don't have a timestamp, default to a
   3848        // safe timestamp: just now.
   3849        data.closedAt = data.closedAt || now;
   3850        if (now - data.closedAt > TIME_TO_LIVE) {
   3851          array.splice(i, 1);
   3852          this._closedObjectsChanged = true;
   3853        }
   3854      }
   3855    }
   3856  },
   3857 
   3858  /* ........ nsISessionStore API .............. */
   3859 
   3860  getBrowserState: function ssi_getBrowserState() {
   3861    let state = this.getCurrentState();
   3862 
   3863    // Don't include the last session state in getBrowserState().
   3864    delete state.lastSessionState;
   3865 
   3866    // Don't include any deferred initial state.
   3867    delete state.deferredInitialState;
   3868 
   3869    return JSON.stringify(state);
   3870  },
   3871 
   3872  setBrowserState: function ssi_setBrowserState(aState) {
   3873    this._handleClosedWindows();
   3874 
   3875    try {
   3876      var state = JSON.parse(aState);
   3877    } catch (ex) {
   3878      /* invalid state object - don't restore anything */
   3879    }
   3880    if (!state) {
   3881      throw Components.Exception(
   3882        "Invalid state string: not JSON",
   3883        Cr.NS_ERROR_INVALID_ARG
   3884      );
   3885    }
   3886    if (!state.windows) {
   3887      throw Components.Exception("No windows", Cr.NS_ERROR_INVALID_ARG);
   3888    }
   3889 
   3890    this._browserSetState = true;
   3891 
   3892    // Make sure the priority queue is emptied out
   3893    this._resetRestoringState();
   3894 
   3895    var window = this._getTopWindow();
   3896    if (!window) {
   3897      this._restoreCount = 1;
   3898      this._openWindowWithState(state);
   3899      return;
   3900    }
   3901 
   3902    // close all other browser windows
   3903    for (let otherWin of this._browserWindows) {
   3904      if (otherWin != window) {
   3905        otherWin.close();
   3906        this.onClose(otherWin);
   3907      }
   3908    }
   3909 
   3910    // make sure closed window data isn't kept
   3911    if (this._closedWindows.length) {
   3912      this._closedWindows = [];
   3913      this._closedObjectsChanged = true;
   3914    }
   3915 
   3916    // determine how many windows are meant to be restored
   3917    this._restoreCount = state.windows ? state.windows.length : 0;
   3918 
   3919    // global data must be restored before restoreWindow is called so that
   3920    // it happens before observers are notified
   3921    this._globalState.setFromState(state);
   3922 
   3923    // Restore session cookies.
   3924    lazy.SessionCookies.restore(state.cookies || []);
   3925 
   3926    // restore to the given state
   3927    this.restoreWindows(window, state, { overwriteTabs: true });
   3928 
   3929    // Notify of changes to closed objects.
   3930    this._notifyOfClosedObjectsChange();
   3931  },
   3932 
   3933  /**
   3934   * @param {Window} aWindow
   3935   *        Window reference
   3936   * @returns {{windows: WindowStateData[]}}
   3937   */
   3938  getWindowState: function ssi_getWindowState(aWindow) {
   3939    if ("__SSi" in aWindow) {
   3940      return Cu.cloneInto(this._getWindowState(aWindow), {});
   3941    }
   3942 
   3943    if (DyingWindowCache.has(aWindow)) {
   3944      let data = DyingWindowCache.get(aWindow);
   3945      return Cu.cloneInto({ windows: [data] }, {});
   3946    }
   3947 
   3948    throw Components.Exception(
   3949      "Window is not tracked",
   3950      Cr.NS_ERROR_INVALID_ARG
   3951    );
   3952  },
   3953 
   3954  setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
   3955    if (!aWindow.__SSi) {
   3956      throw Components.Exception(
   3957        "Window is not tracked",
   3958        Cr.NS_ERROR_INVALID_ARG
   3959      );
   3960    }
   3961 
   3962    this.restoreWindows(aWindow, aState, { overwriteTabs: aOverwrite });
   3963 
   3964    // Notify of changes to closed objects.
   3965    this._notifyOfClosedObjectsChange();
   3966  },
   3967 
   3968  getTabState: function ssi_getTabState(aTab) {
   3969    if (!aTab || !aTab.ownerGlobal) {
   3970      throw Components.Exception("Need a valid tab", Cr.NS_ERROR_INVALID_ARG);
   3971    }
   3972    if (!aTab.ownerGlobal.__SSi) {
   3973      throw Components.Exception(
   3974        "Default view is not tracked",
   3975        Cr.NS_ERROR_INVALID_ARG
   3976      );
   3977    }
   3978 
   3979    let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
   3980 
   3981    return JSON.stringify(tabState);
   3982  },
   3983 
   3984  setTabState(aTab, aState) {
   3985    // Remove the tab state from the cache.
   3986    // Note that we cannot simply replace the contents of the cache
   3987    // as |aState| can be an incomplete state that will be completed
   3988    // by |restoreTabs|.
   3989    let tabState = aState;
   3990    if (typeof tabState == "string") {
   3991      tabState = JSON.parse(aState);
   3992    }
   3993    if (!tabState) {
   3994      throw Components.Exception(
   3995        "Invalid state string: not JSON",
   3996        Cr.NS_ERROR_INVALID_ARG
   3997      );
   3998    }
   3999    if (typeof tabState != "object") {
   4000      throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG);
   4001    }
   4002    if (!("entries" in tabState)) {
   4003      throw Components.Exception(
   4004        "Invalid state object: no entries",
   4005        Cr.NS_ERROR_INVALID_ARG
   4006      );
   4007    }
   4008 
   4009    let window = aTab.ownerGlobal;
   4010    if (!window || !("__SSi" in window)) {
   4011      throw Components.Exception(
   4012        "Window is not tracked",
   4013        Cr.NS_ERROR_INVALID_ARG
   4014      );
   4015    }
   4016 
   4017    if (TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser)) {
   4018      this._resetTabRestoringState(aTab);
   4019    }
   4020 
   4021    this._ensureNoNullsInTabDataList(
   4022      window.gBrowser.tabs,
   4023      this._windows[window.__SSi].tabs,
   4024      aTab._tPos
   4025    );
   4026    this.restoreTab(aTab, tabState);
   4027 
   4028    // Notify of changes to closed objects.
   4029    this._notifyOfClosedObjectsChange();
   4030  },
   4031 
   4032  getInternalObjectState(obj) {
   4033    if (obj.__SSi) {
   4034      return this._windows[obj.__SSi];
   4035    }
   4036    return obj.loadURI
   4037      ? TAB_STATE_FOR_BROWSER.get(obj)
   4038      : TAB_CUSTOM_VALUES.get(obj);
   4039  },
   4040 
   4041  getObjectTypeForClosedId(aClosedId) {
   4042    // check if matches a window first
   4043    if (this.getClosedWindowDataByClosedId(aClosedId)) {
   4044      return this._LAST_ACTION_CLOSED_WINDOW;
   4045    }
   4046    return this._LAST_ACTION_CLOSED_TAB;
   4047  },
   4048 
   4049  /**
   4050   * @param {number} aClosedId
   4051   * @returns {WindowStateData|undefined}
   4052   */
   4053  getClosedWindowDataByClosedId: function ssi_getClosedWindowDataByClosedId(
   4054    aClosedId
   4055  ) {
   4056    return this._closedWindows.find(
   4057      closedData => closedData.closedId == aClosedId
   4058    );
   4059  },
   4060 
   4061  getWindowById: function ssi_getWindowById(aSessionStoreId) {
   4062    let resultWindow;
   4063    for (let window of this._browserWindows) {
   4064      if (window.__SSi === aSessionStoreId) {
   4065        resultWindow = window;
   4066        break;
   4067      }
   4068    }
   4069    return resultWindow;
   4070  },
   4071 
   4072  duplicateTab: function ssi_duplicateTab(
   4073    aWindow,
   4074    aTab,
   4075    aDelta = 0,
   4076    aRestoreImmediately = true,
   4077    { inBackground, tabIndex } = {}
   4078  ) {
   4079    if (!aTab || !aTab.ownerGlobal) {
   4080      throw Components.Exception("Need a valid tab", Cr.NS_ERROR_INVALID_ARG);
   4081    }
   4082    if (!aTab.ownerGlobal.__SSi) {
   4083      throw Components.Exception(
   4084        "Default view is not tracked",
   4085        Cr.NS_ERROR_INVALID_ARG
   4086      );
   4087    }
   4088    if (!aWindow.gBrowser) {
   4089      throw Components.Exception(
   4090        "Invalid window object: no gBrowser",
   4091        Cr.NS_ERROR_INVALID_ARG
   4092      );
   4093    }
   4094 
   4095    // Create a new tab.
   4096    let userContextId = aTab.getAttribute("usercontextid") || "";
   4097 
   4098    let tabOptions = {
   4099      userContextId,
   4100      tabIndex,
   4101      ...(aTab == aWindow.gBrowser.selectedTab
   4102        ? { relatedToCurrent: true, ownerTab: aTab }
   4103        : {}),
   4104      skipLoad: true,
   4105      preferredRemoteType: aTab.linkedBrowser.remoteType,
   4106      tabGroup: aTab.group,
   4107    };
   4108    let newTab = aWindow.gBrowser.addTrustedTab(null, tabOptions);
   4109 
   4110    // Start the throbber to pretend we're doing something while actually
   4111    // waiting for data from the frame script. This throbber is disabled
   4112    // if the URI is a local about: URI.
   4113    let uriObj = aTab.linkedBrowser.currentURI;
   4114    if (!uriObj || (uriObj && !uriObj.schemeIs("about"))) {
   4115      newTab.setAttribute("busy", "true");
   4116    }
   4117 
   4118    // Hack to ensure that the about:home, about:newtab, and about:welcome
   4119    // favicon is loaded instantaneously, to avoid flickering and improve
   4120    // perceived performance.
   4121    aWindow.gBrowser.setDefaultIcon(newTab, uriObj);
   4122 
   4123    // Collect state before flushing.
   4124    let tabState = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
   4125 
   4126    // Flush to get the latest tab state to duplicate.
   4127    let browser = aTab.linkedBrowser;
   4128    lazy.TabStateFlusher.flush(browser).then(() => {
   4129      // The new tab might have been closed in the meantime.
   4130      if (newTab.closing || !newTab.linkedBrowser) {
   4131        return;
   4132      }
   4133 
   4134      let window = newTab.ownerGlobal;
   4135 
   4136      // The tab or its window might be gone.
   4137      if (!window || !window.__SSi) {
   4138        return;
   4139      }
   4140 
   4141      // Update state with flushed data. We can't use TabState.clone() here as
   4142      // the tab to duplicate may have already been closed. In that case we
   4143      // only have access to the <xul:browser>.
   4144      let options = { includePrivateData: true };
   4145      lazy.TabState.copyFromCache(browser.permanentKey, tabState, options);
   4146 
   4147      tabState.index += aDelta;
   4148      tabState.index = Math.max(
   4149        1,
   4150        Math.min(tabState.index, tabState.entries.length)
   4151      );
   4152      tabState.pinned = false;
   4153 
   4154      if (inBackground === false) {
   4155        aWindow.gBrowser.selectedTab = newTab;
   4156      }
   4157 
   4158      // Restore the state into the new tab.
   4159      this.restoreTab(newTab, tabState, {
   4160        restoreImmediately: aRestoreImmediately,
   4161      });
   4162    });
   4163 
   4164    return newTab;
   4165  },
   4166 
   4167  getWindows(aWindowOrOptions) {
   4168    let isPrivate;
   4169    if (!aWindowOrOptions) {
   4170      aWindowOrOptions = this._getTopWindow();
   4171    }
   4172    if (aWindowOrOptions instanceof Ci.nsIDOMWindow) {
   4173      isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aWindowOrOptions);
   4174    } else {
   4175      isPrivate = Boolean(aWindowOrOptions.private);
   4176    }
   4177 
   4178    const browserWindows = Array.from(this._browserWindows).filter(win => {
   4179      return PrivateBrowsingUtils.isBrowserPrivate(win) === isPrivate;
   4180    });
   4181    return browserWindows;
   4182  },
   4183 
   4184  getWindowForTabClosedId(aClosedId, aIncludePrivate) {
   4185    // check non-private windows first, and then only check private windows if
   4186    // aIncludePrivate was true
   4187    const privateValues = aIncludePrivate ? [false, true] : [false];
   4188    for (let privateness of privateValues) {
   4189      for (let window of this.getWindows({ private: privateness })) {
   4190        const windowState = this._windows[window.__SSi];
   4191        const closedTabs =
   4192          this._getStateForClosedTabsAndClosedGroupTabs(windowState);
   4193        if (!closedTabs.length) {
   4194          continue;
   4195        }
   4196        if (closedTabs.find(tab => tab.closedId === aClosedId)) {
   4197          return window;
   4198        }
   4199      }
   4200    }
   4201    return undefined;
   4202  },
   4203 
   4204  getLastClosedTabCount(aWindow) {
   4205    if ("__SSi" in aWindow) {
   4206      return Math.min(
   4207        Math.max(this._windows[aWindow.__SSi]._lastClosedTabGroupCount, 1),
   4208        this.getClosedTabCountForWindow(aWindow)
   4209      );
   4210    }
   4211 
   4212    throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   4213  },
   4214 
   4215  resetLastClosedTabCount(aWindow) {
   4216    if ("__SSi" in aWindow) {
   4217      this._windows[aWindow.__SSi]._lastClosedTabGroupCount = -1;
   4218      this._windows[aWindow.__SSi].lastClosedTabGroupId = null;
   4219    } else {
   4220      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   4221    }
   4222  },
   4223 
   4224  getClosedTabCountForWindow: function ssi_getClosedTabCountForWindow(aWindow) {
   4225    if ("__SSi" in aWindow) {
   4226      return this._getStateForClosedTabsAndClosedGroupTabs(
   4227        this._windows[aWindow.__SSi]
   4228      ).length;
   4229    }
   4230 
   4231    if (!DyingWindowCache.has(aWindow)) {
   4232      throw Components.Exception(
   4233        "Window is not tracked",
   4234        Cr.NS_ERROR_INVALID_ARG
   4235      );
   4236    }
   4237 
   4238    return this._getStateForClosedTabsAndClosedGroupTabs(
   4239      DyingWindowCache.get(aWindow)
   4240    ).length;
   4241  },
   4242 
   4243  _prepareClosedTabOptions(aOptions = {}) {
   4244    const sourceOptions = Object.assign(
   4245      {
   4246        closedTabsFromAllWindows: this._closedTabsFromAllWindowsEnabled,
   4247        closedTabsFromClosedWindows: this._closedTabsFromClosedWindowsEnabled,
   4248        sourceWindow: null,
   4249      },
   4250      aOptions instanceof Ci.nsIDOMWindow
   4251        ? { sourceWindow: aOptions }
   4252        : aOptions
   4253    );
   4254    if (!sourceOptions.sourceWindow) {
   4255      sourceOptions.sourceWindow = this._getTopWindow(sourceOptions.private);
   4256    }
   4257    /*
   4258    _getTopWindow may return null on MacOS when the last window has been closed.
   4259    Since private browsing windows are irrelevant after they have been closed we
   4260    don't need to check if it was a private browsing window.
   4261    */
   4262    if (!sourceOptions.sourceWindow) {
   4263      sourceOptions.private = false;
   4264    }
   4265    if (!sourceOptions.hasOwnProperty("private")) {
   4266      sourceOptions.private = PrivateBrowsingUtils.isWindowPrivate(
   4267        sourceOptions.sourceWindow
   4268      );
   4269    }
   4270    return sourceOptions;
   4271  },
   4272 
   4273  getClosedTabCount(aOptions) {
   4274    const sourceOptions = this._prepareClosedTabOptions(aOptions);
   4275    let tabCount = 0;
   4276 
   4277    if (sourceOptions.closedTabsFromAllWindows) {
   4278      tabCount += this.getWindows({ private: sourceOptions.private })
   4279        .map(win => this.getClosedTabCountForWindow(win))
   4280        .reduce((total, count) => total + count, 0);
   4281    } else {
   4282      tabCount += this.getClosedTabCountForWindow(sourceOptions.sourceWindow);
   4283    }
   4284 
   4285    if (!sourceOptions.private && sourceOptions.closedTabsFromClosedWindows) {
   4286      tabCount += this.getClosedTabCountFromClosedWindows();
   4287    }
   4288    return tabCount;
   4289  },
   4290 
   4291  getClosedTabCountFromClosedWindows:
   4292    function ssi_getClosedTabCountFromClosedWindows() {
   4293      const tabCount = this._closedWindows
   4294        .map(
   4295          winData =>
   4296            this._getStateForClosedTabsAndClosedGroupTabs(winData).length
   4297        )
   4298        .reduce((total, count) => total + count, 0);
   4299      return tabCount;
   4300    },
   4301 
   4302  getClosedTabDataForWindow: function ssi_getClosedTabDataForWindow(aWindow) {
   4303    return this._getClonedDataForWindow(
   4304      aWindow,
   4305      this._getStateForClosedTabsAndClosedGroupTabs
   4306    );
   4307  },
   4308 
   4309  getClosedTabData: function ssi_getClosedTabData(aOptions) {
   4310    const sourceOptions = this._prepareClosedTabOptions(aOptions);
   4311    const closedTabData = [];
   4312    if (sourceOptions.closedTabsFromAllWindows) {
   4313      for (let win of this.getWindows({ private: sourceOptions.private })) {
   4314        closedTabData.push(...this.getClosedTabDataForWindow(win));
   4315      }
   4316    } else {
   4317      closedTabData.push(
   4318        ...this.getClosedTabDataForWindow(sourceOptions.sourceWindow)
   4319      );
   4320    }
   4321    return closedTabData;
   4322  },
   4323 
   4324  getClosedTabDataFromClosedWindows:
   4325    function ssi_getClosedTabDataFromClosedWindows() {
   4326      const closedTabData = [];
   4327      for (let winData of this._closedWindows) {
   4328        const sourceClosedId = winData.closedId;
   4329        const closedTabs = Cu.cloneInto(
   4330          this._getStateForClosedTabsAndClosedGroupTabs(winData),
   4331          {}
   4332        );
   4333        // Add a property pointing back to the closed window source
   4334        for (let tabData of closedTabs) {
   4335          tabData.sourceClosedId = sourceClosedId;
   4336        }
   4337        closedTabData.push(...closedTabs);
   4338      }
   4339      // sorting is left to the caller
   4340      return closedTabData;
   4341    },
   4342 
   4343  /**
   4344   * @param {Window|object} aOptions
   4345   * @param {Window} [aOptions.sourceWindow]
   4346   * @param {boolean} [aOptions.private = false]
   4347   * @param {boolean} [aOptions.closedTabsFromAllWindows]
   4348   * @param {boolean} [aOptions.closedTabsFromClosedWindows]
   4349   * @returns {ClosedTabGroupStateData[]}
   4350   */
   4351  getClosedTabGroups: function ssi_getClosedTabGroups(aOptions) {
   4352    const sourceOptions = this._prepareClosedTabOptions(aOptions);
   4353    const closedTabGroups = [];
   4354    if (sourceOptions.closedTabsFromAllWindows) {
   4355      for (let win of this.getWindows({ private: sourceOptions.private })) {
   4356        closedTabGroups.push(
   4357          ...this._getClonedDataForWindow(win, w => w.closedGroups ?? [])
   4358        );
   4359      }
   4360    } else if (sourceOptions.sourceWindow.closedGroups) {
   4361      closedTabGroups.push(
   4362        ...this._getClonedDataForWindow(
   4363          sourceOptions.sourceWindow,
   4364          w => w.closedGroups ?? []
   4365        )
   4366      );
   4367    }
   4368 
   4369    if (sourceOptions.closedTabsFromClosedWindows) {
   4370      for (let winData of this.getClosedWindowData()) {
   4371        if (!winData.closedGroups) {
   4372          continue;
   4373        }
   4374        // Add a property pointing back to the closed window source
   4375        for (let groupData of winData.closedGroups) {
   4376          for (let tabData of groupData.tabs) {
   4377            tabData.sourceClosedId = winData.closedId;
   4378          }
   4379        }
   4380        closedTabGroups.push(...winData.closedGroups);
   4381      }
   4382    }
   4383    return closedTabGroups;
   4384  },
   4385 
   4386  getLastClosedTabGroupId(aWindow) {
   4387    if ("__SSi" in aWindow) {
   4388      return this._windows[aWindow.__SSi].lastClosedTabGroupId;
   4389    }
   4390 
   4391    throw new Error("Window is not tracked");
   4392  },
   4393 
   4394  /**
   4395   * Returns a clone of some subset of a window's state data.
   4396   *
   4397   * @template D
   4398   * @param {Window} aWindow
   4399   * @param {function(WindowStateData):D} selector
   4400   *   A function that returns the desired data located within
   4401   *   a supplied window state.
   4402   * @returns {D}
   4403   */
   4404  _getClonedDataForWindow: function ssi_getClonedDataForWindow(
   4405    aWindow,
   4406    selector
   4407  ) {
   4408    // We need to enable wrapping reflectors in order to allow the cloning of
   4409    // objects containing FormDatas, which could be stored by
   4410    // form-associated custom elements.
   4411    let options = { wrapReflectors: true };
   4412    /** @type {WindowStateData} */
   4413    let winData;
   4414 
   4415    if ("__SSi" in aWindow) {
   4416      winData = this._windows[aWindow.__SSi];
   4417    }
   4418 
   4419    if (!winData && !DyingWindowCache.has(aWindow)) {
   4420      throw Components.Exception(
   4421        "Window is not tracked",
   4422        Cr.NS_ERROR_INVALID_ARG
   4423      );
   4424    }
   4425 
   4426    winData ??= DyingWindowCache.get(aWindow);
   4427    let data = selector(winData);
   4428    return Cu.cloneInto(data, {}, options);
   4429  },
   4430 
   4431  /**
   4432   * Returns either a unified list of closed tabs from both
   4433   * `_closedTabs` and `closedGroups` or else, when supplying an index,
   4434   * returns the specific closed tab from that unified list.
   4435   *
   4436   * This bridges the gap between callers that want a unified list of all closed tabs
   4437   * from all contexts vs. callers that want a specific list of closed tabs from a
   4438   * specific context (e.g. only closed tabs from a specific closed tab group).
   4439   *
   4440   * @param {WindowStateData} winData
   4441   * @param {number} [aIndex]
   4442   *   If not supplied, returns all closed tabs and tabs from closed tab groups.
   4443   *   If supplied, returns the single closed tab with the given index.
   4444   * @returns {TabStateData|TabStateData[]}
   4445   */
   4446  _getStateForClosedTabsAndClosedGroupTabs:
   4447    function ssi_getStateForClosedTabsAndClosedGroupTabs(winData, aIndex) {
   4448      const closedGroups = winData.closedGroups ?? [];
   4449      const closedTabs = winData._closedTabs ?? [];
   4450 
   4451      // Merge tabs and groups into a single sorted array of tabs sorted by
   4452      // closedAt
   4453      let result = [];
   4454      let groupIdx = 0;
   4455      let tabIdx = 0;
   4456      let current = 0;
   4457      let totalLength = closedGroups.length + closedTabs.length;
   4458 
   4459      while (current < totalLength) {
   4460        let group = closedGroups[groupIdx];
   4461        let tab = closedTabs[tabIdx];
   4462 
   4463        if (
   4464          groupIdx < closedGroups.length &&
   4465          (tabIdx >= closedTabs.length || group?.closedAt > tab?.closedAt)
   4466        ) {
   4467          group.tabs.forEach((groupTab, idx) => {
   4468            groupTab._originalStateIndex = idx;
   4469            groupTab._originalGroupStateIndex = groupIdx;
   4470            result.push(groupTab);
   4471          });
   4472          groupIdx++;
   4473        } else {
   4474          tab._originalStateIndex = tabIdx;
   4475          result.push(tab);
   4476          tabIdx++;
   4477        }
   4478 
   4479        current++;
   4480        if (current > aIndex) {
   4481          break;
   4482        }
   4483      }
   4484 
   4485      if (aIndex !== undefined) {
   4486        return result[aIndex];
   4487      }
   4488 
   4489      return result;
   4490    },
   4491 
   4492  /**
   4493   * For a given closed tab that was retrieved by `_getStateForClosedTabsAndClosedGroupTabs`,
   4494   * returns the specific closed tab list data source and the index within that data source
   4495   * where the closed tab can be found.
   4496   *
   4497   * This bridges the gap between callers that want a unified list of all closed tabs
   4498   * from all contexts vs. callers that want a specific list of closed tabs from a
   4499   * specific context (e.g. only closed tabs from a specific closed tab group).
   4500   *
   4501   * @param {WindowState} sourceWinData
   4502   * @param {TabStateData} tabState
   4503   * @returns {{closedTabSet: TabStateData[], closedTabIndex: number}}
   4504   */
   4505  _getClosedTabStateFromUnifiedIndex: function ssi_getClosedTabForUnifiedIndex(
   4506    sourceWinData,
   4507    tabState
   4508  ) {
   4509    let closedTabSet, closedTabIndex;
   4510    if (tabState._originalGroupStateIndex == null) {
   4511      closedTabSet = sourceWinData._closedTabs;
   4512    } else {
   4513      closedTabSet =
   4514        sourceWinData.closedGroups[tabState._originalGroupStateIndex].tabs;
   4515    }
   4516    closedTabIndex = tabState._originalStateIndex;
   4517 
   4518    return { closedTabSet, closedTabIndex };
   4519  },
   4520 
   4521  undoCloseTab: function ssi_undoCloseTab(aSource, aIndex, aTargetWindow) {
   4522    const sourceWinData = this._resolveClosedDataSource(aSource);
   4523    const isPrivateSource = Boolean(sourceWinData.isPrivate);
   4524    if (aTargetWindow && !aTargetWindow.__SSi) {
   4525      throw Components.Exception(
   4526        "Target window is not tracked",
   4527        Cr.NS_ERROR_INVALID_ARG
   4528      );
   4529    } else if (!aTargetWindow) {
   4530      aTargetWindow = this._getTopWindow(isPrivateSource);
   4531    }
   4532    if (
   4533      isPrivateSource !== PrivateBrowsingUtils.isWindowPrivate(aTargetWindow)
   4534    ) {
   4535      throw Components.Exception(
   4536        "Target window doesn't have the same privateness as the source window",
   4537        Cr.NS_ERROR_INVALID_ARG
   4538      );
   4539    }
   4540 
   4541    // default to the most-recently closed tab
   4542    aIndex = aIndex || 0;
   4543 
   4544    const closedTabState = this._getStateForClosedTabsAndClosedGroupTabs(
   4545      sourceWinData,
   4546      aIndex
   4547    );
   4548    if (!closedTabState) {
   4549      throw Components.Exception(
   4550        "Invalid index: not in the closed tabs",
   4551        Cr.NS_ERROR_INVALID_ARG
   4552      );
   4553    }
   4554    let { closedTabSet, closedTabIndex } =
   4555      this._getClosedTabStateFromUnifiedIndex(sourceWinData, closedTabState);
   4556 
   4557    // fetch the data of closed tab, while removing it from the array
   4558    let { state, pos } = this.removeClosedTabData(
   4559      sourceWinData,
   4560      closedTabSet,
   4561      closedTabIndex
   4562    );
   4563    this._cleanupOrphanedClosedGroups(sourceWinData);
   4564 
   4565    // Predict the remote type to use for the load to avoid unnecessary process
   4566    // switches.
   4567    let preferredRemoteType = lazy.E10SUtils.DEFAULT_REMOTE_TYPE;
   4568    let url;
   4569    if (state.entries?.length) {
   4570      let activeIndex = (state.index || state.entries.length) - 1;
   4571      activeIndex = Math.min(activeIndex, state.entries.length - 1);
   4572      activeIndex = Math.max(activeIndex, 0);
   4573      url = state.entries[activeIndex].url;
   4574    }
   4575    if (url) {
   4576      preferredRemoteType = this.getPreferredRemoteType(
   4577        url,
   4578        aTargetWindow,
   4579        state.userContextId
   4580      );
   4581    }
   4582 
   4583    // create a new tab
   4584    let tabbrowser = aTargetWindow.gBrowser;
   4585    let tab = (tabbrowser.selectedTab = tabbrowser.addTrustedTab(null, {
   4586      // Append the tab if we're opening into a different window,
   4587      tabIndex: aSource == aTargetWindow ? pos : Infinity,
   4588      pinned: state.pinned,
   4589      userContextId: state.userContextId,
   4590      skipLoad: true,
   4591      preferredRemoteType,
   4592      tabGroup: tabbrowser.tabGroups.find(g => g.id == state.groupId),
   4593    }));
   4594 
   4595    // restore tab content
   4596    this.restoreTab(tab, state);
   4597 
   4598    // Notify of changes to closed objects.
   4599    this._notifyOfClosedObjectsChange();
   4600 
   4601    return tab;
   4602  },
   4603 
   4604  undoClosedTabFromClosedWindow: function ssi_undoClosedTabFromClosedWindow(
   4605    aSource,
   4606    aClosedId,
   4607    aTargetWindow
   4608  ) {
   4609    const sourceWinData = this._resolveClosedDataSource(aSource);
   4610    const closedTabs =
   4611      this._getStateForClosedTabsAndClosedGroupTabs(sourceWinData);
   4612    const closedIndex = closedTabs.findIndex(
   4613      tabData => tabData.closedId == aClosedId
   4614    );
   4615    if (closedIndex >= 0) {
   4616      return this.undoCloseTab(aSource, closedIndex, aTargetWindow);
   4617    }
   4618    throw Components.Exception(
   4619      "Invalid closedId: not in the closed tabs",
   4620      Cr.NS_ERROR_INVALID_ARG
   4621    );
   4622  },
   4623 
   4624  getPreferredRemoteType(url, aWindow, userContextId) {
   4625    return lazy.E10SUtils.getRemoteTypeForURI(
   4626      url,
   4627      aWindow.gMultiProcessBrowser,
   4628      aWindow.gFissionBrowser,
   4629      lazy.E10SUtils.DEFAULT_REMOTE_TYPE,
   4630      null,
   4631      lazy.E10SUtils.predictOriginAttributes({
   4632        window: aWindow,
   4633        userContextId,
   4634      })
   4635    );
   4636  },
   4637 
   4638  /**
   4639   * @param {Window|{sourceWindow: Window}|{sourceClosedId: number}|{sourceWindowId: string}} aSource
   4640   * @returns {WindowStateData}
   4641   */
   4642  _resolveClosedDataSource(aSource) {
   4643    let winData;
   4644    if (aSource instanceof Ci.nsIDOMWindow) {
   4645      winData = this.getWindowStateData(aSource);
   4646    } else if (aSource.sourceWindow instanceof Ci.nsIDOMWindow) {
   4647      winData = this.getWindowStateData(aSource.sourceWindow);
   4648    } else if (typeof aSource.sourceClosedId == "number") {
   4649      winData = this.getClosedWindowDataByClosedId(aSource.sourceClosedId);
   4650      if (!winData) {
   4651        throw Components.Exception(
   4652          "No such closed window",
   4653          Cr.NS_ERROR_INVALID_ARG
   4654        );
   4655      }
   4656    } else if (typeof aSource.sourceWindowId == "string") {
   4657      let win = this.getWindowById(aSource.sourceWindowId);
   4658      winData = this.getWindowStateData(win);
   4659    } else {
   4660      throw Components.Exception(
   4661        "Invalid source object",
   4662        Cr.NS_ERROR_INVALID_ARG
   4663      );
   4664    }
   4665    return winData;
   4666  },
   4667 
   4668  forgetClosedTab: function ssi_forgetClosedTab(aSource, aIndex) {
   4669    const winData = this._resolveClosedDataSource(aSource);
   4670    // default to the most-recently closed tab
   4671    aIndex = aIndex || 0;
   4672    if (!(aIndex in winData._closedTabs)) {
   4673      throw Components.Exception(
   4674        "Invalid index: not in the closed tabs",
   4675        Cr.NS_ERROR_INVALID_ARG
   4676      );
   4677    }
   4678 
   4679    // remove closed tab from the array
   4680    this.removeClosedTabData(winData, winData._closedTabs, aIndex);
   4681 
   4682    // Notify of changes to closed objects.
   4683    this._notifyOfClosedObjectsChange();
   4684  },
   4685 
   4686  forgetClosedTabGroup: function ssi_forgetClosedTabGroup(aSource, tabGroupId) {
   4687    const winData = this._resolveClosedDataSource(aSource);
   4688    let closedGroupIndex = winData.closedGroups.findIndex(
   4689      closedTabGroup => closedTabGroup.id == tabGroupId
   4690    );
   4691    // let closedTabGroup = this.getClosedTabGroup(aSource, tabGroupId);
   4692    if (closedGroupIndex < 0) {
   4693      throw Components.Exception(
   4694        "Closed tab group not found",
   4695        Cr.NS_ERROR_INVALID_ARG
   4696      );
   4697    }
   4698 
   4699    let closedGroup = winData.closedGroups[closedGroupIndex];
   4700    while (closedGroup.tabs.length) {
   4701      this.removeClosedTabData(winData, closedGroup.tabs, 0);
   4702    }
   4703    winData.closedGroups.splice(closedGroupIndex, 1);
   4704 
   4705    // Notify of changes to closed objects.
   4706    this._notifyOfClosedObjectsChange();
   4707  },
   4708 
   4709  /**
   4710   * @param {string} savedTabGroupId
   4711   */
   4712  forgetSavedTabGroup: function ssi_forgetSavedTabGroup(savedTabGroupId) {
   4713    let savedGroupIndex = this._savedGroups.findIndex(
   4714      savedTabGroup => savedTabGroup.id == savedTabGroupId
   4715    );
   4716    if (savedGroupIndex < 0) {
   4717      throw Components.Exception(
   4718        "Saved tab group not found",
   4719        Cr.NS_ERROR_INVALID_ARG
   4720      );
   4721    }
   4722 
   4723    let savedGroup = this._savedGroups[savedGroupIndex];
   4724    for (let i = 0; i < savedGroup.tabs.length; i++) {
   4725      this.removeClosedTabData({}, savedGroup.tabs, i);
   4726    }
   4727    this._savedGroups.splice(savedGroupIndex, 1);
   4728    this._notifyOfSavedTabGroupsChange();
   4729 
   4730    // Notify of changes to closed objects.
   4731    this._closedObjectsChanged = true;
   4732    this._notifyOfClosedObjectsChange();
   4733  },
   4734 
   4735  forgetClosedWindowById(aClosedId) {
   4736    // We don't keep any record for closed private windows so privateness is not relevant here
   4737    let closedIndex = this._closedWindows.findIndex(
   4738      windowState => windowState.closedId == aClosedId
   4739    );
   4740    if (closedIndex < 0) {
   4741      throw Components.Exception(
   4742        "Invalid closedId: not in the closed windows",
   4743        Cr.NS_ERROR_INVALID_ARG
   4744      );
   4745    }
   4746    this.forgetClosedWindow(closedIndex);
   4747  },
   4748 
   4749  forgetClosedTabById(aClosedId, aSourceOptions = {}) {
   4750    let sourceWindowsData;
   4751    let searchPrivateWindows = aSourceOptions.includePrivate ?? true;
   4752    if (
   4753      aSourceOptions instanceof Ci.nsIDOMWindow ||
   4754      "sourceWindowId" in aSourceOptions ||
   4755      "sourceClosedId" in aSourceOptions
   4756    ) {
   4757      sourceWindowsData = [this._resolveClosedDataSource(aSourceOptions)];
   4758    } else {
   4759      // Get the windows we'll look for the closed tab in, filtering out private
   4760      // windows if necessary
   4761      let browserWindows = Array.from(this._browserWindows);
   4762      sourceWindowsData = [];
   4763      for (let win of browserWindows) {
   4764        if (
   4765          !searchPrivateWindows &&
   4766          PrivateBrowsingUtils.isBrowserPrivate(win)
   4767        ) {
   4768          continue;
   4769        }
   4770        sourceWindowsData.push(this._windows[win.__SSi]);
   4771      }
   4772    }
   4773 
   4774    // See if the aClosedId matches a closed tab in any window data
   4775    for (let winData of sourceWindowsData) {
   4776      let closedTabs = this._getStateForClosedTabsAndClosedGroupTabs(winData);
   4777      let closedTabState = closedTabs.find(
   4778        tabData => tabData.closedId == aClosedId
   4779      );
   4780 
   4781      if (closedTabState) {
   4782        let { closedTabSet, closedTabIndex } =
   4783          this._getClosedTabStateFromUnifiedIndex(winData, closedTabState);
   4784        // remove closed tab from the array
   4785        this.removeClosedTabData(winData, closedTabSet, closedTabIndex);
   4786        // Notify of changes to closed objects.
   4787        this._notifyOfClosedObjectsChange();
   4788        return;
   4789      }
   4790    }
   4791 
   4792    throw Components.Exception(
   4793      "Invalid closedId: not found in the closed tabs of any window",
   4794      Cr.NS_ERROR_INVALID_ARG
   4795    );
   4796  },
   4797 
   4798  getClosedWindowCount: function ssi_getClosedWindowCount() {
   4799    return this._closedWindows.length;
   4800  },
   4801 
   4802  /**
   4803   * @returns {WindowStateData[]}
   4804   */
   4805  getClosedWindowData: function ssi_getClosedWindowData() {
   4806    let closedWindows = Cu.cloneInto(this._closedWindows, {});
   4807    for (let closedWinData of closedWindows) {
   4808      this._trimSavedTabGroupMetadataInClosedWindow(closedWinData);
   4809    }
   4810    return closedWindows;
   4811  },
   4812 
   4813  /**
   4814   * If a closed window has a saved tab group inside of it, the closed window's
   4815   * `groups` array entry will be a reference to a saved tab group entry.
   4816   * However, since saved tab groups contain a lot of extra and duplicate
   4817   * information, like their `tabs`, we only want to surface some of the
   4818   * metadata about the saved tab groups to outside clients.
   4819   *
   4820   * @param {WindowStateData} closedWinData
   4821   * @returns {void} mutates the argument `closedWinData`
   4822   */
   4823  _trimSavedTabGroupMetadataInClosedWindow(closedWinData) {
   4824    let abbreviatedGroups = closedWinData.groups?.map(tabGroup =>
   4825      lazy.TabGroupState.abbreviated(tabGroup)
   4826    );
   4827    closedWinData.groups = Cu.cloneInto(abbreviatedGroups, {});
   4828  },
   4829 
   4830  maybeDontRestoreTabs(aWindow) {
   4831    // Don't restore the tabs if we restore the session at startup
   4832    this._windows[aWindow.__SSi]._maybeDontRestoreTabs = true;
   4833  },
   4834 
   4835  isLastRestorableWindow() {
   4836    return (
   4837      Object.values(this._windows).filter(winData => !winData.isPrivate)
   4838        .length == 1 &&
   4839      !this._closedWindows.some(win => win._shouldRestore || false)
   4840    );
   4841  },
   4842 
   4843  undoCloseWindow: function ssi_undoCloseWindow(aIndex) {
   4844    if (!(aIndex in this._closedWindows)) {
   4845      throw Components.Exception(
   4846        "Invalid index: not in the closed windows",
   4847        Cr.NS_ERROR_INVALID_ARG
   4848      );
   4849    }
   4850    // reopen the window
   4851    let state = { windows: this._removeClosedWindow(aIndex) };
   4852    delete state.windows[0].closedAt; // Window is now open.
   4853 
   4854    // If any saved tab groups are in the closed window, convert the saved tab
   4855    // groups into open tab groups in the closed window and then forget the saved
   4856    // tab groups. This should have the effect of "moving" the saved tab groups
   4857    // into the window that's about to be restored.
   4858    this._trimSavedTabGroupMetadataInClosedWindow(state.windows[0]);
   4859    for (let tabGroup of state.windows[0].groups ?? []) {
   4860      if (this.getSavedTabGroup(tabGroup.id)) {
   4861        this.forgetSavedTabGroup(tabGroup.id);
   4862      }
   4863    }
   4864 
   4865    let window = this._openWindowWithState(state);
   4866    this.windowToFocus = window;
   4867    WINDOW_SHOWING_PROMISES.get(window).promise.then(win =>
   4868      this.restoreWindows(win, state, { overwriteTabs: true })
   4869    );
   4870 
   4871    // Notify of changes to closed objects.
   4872    this._notifyOfClosedObjectsChange();
   4873 
   4874    return window;
   4875  },
   4876 
   4877  forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
   4878    // default to the most-recently closed window
   4879    aIndex = aIndex || 0;
   4880    if (!(aIndex in this._closedWindows)) {
   4881      throw Components.Exception(
   4882        "Invalid index: not in the closed windows",
   4883        Cr.NS_ERROR_INVALID_ARG
   4884      );
   4885    }
   4886 
   4887    // remove closed window from the array
   4888    let winData = this._closedWindows[aIndex];
   4889    this._removeClosedWindow(aIndex);
   4890    this._saveableClosedWindowData.delete(winData);
   4891 
   4892    // Notify of changes to closed objects.
   4893    this._notifyOfClosedObjectsChange();
   4894  },
   4895 
   4896  getCustomWindowValue(aWindow, aKey) {
   4897    if ("__SSi" in aWindow) {
   4898      let data = this._windows[aWindow.__SSi].extData || {};
   4899      return data[aKey] || "";
   4900    }
   4901 
   4902    if (DyingWindowCache.has(aWindow)) {
   4903      let data = DyingWindowCache.get(aWindow).extData || {};
   4904      return data[aKey] || "";
   4905    }
   4906 
   4907    throw Components.Exception(
   4908      "Window is not tracked",
   4909      Cr.NS_ERROR_INVALID_ARG
   4910    );
   4911  },
   4912 
   4913  setCustomWindowValue(aWindow, aKey, aStringValue) {
   4914    if (typeof aStringValue != "string") {
   4915      throw new TypeError("setCustomWindowValue only accepts string values");
   4916    }
   4917 
   4918    if (!("__SSi" in aWindow)) {
   4919      throw Components.Exception(
   4920        "Window is not tracked",
   4921        Cr.NS_ERROR_INVALID_ARG
   4922      );
   4923    }
   4924    if (!this._windows[aWindow.__SSi].extData) {
   4925      this._windows[aWindow.__SSi].extData = {};
   4926    }
   4927    this._windows[aWindow.__SSi].extData[aKey] = aStringValue;
   4928    this.saveStateDelayed(aWindow);
   4929  },
   4930 
   4931  deleteCustomWindowValue(aWindow, aKey) {
   4932    if (
   4933      aWindow.__SSi &&
   4934      this._windows[aWindow.__SSi].extData &&
   4935      this._windows[aWindow.__SSi].extData[aKey]
   4936    ) {
   4937      delete this._windows[aWindow.__SSi].extData[aKey];
   4938    }
   4939    this.saveStateDelayed(aWindow);
   4940  },
   4941 
   4942  getCustomTabValue(aTab, aKey) {
   4943    return (TAB_CUSTOM_VALUES.get(aTab) || {})[aKey] || "";
   4944  },
   4945 
   4946  setCustomTabValue(aTab, aKey, aStringValue) {
   4947    if (typeof aStringValue != "string") {
   4948      throw new TypeError("setCustomTabValue only accepts string values");
   4949    }
   4950 
   4951    // If the tab hasn't been restored, then set the data there, otherwise we
   4952    // could lose newly added data.
   4953    if (!TAB_CUSTOM_VALUES.has(aTab)) {
   4954      TAB_CUSTOM_VALUES.set(aTab, {});
   4955    }
   4956 
   4957    TAB_CUSTOM_VALUES.get(aTab)[aKey] = aStringValue;
   4958    this.saveStateDelayed(aTab.ownerGlobal);
   4959  },
   4960 
   4961  deleteCustomTabValue(aTab, aKey) {
   4962    let state = TAB_CUSTOM_VALUES.get(aTab);
   4963    if (state && aKey in state) {
   4964      delete state[aKey];
   4965      this.saveStateDelayed(aTab.ownerGlobal);
   4966    }
   4967  },
   4968 
   4969  moveCustomTabValue(aFromTab, aToTab) {
   4970    let state = TAB_CUSTOM_VALUES.get(aFromTab);
   4971    if (state) {
   4972      TAB_CUSTOM_VALUES.set(aToTab, state);
   4973      TAB_CUSTOM_VALUES.delete(aFromTab);
   4974      // No saveStateDelayed calls for either window here, because the callers
   4975      // of moveCustomTabValue already call saveStateDelayed for both windows
   4976      // as needed, from onTabAdd and onTabRemove.
   4977    }
   4978  },
   4979 
   4980  /**
   4981   * Retrieves data specific to lazy-browser tabs.  If tab is not lazy,
   4982   * will return undefined.
   4983   *
   4984   * @param aTab (xul:tab)
   4985   *        The tabbrowser-tab the data is for.
   4986   * @param aKey (string)
   4987   *        The key which maps to the desired data.
   4988   */
   4989  getLazyTabValue(aTab, aKey) {
   4990    return (TAB_LAZY_STATES.get(aTab) || {})[aKey];
   4991  },
   4992 
   4993  getCustomGlobalValue(aKey) {
   4994    return this._globalState.get(aKey);
   4995  },
   4996 
   4997  setCustomGlobalValue(aKey, aStringValue) {
   4998    if (typeof aStringValue != "string") {
   4999      throw new TypeError("setCustomGlobalValue only accepts string values");
   5000    }
   5001 
   5002    this._globalState.set(aKey, aStringValue);
   5003    this.saveStateDelayed();
   5004  },
   5005 
   5006  deleteCustomGlobalValue(aKey) {
   5007    this._globalState.delete(aKey);
   5008    this.saveStateDelayed();
   5009  },
   5010 
   5011  /**
   5012   * Undoes the closing of a tab or window which corresponds
   5013   * to the closedId passed in.
   5014   *
   5015   * @param {integer} aClosedId
   5016   *        The closedId of the tab or window
   5017   * @param {boolean} [aIncludePrivate = true]
   5018   *        Whether to restore private tabs or windows. Defaults to true
   5019   * @param {Window} [aTargetWindow]
   5020   *        When aClosedId is for a closed tab, which window to re-open the tab into.
   5021   *        Defaults to current (topWindow).
   5022   *
   5023   * @returns a tab or window object
   5024   */
   5025  undoCloseById(aClosedId, aIncludePrivate = true, aTargetWindow) {
   5026    // Check if we are re-opening a window first.
   5027    for (let i = 0, l = this._closedWindows.length; i < l; i++) {
   5028      if (this._closedWindows[i].closedId == aClosedId) {
   5029        return this.undoCloseWindow(i);
   5030      }
   5031    }
   5032 
   5033    // See if the aCloseId matches a tab in an open window
   5034    // Check for a tab.
   5035    for (let sourceWindow of Services.wm.getEnumerator("navigator:browser")) {
   5036      if (
   5037        !aIncludePrivate &&
   5038        PrivateBrowsingUtils.isWindowPrivate(sourceWindow)
   5039      ) {
   5040        continue;
   5041      }
   5042      let windowState = this._windows[sourceWindow.__SSi];
   5043      if (windowState) {
   5044        let closedTabs =
   5045          this._getStateForClosedTabsAndClosedGroupTabs(windowState);
   5046        for (let j = 0, l = closedTabs.length; j < l; j++) {
   5047          if (closedTabs[j].closedId == aClosedId) {
   5048            return this.undoCloseTab(sourceWindow, j, aTargetWindow);
   5049          }
   5050        }
   5051      }
   5052    }
   5053 
   5054    // Neither a tab nor a window was found, return undefined and let the caller decide what to do about it.
   5055    return undefined;
   5056  },
   5057 
   5058  /**
   5059   * Updates the label and icon for a <xul:tab> using the data from
   5060   * tabData.
   5061   *
   5062   * @param tab
   5063   *        The <xul:tab> to update.
   5064   * @param tabData (optional)
   5065   *        The tabData to use to update the tab. If the argument is
   5066   *        not supplied, the data will be retrieved from the cache.
   5067   */
   5068  updateTabLabelAndIcon(tab, tabData = null) {
   5069    if (tab.hasAttribute("customizemode")) {
   5070      return;
   5071    }
   5072 
   5073    let browser = tab.linkedBrowser;
   5074    let win = browser.ownerGlobal;
   5075 
   5076    if (!tabData) {
   5077      tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
   5078      if (!tabData) {
   5079        throw new Error("tabData not found for given tab");
   5080      }
   5081    }
   5082 
   5083    let activePageData = tabData.entries[tabData.index - 1] || null;
   5084 
   5085    // If the page has a title, set it.
   5086    if (activePageData) {
   5087      if (activePageData.title && activePageData.title != activePageData.url) {
   5088        win.gBrowser.setInitialTabTitle(tab, activePageData.title, {
   5089          isContentTitle: true,
   5090        });
   5091      } else {
   5092        win.gBrowser.setInitialTabTitle(tab, activePageData.url);
   5093      }
   5094    }
   5095 
   5096    // Restore the tab icon.
   5097    if ("image" in tabData) {
   5098      // We know that about:blank is safe to load in any remote type. Since
   5099      // SessionStore is triggered with about:blank, there must be a process
   5100      // flip. We will ignore the first about:blank load to prevent resetting the
   5101      // favicon that we have set earlier to avoid flickering and improve
   5102      // perceived performance.
   5103      if (
   5104        !activePageData ||
   5105        (activePageData && activePageData.url != "about:blank")
   5106      ) {
   5107        win.gBrowser.setIcon(tab, tabData.image);
   5108      }
   5109      lazy.TabStateCache.update(browser.permanentKey, {
   5110        image: null,
   5111      });
   5112    }
   5113  },
   5114 
   5115  // This method deletes all the closedTabs matching userContextId.
   5116  _forgetTabsWithUserContextId(userContextId) {
   5117    for (let window of Services.wm.getEnumerator("navigator:browser")) {
   5118      let windowState = this._windows[window.__SSi];
   5119      if (windowState) {
   5120        // In order to remove the tabs in the correct order, we store the
   5121        // indexes, into an array, then we revert the array and remove closed
   5122        // data from the last one going backward.
   5123        let indexes = [];
   5124        windowState._closedTabs.forEach((closedTab, index) => {
   5125          if (closedTab.state.userContextId == userContextId) {
   5126            indexes.push(index);
   5127          }
   5128        });
   5129 
   5130        for (let index of indexes.reverse()) {
   5131          this.removeClosedTabData(windowState, windowState._closedTabs, index);
   5132        }
   5133      }
   5134    }
   5135 
   5136    // Notify of changes to closed objects.
   5137    this._notifyOfClosedObjectsChange();
   5138  },
   5139 
   5140  /**
   5141   * Restores the session state stored in LastSession. This will attempt
   5142   * to merge data into the current session. If a window was opened at startup
   5143   * with pinned tab(s), then the remaining data from the previous session for
   5144   * that window will be opened into that window. Otherwise new windows will
   5145   * be opened.
   5146   */
   5147  restoreLastSession: function ssi_restoreLastSession() {
   5148    // Use the public getter since it also checks PB mode
   5149    if (!this.canRestoreLastSession) {
   5150      throw Components.Exception("Last session can not be restored");
   5151    }
   5152 
   5153    Services.obs.notifyObservers(null, NOTIFY_INITIATING_MANUAL_RESTORE);
   5154 
   5155    // First collect each window with its id...
   5156    let windows = {};
   5157    for (let window of this._browserWindows) {
   5158      if (window.__SS_lastSessionWindowID) {
   5159        windows[window.__SS_lastSessionWindowID] = window;
   5160      }
   5161    }
   5162 
   5163    let lastSessionState = LastSession.getState();
   5164 
   5165    // This shouldn't ever be the case...
   5166    if (!lastSessionState.windows.length) {
   5167      throw Components.Exception(
   5168        "lastSessionState has no windows",
   5169        Cr.NS_ERROR_UNEXPECTED
   5170      );
   5171    }
   5172 
   5173    // We're technically doing a restore, so set things up so we send the
   5174    // notification when we're done. We want to send "sessionstore-browser-state-restored".
   5175    this._restoreCount = lastSessionState.windows.length;
   5176    this._browserSetState = true;
   5177 
   5178    // We want to re-use the last opened window instead of opening a new one in
   5179    // the case where it's "empty" and not associated with a window in the session.
   5180    // We will do more processing via _prepWindowToRestoreInto if we need to use
   5181    // the lastWindow.
   5182    let lastWindow = this._getTopWindow();
   5183    let canUseLastWindow = lastWindow && !lastWindow.__SS_lastSessionWindowID;
   5184 
   5185    // global data must be restored before restoreWindow is called so that
   5186    // it happens before observers are notified
   5187    this._globalState.setFromState(lastSessionState);
   5188 
   5189    let openWindows = [];
   5190    let windowsToOpen = [];
   5191 
   5192    // Restore session cookies.
   5193    lazy.SessionCookies.restore(lastSessionState.cookies || []);
   5194 
   5195    // Restore into windows or open new ones as needed.
   5196    for (let i = 0; i < lastSessionState.windows.length; i++) {
   5197      let winState = lastSessionState.windows[i];
   5198 
   5199      // If we're restoring multiple times without
   5200      // Firefox restarting, we need to remove
   5201      // the window being restored from "previously closed windows"
   5202      if (this._restoreWithoutRestart) {
   5203        let restoreIndex = this._closedWindows.findIndex(win => {
   5204          return win.closedId == winState.closedId;
   5205        });
   5206        if (restoreIndex > -1) {
   5207          this._closedWindows.splice(restoreIndex, 1);
   5208        }
   5209      }
   5210 
   5211      let lastSessionWindowID = winState.__lastSessionWindowID;
   5212      // delete lastSessionWindowID so we don't add that to the window again
   5213      delete winState.__lastSessionWindowID;
   5214 
   5215      // See if we can use an open window. First try one that is associated with
   5216      // the state we're trying to restore and then fallback to the last selected
   5217      // window.
   5218      let windowToUse = windows[lastSessionWindowID];
   5219      if (!windowToUse && canUseLastWindow) {
   5220        windowToUse = lastWindow;
   5221        canUseLastWindow = false;
   5222      }
   5223 
   5224      let [canUseWindow, canOverwriteTabs] =
   5225        this._prepWindowToRestoreInto(windowToUse);
   5226 
   5227      // If there's a window already open that we can restore into, use that
   5228      if (canUseWindow) {
   5229        if (!PERSIST_SESSIONS) {
   5230          // Since we're not overwriting existing tabs, we want to merge _closedTabs,
   5231          // putting existing ones first. Then make sure we're respecting the max pref.
   5232          if (winState._closedTabs && winState._closedTabs.length) {
   5233            let curWinState = this._windows[windowToUse.__SSi];
   5234            curWinState._closedTabs = curWinState._closedTabs.concat(
   5235              winState._closedTabs
   5236            );
   5237            curWinState._closedTabs.splice(
   5238              this._max_tabs_undo,
   5239              curWinState._closedTabs.length
   5240            );
   5241          }
   5242        }
   5243        // We don't restore window right away, just store its data.
   5244        // Later, these windows will be restored with newly opened windows.
   5245        this._updateWindowRestoreState(windowToUse, {
   5246          windows: [winState],
   5247          options: { overwriteTabs: canOverwriteTabs },
   5248        });
   5249        openWindows.push(windowToUse);
   5250      } else {
   5251        windowsToOpen.push(winState);
   5252      }
   5253    }
   5254 
   5255    // Actually restore windows in reversed z-order.
   5256    this._openWindows({ windows: windowsToOpen }).then(openedWindows =>
   5257      this._restoreWindowsInReversedZOrder(openWindows.concat(openedWindows))
   5258    );
   5259 
   5260    if (this._restoreWithoutRestart) {
   5261      this.removeDuplicateClosedWindows(lastSessionState);
   5262    }
   5263 
   5264    // Merge closed windows from this session with ones from last session
   5265    if (lastSessionState._closedWindows) {
   5266      // reset window closedIds and any references to them from closed tabs
   5267      for (let closedWindow of lastSessionState._closedWindows) {
   5268        closedWindow.closedId = this._nextClosedId++;
   5269        if (closedWindow._closedTabs?.length) {
   5270          this._resetClosedTabIds(
   5271            closedWindow._closedTabs,
   5272            closedWindow.closedId
   5273          );
   5274        }
   5275      }
   5276      this._closedWindows = this._closedWindows.concat(
   5277        lastSessionState._closedWindows
   5278      );
   5279      this._capClosedWindows();
   5280      this._closedObjectsChanged = true;
   5281    }
   5282 
   5283    lazy.DevToolsShim.restoreDevToolsSession(lastSessionState);
   5284 
   5285    // When the deferred session was created, open tab groups were converted to saved groups.
   5286    // Now that they have been restored, they need to be removed from the saved groups list.
   5287    let groupsToRemove = this._savedGroups.filter(
   5288      group => group.removeAfterRestore
   5289    );
   5290    for (let group of groupsToRemove) {
   5291      this.forgetSavedTabGroup(group.id);
   5292    }
   5293 
   5294    // Set data that persists between sessions
   5295    this._recentCrashes =
   5296      (lastSessionState.session && lastSessionState.session.recentCrashes) || 0;
   5297 
   5298    // Update the session start time using the restored session state.
   5299    this._updateSessionStartTime(lastSessionState);
   5300 
   5301    LastSession.clear();
   5302 
   5303    // Notify of changes to closed objects.
   5304    this._notifyOfClosedObjectsChange();
   5305  },
   5306 
   5307  /**
   5308   * There might be duplicates in these two arrays if we
   5309   * restore multiple times without restarting in between.
   5310   * We will keep the contents of the more recent _closedWindows array
   5311   *
   5312   * @param lastSessionState
   5313   * An object containing information about the previous browsing session
   5314   */
   5315  removeDuplicateClosedWindows(lastSessionState) {
   5316    // A set of closedIDs for the most recent list of closed windows
   5317    let currentClosedIds = new Set(
   5318      this._closedWindows.map(window => window.closedId)
   5319    );
   5320 
   5321    // Remove closed windows that are present in both current and last session
   5322    lastSessionState._closedWindows = lastSessionState._closedWindows.filter(
   5323      win => !currentClosedIds.has(win.closedId)
   5324    );
   5325  },
   5326 
   5327  /**
   5328   * Revive a crashed tab and restore its state from before it crashed.
   5329   *
   5330   * @param aTab
   5331   *        A <xul:tab> linked to a crashed browser. This is a no-op if the
   5332   *        browser hasn't actually crashed, or is not associated with a tab.
   5333   *        This function will also throw if the browser happens to be remote.
   5334   */
   5335  reviveCrashedTab(aTab) {
   5336    if (!aTab) {
   5337      throw new Error(
   5338        "SessionStore.reviveCrashedTab expected a tab, but got null."
   5339      );
   5340    }
   5341 
   5342    let browser = aTab.linkedBrowser;
   5343    if (!this._crashedBrowsers.has(browser.permanentKey)) {
   5344      return;
   5345    }
   5346 
   5347    // Sanity check - the browser to be revived should not be remote
   5348    // at this point.
   5349    if (browser.isRemoteBrowser) {
   5350      throw new Error(
   5351        "SessionStore.reviveCrashedTab: " +
   5352          "Somehow a crashed browser is still remote."
   5353      );
   5354    }
   5355 
   5356    // We put the browser at about:blank in case the user is
   5357    // restoring tabs on demand. This way, the user won't see
   5358    // a flash of the about:tabcrashed page after selecting
   5359    // the revived tab.
   5360    aTab.removeAttribute("crashed");
   5361 
   5362    browser.loadURI(lazy.blankURI, {
   5363      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
   5364        userContextId: aTab.userContextId,
   5365      }),
   5366      remoteTypeOverride: lazy.E10SUtils.NOT_REMOTE,
   5367    });
   5368 
   5369    let data = lazy.TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
   5370    this.restoreTab(aTab, data, {
   5371      forceOnDemand: true,
   5372    });
   5373  },
   5374 
   5375  /**
   5376   * Revive all crashed tabs and reset the crashed tabs count to 0.
   5377   */
   5378  reviveAllCrashedTabs() {
   5379    for (let window of Services.wm.getEnumerator("navigator:browser")) {
   5380      for (let tab of window.gBrowser.tabs) {
   5381        this.reviveCrashedTab(tab);
   5382      }
   5383    }
   5384  },
   5385 
   5386  /**
   5387   * Retrieves the latest session history information for a tab. The cached data
   5388   * is returned immediately, but a callback may be provided that supplies
   5389   * up-to-date data when or if it is available. The callback is passed a single
   5390   * argument with data in the same format as the return value.
   5391   *
   5392   * @param tab tab to retrieve the session history for
   5393   * @param updatedCallback function to call with updated data as the single argument
   5394   * @returns a object containing 'index' specifying the current index, and an
   5395   * array 'entries' containing an object for each history item.
   5396   */
   5397  getSessionHistory(tab, updatedCallback) {
   5398    if (updatedCallback) {
   5399      lazy.TabStateFlusher.flush(tab.linkedBrowser).then(() => {
   5400        let sessionHistory = this.getSessionHistory(tab);
   5401        if (sessionHistory) {
   5402          updatedCallback(sessionHistory);
   5403        }
   5404      });
   5405    }
   5406 
   5407    // Don't continue if the tab was closed before TabStateFlusher.flush resolves.
   5408    if (tab.linkedBrowser) {
   5409      let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
   5410      return { index: tabState.index - 1, entries: tabState.entries };
   5411    }
   5412    return null;
   5413  },
   5414 
   5415  /**
   5416   * See if aWindow is usable for use when restoring a previous session via
   5417   * restoreLastSession. If usable, prepare it for use.
   5418   *
   5419   * @param aWindow
   5420   *        the window to inspect & prepare
   5421   * @returns [canUseWindow, canOverwriteTabs]
   5422   *          canUseWindow: can the window be used to restore into
   5423   *          canOverwriteTabs: all of the current tabs are home pages and we
   5424   *                            can overwrite them
   5425   */
   5426  _prepWindowToRestoreInto: function ssi_prepWindowToRestoreInto(aWindow) {
   5427    if (!aWindow) {
   5428      return [false, false];
   5429    }
   5430 
   5431    // We might be able to overwrite the existing tabs instead of just adding
   5432    // the previous session's tabs to the end. This will be set if possible.
   5433    let canOverwriteTabs = false;
   5434 
   5435    // Look at the open tabs in comparison to home pages. If all the tabs are
   5436    // home pages then we'll end up overwriting all of them. Otherwise we'll
   5437    // just close the tabs that match home pages. Tabs with the about:blank
   5438    // URI will always be overwritten.
   5439    let homePages = ["about:blank"];
   5440    let removableTabs = [];
   5441    let tabbrowser = aWindow.gBrowser;
   5442    let startupPref = this._prefBranch.getIntPref("startup.page");
   5443    if (startupPref == 1) {
   5444      homePages = homePages.concat(lazy.HomePage.get(aWindow).split("|"));
   5445    }
   5446 
   5447    for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
   5448      let tab = tabbrowser.tabs[i];
   5449      if (homePages.includes(tab.linkedBrowser.currentURI.spec)) {
   5450        removableTabs.push(tab);
   5451      }
   5452    }
   5453 
   5454    if (
   5455      tabbrowser.tabs.length > tabbrowser.visibleTabs.length &&
   5456      tabbrowser.visibleTabs.length === removableTabs.length
   5457    ) {
   5458      // If all the visible tabs are also removable and the selected tab is hidden or removeable, we will later remove
   5459      // all "removable" tabs causing the browser to automatically close because the only tab left is hidden.
   5460      // To prevent the browser from automatically closing, we will leave one other visible tab open.
   5461      removableTabs.shift();
   5462    }
   5463 
   5464    if (tabbrowser.tabs.length == removableTabs.length) {
   5465      canOverwriteTabs = true;
   5466    } else {
   5467      // If we're not overwriting all of the tabs, then close the home tabs.
   5468      for (let i = removableTabs.length - 1; i >= 0; i--) {
   5469        tabbrowser.removeTab(removableTabs.pop(), { animate: false });
   5470      }
   5471    }
   5472 
   5473    return [true, canOverwriteTabs];
   5474  },
   5475 
   5476  /* ........ Saving Functionality .............. */
   5477 
   5478  /**
   5479   * Store window dimensions, visibility, sidebar
   5480   *
   5481   * @param aWindow
   5482   *        Window reference
   5483   */
   5484  _updateWindowFeatures: function ssi_updateWindowFeatures(aWindow) {
   5485    var winData = this._windows[aWindow.__SSi];
   5486 
   5487    WINDOW_ATTRIBUTES.forEach(function (aAttr) {
   5488      winData[aAttr] = this._getWindowDimension(aWindow, aAttr);
   5489    }, this);
   5490 
   5491    if (winData.sizemode != "minimized") {
   5492      winData.sizemodeBeforeMinimized = winData.sizemode;
   5493    }
   5494 
   5495    var hidden = WINDOW_HIDEABLE_FEATURES.filter(function (aItem) {
   5496      return aWindow[aItem] && !aWindow[aItem].visible;
   5497    });
   5498    if (hidden.length) {
   5499      winData.hidden = hidden.join(",");
   5500    } else if (winData.hidden) {
   5501      delete winData.hidden;
   5502    }
   5503 
   5504    const sidebarUIState = aWindow.SidebarController.getUIState();
   5505    if (sidebarUIState) {
   5506      winData.sidebar = structuredClone(sidebarUIState);
   5507    }
   5508 
   5509    let workspaceID = aWindow.getWorkspaceID();
   5510    if (workspaceID) {
   5511      winData.workspaceID = workspaceID;
   5512    }
   5513 
   5514    winData.isAIWindow = lazy.AIWindow.isAIWindowActiveAndEnabled(aWindow);
   5515  },
   5516 
   5517  /**
   5518   * gather session data as object
   5519   *
   5520   * @param aUpdateAll
   5521   *        Bool update all windows
   5522   * @returns object
   5523   */
   5524  getCurrentState(aUpdateAll) {
   5525    this._handleClosedWindows().then(() => {
   5526      this._notifyOfClosedObjectsChange();
   5527    });
   5528 
   5529    var activeWindow = this._getTopWindow();
   5530 
   5531    let timerId = Glean.sessionRestore.collectAllWindowsData.start();
   5532    if (lazy.RunState.isRunning) {
   5533      // update the data for all windows with activities since the last save operation.
   5534      let index = 0;
   5535      for (let window of this._orderedBrowserWindows) {
   5536        if (!this._isWindowLoaded(window)) {
   5537          // window data is still in _statesToRestore
   5538          continue;
   5539        }
   5540        if (aUpdateAll || DirtyWindows.has(window) || window == activeWindow) {
   5541          this._collectWindowData(window);
   5542        } else {
   5543          // always update the window features (whose change alone never triggers a save operation)
   5544          this._updateWindowFeatures(window);
   5545        }
   5546        this._windows[window.__SSi].zIndex = ++index;
   5547      }
   5548      DirtyWindows.clear();
   5549    }
   5550    Glean.sessionRestore.collectAllWindowsData.stopAndAccumulate(timerId);
   5551 
   5552    // An array that at the end will hold all current window data.
   5553    var total = [];
   5554    // The ids of all windows contained in 'total' in the same order.
   5555    var ids = [];
   5556    // The number of window that are _not_ popups.
   5557    var nonPopupCount = 0;
   5558    var ix;
   5559 
   5560    // collect the data for all windows
   5561    for (ix in this._windows) {
   5562      if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) {
   5563        // window data is still in _statesToRestore
   5564        continue;
   5565      }
   5566      total.push(this._windows[ix]);
   5567      ids.push(ix);
   5568      if (!this._windows[ix].isPopup) {
   5569        nonPopupCount++;
   5570      }
   5571    }
   5572 
   5573    // collect the data for all windows yet to be restored
   5574    for (ix in this._statesToRestore) {
   5575      for (let winData of this._statesToRestore[ix].windows) {
   5576        total.push(winData);
   5577        if (!winData.isPopup) {
   5578          nonPopupCount++;
   5579        }
   5580      }
   5581    }
   5582 
   5583    // shallow copy this._closedWindows to preserve current state
   5584    let lastClosedWindowsCopy = this._closedWindows.slice();
   5585 
   5586    if (AppConstants.platform != "macosx") {
   5587      // If no non-popup browser window remains open, return the state of the last
   5588      // closed window(s). We only want to do this when we're actually "ending"
   5589      // the session.
   5590      // XXXzpao We should do this for _restoreLastWindow == true, but that has
   5591      //        its own check for popups. c.f. bug 597619
   5592      if (
   5593        nonPopupCount == 0 &&
   5594        !!lastClosedWindowsCopy.length &&
   5595        lazy.RunState.isQuitting
   5596      ) {
   5597        // prepend the last non-popup browser window, so that if the user loads more tabs
   5598        // at startup we don't accidentally add them to a popup window
   5599        do {
   5600          total.unshift(lastClosedWindowsCopy.shift());
   5601        } while (total[0].isPopup && lastClosedWindowsCopy.length);
   5602      }
   5603    }
   5604 
   5605    if (activeWindow) {
   5606      this.activeWindowSSiCache = activeWindow.__SSi || "";
   5607    }
   5608    ix = ids.indexOf(this.activeWindowSSiCache);
   5609    // We don't want to restore focus to a minimized window or a window which had all its
   5610    // tabs stripped out (doesn't exist).
   5611    if (ix != -1 && total[ix] && total[ix].sizemode == "minimized") {
   5612      ix = -1;
   5613    }
   5614 
   5615    let session = {
   5616      lastUpdate: Date.now(),
   5617      startTime: this._sessionStartTime,
   5618      recentCrashes: this._recentCrashes,
   5619    };
   5620 
   5621    let state = {
   5622      version: ["sessionrestore", FORMAT_VERSION],
   5623      windows: total,
   5624      selectedWindow: ix + 1,
   5625      _closedWindows: lastClosedWindowsCopy,
   5626      savedGroups: this._savedGroups,
   5627      session,
   5628      global: this._globalState.getState(),
   5629    };
   5630 
   5631    // Collect and store session cookies.
   5632    state.cookies = lazy.SessionCookies.collect();
   5633 
   5634    lazy.DevToolsShim.saveDevToolsSession(state);
   5635 
   5636    // Persist the last session if we deferred restoring it
   5637    if (LastSession.canRestore) {
   5638      state.lastSessionState = LastSession.getState();
   5639    }
   5640 
   5641    // If we were called by the SessionSaver and started with only a private
   5642    // window we want to pass the deferred initial state to not lose the
   5643    // previous session.
   5644    if (this._deferredInitialState) {
   5645      state.deferredInitialState = this._deferredInitialState;
   5646    }
   5647 
   5648    return state;
   5649  },
   5650 
   5651  /**
   5652   * serialize session data for a window
   5653   *
   5654   * @param {Window} aWindow
   5655   *        Window reference
   5656   * @returns {{windows: [WindowStateData]}}
   5657   */
   5658  _getWindowState: function ssi_getWindowState(aWindow) {
   5659    if (!this._isWindowLoaded(aWindow)) {
   5660      return this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)];
   5661    }
   5662 
   5663    if (lazy.RunState.isRunning) {
   5664      this._collectWindowData(aWindow);
   5665    }
   5666 
   5667    return { windows: [this._windows[aWindow.__SSi]] };
   5668  },
   5669 
   5670  /**
   5671   * Retrieves window data for an active session.
   5672   *
   5673   * @param {Window} aWindow
   5674   * @returns {WindowStateData}
   5675   * @throws {Error} if `aWindow` is not being managed in the session store.
   5676   */
   5677  getWindowStateData: function ssi_getWindowStateData(aWindow) {
   5678    if (!aWindow.__SSi || !(aWindow.__SSi in this._windows)) {
   5679      throw Components.Exception(
   5680        "Window is not tracked",
   5681        Cr.NS_ERROR_INVALID_ARG
   5682      );
   5683    }
   5684 
   5685    return this._windows[aWindow.__SSi];
   5686  },
   5687 
   5688  /**
   5689   * Gathers data about a window and its tabs, and updates its
   5690   * entry in this._windows.
   5691   *
   5692   * @param aWindow
   5693   *        Window references.
   5694   * @returns a Map mapping the browser tabs from aWindow to the tab
   5695   *          entry that was put into the window data in this._windows.
   5696   */
   5697  _collectWindowData: function ssi_collectWindowData(aWindow) {
   5698    let tabMap = new Map();
   5699 
   5700    if (!this._isWindowLoaded(aWindow)) {
   5701      return tabMap;
   5702    }
   5703 
   5704    let tabbrowser = aWindow.gBrowser;
   5705    let tabs = tabbrowser.tabs;
   5706    /** @type {WindowStateData} */
   5707    let winData = this._windows[aWindow.__SSi];
   5708    let tabsData = (winData.tabs = []);
   5709 
   5710    // update the internal state data for this window
   5711    for (let tab of tabs) {
   5712      if (tab == aWindow.FirefoxViewHandler.tab) {
   5713        continue;
   5714      }
   5715      let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
   5716      tabMap.set(tab, tabData);
   5717      tabsData.push(tabData);
   5718    }
   5719 
   5720    // update tab group state for this window
   5721    winData.groups = [];
   5722    for (let tabGroup of aWindow.gBrowser.tabGroups) {
   5723      let tabGroupData = lazy.TabGroupState.collect(tabGroup);
   5724      winData.groups.push(tabGroupData);
   5725    }
   5726 
   5727    let selectedIndex = tabbrowser.tabbox.selectedIndex + 1;
   5728    // We don't store the Firefox View tab in Session Store, so if it was the last selected "tab" when
   5729    // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
   5730    // since it's only inserted into the tab strip after it's selected).
   5731    if (aWindow.FirefoxViewHandler.tab?.selected) {
   5732      selectedIndex = 1;
   5733      winData.title = tabbrowser.tabs[0].label;
   5734    }
   5735    winData.selected = selectedIndex;
   5736 
   5737    this._updateWindowFeatures(aWindow);
   5738 
   5739    // Make sure we keep __SS_lastSessionWindowID around for cases like entering
   5740    // or leaving PB mode.
   5741    if (aWindow.__SS_lastSessionWindowID) {
   5742      this._windows[aWindow.__SSi].__lastSessionWindowID =
   5743        aWindow.__SS_lastSessionWindowID;
   5744    }
   5745 
   5746    DirtyWindows.remove(aWindow);
   5747    return tabMap;
   5748  },
   5749 
   5750  /* ........ Restoring Functionality .............. */
   5751 
   5752  /**
   5753   * Open windows with data
   5754   *
   5755   * @param root
   5756   *        Windows data
   5757   * @returns a promise resolved when all windows have been opened
   5758   */
   5759  _openWindows(root) {
   5760    let windowsOpened = [];
   5761    for (let winData of root.windows) {
   5762      if (!winData || !winData.tabs || !winData.tabs[0]) {
   5763        this._log.debug(`_openWindows, skipping window with no tabs data`);
   5764        this._restoreCount--;
   5765        continue;
   5766      }
   5767      windowsOpened.push(this._openWindowWithState({ windows: [winData] }));
   5768    }
   5769    let windowOpenedPromises = [];
   5770    for (const openedWindow of windowsOpened) {
   5771      let deferred = WINDOW_SHOWING_PROMISES.get(openedWindow);
   5772      windowOpenedPromises.push(deferred.promise);
   5773    }
   5774    return Promise.all(windowOpenedPromises);
   5775  },
   5776 
   5777  /**
   5778   * Reset closedId's from previous sessions to ensure these IDs are unique
   5779   *
   5780   * @param tabData
   5781   *        an array of data to be restored
   5782   * @param {string} windowId
   5783   *        The SessionStore id for the window these tabs should be associated with
   5784   * @returns the updated tabData array
   5785   */
   5786  _resetClosedTabIds(tabData, windowId) {
   5787    for (let entry of tabData) {
   5788      entry.closedId = this._nextClosedId++;
   5789      entry.sourceWindowId = windowId;
   5790    }
   5791    return tabData;
   5792  },
   5793  /**
   5794   * restore features to a single window
   5795   *
   5796   * @param aWindow
   5797   *        Window reference to the window to use for restoration
   5798   * @param winData
   5799   *        JS object
   5800   * @param aOptions.overwriteTabs
   5801   *        to overwrite existing tabs w/ new ones
   5802   * @param aOptions.firstWindow
   5803   *        if this is the first non-private window we're
   5804   *        restoring in this session, that might open an
   5805   *        external link as well
   5806   */
   5807  restoreWindow: function ssi_restoreWindow(aWindow, winData, aOptions = {}) {
   5808    let overwriteTabs = aOptions && aOptions.overwriteTabs;
   5809    let firstWindow = aOptions && aOptions.firstWindow;
   5810 
   5811    this.restoreSidebar(aWindow, winData.sidebar, winData.isPopup);
   5812 
   5813    // initialize window if necessary
   5814    if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) {
   5815      this.onLoad(aWindow);
   5816    }
   5817 
   5818    let timerId = Glean.sessionRestore.restoreWindow.start();
   5819 
   5820    // We're not returning from this before we end up calling restoreTabs
   5821    // for this window, so make sure we send the SSWindowStateBusy event.
   5822    this._sendWindowRestoringNotification(aWindow);
   5823    this._setWindowStateBusy(aWindow);
   5824 
   5825    if (winData.workspaceID && lazy.gRestoreWindowsToVirtualDesktop) {
   5826      this._log.debug(`Moving window to workspace: ${winData.workspaceID}`);
   5827      aWindow.moveToWorkspace(winData.workspaceID);
   5828    }
   5829 
   5830    if (!winData.tabs) {
   5831      winData.tabs = [];
   5832      // don't restore a single blank tab when we've had an external
   5833      // URL passed in for loading at startup (cf. bug 357419)
   5834    } else if (
   5835      firstWindow &&
   5836      !overwriteTabs &&
   5837      winData.tabs.length == 1 &&
   5838      (!winData.tabs[0].entries || !winData.tabs[0].entries.length)
   5839    ) {
   5840      winData.tabs = [];
   5841    }
   5842 
   5843    // See SessionStoreInternal.restoreTabs for a description of what
   5844    // selectTab represents.
   5845    let selectTab = 0;
   5846    if (overwriteTabs) {
   5847      selectTab = parseInt(winData.selected || 1, 10);
   5848      selectTab = Math.max(selectTab, 1);
   5849      selectTab = Math.min(selectTab, winData.tabs.length);
   5850    }
   5851 
   5852    let tabbrowser = aWindow.gBrowser;
   5853 
   5854    // disable smooth scrolling while adding, moving, removing and selecting tabs
   5855    let arrowScrollbox = tabbrowser.tabContainer.arrowScrollbox;
   5856    let smoothScroll = arrowScrollbox.smoothScroll;
   5857    arrowScrollbox.smoothScroll = false;
   5858 
   5859    // We need to keep track of the initially open tabs so that they
   5860    // can be moved to the end of the restored tabs.
   5861    let initialTabs;
   5862    if (!overwriteTabs && firstWindow) {
   5863      initialTabs = Array.from(tabbrowser.tabs);
   5864    }
   5865 
   5866    // Get rid of tabs that aren't needed anymore.
   5867    if (overwriteTabs) {
   5868      for (let i = tabbrowser.browsers.length - 1; i >= 0; i--) {
   5869        if (!tabbrowser.tabs[i].selected) {
   5870          tabbrowser.removeTab(tabbrowser.tabs[i]);
   5871        }
   5872      }
   5873    }
   5874 
   5875    let restoreTabsLazily =
   5876      this._prefBranch.getBoolPref("sessionstore.restore_tabs_lazily") &&
   5877      this._restore_on_demand;
   5878 
   5879    this._log.debug(
   5880      `restoreWindow, will restore ${winData.tabs.length} tabs and ${
   5881        winData.groups?.length ?? 0
   5882      } tab groups, restoreTabsLazily: ${restoreTabsLazily}`
   5883    );
   5884    if (winData.tabs.length) {
   5885      var tabs = tabbrowser.createTabsForSessionRestore(
   5886        restoreTabsLazily,
   5887        selectTab,
   5888        winData.tabs,
   5889        winData.groups ?? []
   5890      );
   5891      this._log.debug(
   5892        `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
   5893      );
   5894      // If restoring this window resulted in reopening any saved tab groups,
   5895      // we no longer need to track those saved tab groups.
   5896      const openTabGroupIdsInWindow = new Set(
   5897        tabbrowser.tabGroups.map(group => group.id)
   5898      );
   5899      this._savedGroups = this._savedGroups.filter(
   5900        savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id)
   5901      );
   5902    }
   5903 
   5904    // Move the originally open tabs to the end.
   5905    if (initialTabs) {
   5906      let endPosition = tabbrowser.tabs.length - 1;
   5907      for (let i = 0; i < initialTabs.length; i++) {
   5908        tabbrowser.unpinTab(initialTabs[i]);
   5909        tabbrowser.moveTabTo(initialTabs[i], {
   5910          tabIndex: endPosition,
   5911          forceUngrouped: true,
   5912        });
   5913      }
   5914    }
   5915 
   5916    // We want to correlate the window with data from the last session, so
   5917    // assign another id if we have one. Otherwise clear so we don't do
   5918    // anything with it.
   5919    delete aWindow.__SS_lastSessionWindowID;
   5920    if (winData.__lastSessionWindowID) {
   5921      aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
   5922    }
   5923 
   5924    if (overwriteTabs) {
   5925      delete this._windows[aWindow.__SSi].extData;
   5926    }
   5927 
   5928    // Restore cookies from legacy sessions, i.e. before bug 912717.
   5929    lazy.SessionCookies.restore(winData.cookies || []);
   5930 
   5931    if (winData.extData) {
   5932      if (!this._windows[aWindow.__SSi].extData) {
   5933        this._windows[aWindow.__SSi].extData = {};
   5934      }
   5935      for (var key in winData.extData) {
   5936        this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
   5937      }
   5938    }
   5939 
   5940    let newClosedTabsData;
   5941    if (winData._closedTabs) {
   5942      newClosedTabsData = winData._closedTabs;
   5943      this._resetClosedTabIds(newClosedTabsData, aWindow.__SSi);
   5944    } else {
   5945      newClosedTabsData = [];
   5946    }
   5947 
   5948    let newLastClosedTabGroupCount = winData._lastClosedTabGroupCount || -1;
   5949 
   5950    if (overwriteTabs || firstWindow) {
   5951      // Overwrite existing closed tabs data when overwriteTabs=true
   5952      // or we're the first window to be restored.
   5953      this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData;
   5954    } else if (this._max_tabs_undo > 0) {
   5955      // We preserve tabs between sessions so we just want to filter out any previously open tabs that
   5956      // were added to the _closedTabs list prior to restoreLastSession
   5957      if (PERSIST_SESSIONS) {
   5958        newClosedTabsData = this._windows[aWindow.__SSi]._closedTabs.filter(
   5959          tab => !tab.removeAfterRestore
   5960        );
   5961      } else {
   5962        newClosedTabsData = newClosedTabsData.concat(
   5963          this._windows[aWindow.__SSi]._closedTabs
   5964        );
   5965      }
   5966 
   5967      // ... and make sure that we don't exceed the max number of closed tabs
   5968      // we can restore.
   5969      this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData.slice(
   5970        0,
   5971        this._max_tabs_undo
   5972      );
   5973    }
   5974    // Because newClosedTabsData are put in first, we need to
   5975    // copy also the _lastClosedTabGroupCount.
   5976    this._windows[aWindow.__SSi]._lastClosedTabGroupCount =
   5977      newLastClosedTabGroupCount;
   5978 
   5979    // Copy over closed tab groups from the previous session,
   5980    // and reset closed tab ids for tabs within each group.
   5981    let newClosedTabGroupsData = winData.closedGroups || [];
   5982    newClosedTabGroupsData.forEach(group => {
   5983      this._resetClosedTabIds(group.tabs, aWindow.__SSi);
   5984    });
   5985    this._windows[aWindow.__SSi].closedGroups = newClosedTabGroupsData;
   5986    this._windows[aWindow.__SSi].lastClosedTabGroupId =
   5987      winData.lastClosedTabGroupId || null;
   5988 
   5989    if (!this._isWindowLoaded(aWindow)) {
   5990      // from now on, the data will come from the actual window
   5991      delete this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)];
   5992      WINDOW_RESTORE_IDS.delete(aWindow);
   5993      delete this._windows[aWindow.__SSi]._restoring;
   5994    }
   5995 
   5996    // Restore tabs, if any.
   5997    if (winData.tabs.length) {
   5998      this.restoreTabs(aWindow, tabs, winData.tabs, selectTab);
   5999    }
   6000 
   6001    // set smoothScroll back to the original value
   6002    arrowScrollbox.smoothScroll = smoothScroll;
   6003 
   6004    Glean.sessionRestore.restoreWindow.stopAndAccumulate(timerId);
   6005  },
   6006 
   6007  /**
   6008   * Prepare connection to host beforehand.
   6009   *
   6010   * @param tab
   6011   *        Tab we are loading from.
   6012   * @param url
   6013   *        URL of a host.
   6014   * @returns a flag indicates whether a connection has been made
   6015   */
   6016  prepareConnectionToHost(tab, url) {
   6017    if (url && !url.startsWith("about:")) {
   6018      let principal = Services.scriptSecurityManager.createNullPrincipal({
   6019        userContextId: tab.userContextId,
   6020      });
   6021      let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
   6022      let uri = Services.io.newURI(url);
   6023      try {
   6024        sc.speculativeConnect(uri, principal, null, false);
   6025        return true;
   6026      } catch (error) {
   6027        // Can't setup speculative connection for this url.
   6028        return false;
   6029      }
   6030    }
   6031    return false;
   6032  },
   6033 
   6034  /**
   6035   * Make a connection to a host when users hover mouse on a tab.
   6036   * This will also set a flag in the tab to prevent us from speculatively
   6037   * connecting a second time.
   6038   *
   6039   * @param tab
   6040   *        a tab to speculatively connect on mouse hover.
   6041   */
   6042  speculativeConnectOnTabHover(tab) {
   6043    let tabState = TAB_LAZY_STATES.get(tab);
   6044    if (tabState && !tabState.connectionPrepared) {
   6045      let url = this.getLazyTabValue(tab, "url");
   6046      let prepared = this.prepareConnectionToHost(tab, url);
   6047      // This is used to test if a connection has been made beforehand.
   6048      if (gDebuggingEnabled) {
   6049        tab.__test_connection_prepared = prepared;
   6050        tab.__test_connection_url = url;
   6051      }
   6052      // A flag indicate that we've prepared a connection for this tab and
   6053      // if is called again, we shouldn't prepare another connection.
   6054      tabState.connectionPrepared = true;
   6055    }
   6056  },
   6057 
   6058  /**
   6059   * This function will restore window features and then restore window data.
   6060   *
   6061   * @param windows
   6062   *        ordered array of windows to restore
   6063   */
   6064  _restoreWindowsFeaturesAndTabs(windows) {
   6065    // First, we restore window features, so that when users start interacting
   6066    // with a window, we don't steal the window focus.
   6067    let resizePromises = [];
   6068    for (let window of windows) {
   6069      let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(window)];
   6070      // Wait for these promises after we've restored data into them below.
   6071      resizePromises.push(this.restoreWindowFeatures(window, state.windows[0]));
   6072    }
   6073 
   6074    // Then we restore data into windows.
   6075    for (let window of windows) {
   6076      let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(window)];
   6077      this.restoreWindow(
   6078        window,
   6079        state.windows[0],
   6080        state.options || { overwriteTabs: true }
   6081      );
   6082      WINDOW_RESTORE_ZINDICES.delete(window);
   6083    }
   6084    for (let resizePromise of resizePromises) {
   6085      resizePromise.then(resizedWindow => {
   6086        this._setWindowStateReady(resizedWindow);
   6087 
   6088        this._sendWindowRestoredNotification(resizedWindow);
   6089 
   6090        Services.obs.notifyObservers(
   6091          resizedWindow,
   6092          NOTIFY_SINGLE_WINDOW_RESTORED
   6093        );
   6094 
   6095        this._sendRestoreCompletedNotifications();
   6096      });
   6097    }
   6098  },
   6099 
   6100  /**
   6101   * This function will restore window in reversed z-index, so that users will
   6102   * be presented with most recently used window first.
   6103   *
   6104   * @param windows
   6105   *        unordered array of windows to restore
   6106   */
   6107  _restoreWindowsInReversedZOrder(windows) {
   6108    windows.sort(
   6109      (a, b) =>
   6110        (WINDOW_RESTORE_ZINDICES.get(a) || 0) -
   6111        (WINDOW_RESTORE_ZINDICES.get(b) || 0)
   6112    );
   6113 
   6114    this.windowToFocus = windows[0];
   6115    this._restoreWindowsFeaturesAndTabs(windows);
   6116  },
   6117 
   6118  /**
   6119   * Restore multiple windows using the provided state.
   6120   *
   6121   * @param aWindow
   6122   *        Window reference to the first window to use for restoration.
   6123   *        Additionally required windows will be opened.
   6124   * @param aState
   6125   *        JS object or JSON string
   6126   * @param aOptions.overwriteTabs
   6127   *        to overwrite existing tabs w/ new ones
   6128   * @param aOptions.firstWindow
   6129   *        if this is the first non-private window we're
   6130   *        restoring in this session, that might open an
   6131   *        external link as well
   6132   */
   6133  restoreWindows: function ssi_restoreWindows(aWindow, aState, aOptions = {}) {
   6134    // initialize window if necessary
   6135    if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) {
   6136      this.onLoad(aWindow);
   6137    }
   6138 
   6139    let root;
   6140    try {
   6141      root = typeof aState == "string" ? JSON.parse(aState) : aState;
   6142    } catch (ex) {
   6143      // invalid state object - don't restore anything
   6144      this._log.debug(`restoreWindows failed to parse ${typeof aState} state`);
   6145      this._log.error(ex);
   6146      this._sendRestoreCompletedNotifications();
   6147      return;
   6148    }
   6149 
   6150    // Restore closed windows if any.
   6151    if (root._closedWindows) {
   6152      this._closedWindows = root._closedWindows;
   6153      // reset window closedIds and any references to them from closed tabs
   6154      for (let closedWindow of this._closedWindows) {
   6155        closedWindow.closedId = this._nextClosedId++;
   6156        if (closedWindow._closedTabs?.length) {
   6157          this._resetClosedTabIds(
   6158            closedWindow._closedTabs,
   6159            closedWindow.closedId
   6160          );
   6161        }
   6162      }
   6163      this._log.debug(`Restored ${this._closedWindows.length} closed windows`);
   6164      this._closedObjectsChanged = true;
   6165    }
   6166 
   6167    this._log.debug(
   6168      `restoreWindows will restore ${root.windows?.length} windows`
   6169    );
   6170    // We're done here if there are no windows.
   6171    if (!root.windows || !root.windows.length) {
   6172      this._sendRestoreCompletedNotifications();
   6173      return;
   6174    }
   6175 
   6176    let firstWindowData = root.windows.splice(0, 1);
   6177    // Store the restore state and restore option of the current window,
   6178    // so that the window can be restored in reversed z-order.
   6179    this._updateWindowRestoreState(aWindow, {
   6180      windows: firstWindowData,
   6181      options: aOptions,
   6182    });
   6183 
   6184    // Begin the restoration: First open all windows in creation order. After all
   6185    // windows have opened, we restore states to windows in reversed z-order.
   6186    this._openWindows(root).then(windows => {
   6187      // We want to add current window to opened window, so that this window will be
   6188      // restored in reversed z-order. (We add the window to first position, in case
   6189      // no z-indices are found, that window will be restored first.)
   6190      windows.unshift(aWindow);
   6191 
   6192      this._restoreWindowsInReversedZOrder(windows);
   6193    });
   6194 
   6195    lazy.DevToolsShim.restoreDevToolsSession(aState);
   6196  },
   6197 
   6198  /**
   6199   * Manage history restoration for a window
   6200   *
   6201   * @param aWindow
   6202   *        Window to restore the tabs into
   6203   * @param aTabs
   6204   *        Array of tab references
   6205   * @param aTabData
   6206   *        Array of tab data
   6207   * @param aSelectTab
   6208   *        Index of the tab to select. This is a 1-based index where "1"
   6209   *        indicates the first tab should be selected, and "0" indicates that
   6210   *        the currently selected tab will not be changed.
   6211   */
   6212  restoreTabs(aWindow, aTabs, aTabData, aSelectTab) {
   6213    var tabbrowser = aWindow.gBrowser;
   6214 
   6215    let numTabsToRestore = aTabs.length;
   6216    let numTabsInWindow = tabbrowser.tabs.length;
   6217    let tabsDataArray = this._windows[aWindow.__SSi].tabs;
   6218 
   6219    // Update the window state in case we shut down without being notified.
   6220    // Individual tab states will be taken care of by restoreTab() below.
   6221    if (numTabsInWindow == numTabsToRestore) {
   6222      // Remove all previous tab data.
   6223      tabsDataArray.length = 0;
   6224    } else {
   6225      // Remove all previous tab data except tabs that should not be overriden.
   6226      tabsDataArray.splice(numTabsInWindow - numTabsToRestore);
   6227    }
   6228 
   6229    // Remove items from aTabData if there is no corresponding tab:
   6230    if (numTabsInWindow < tabsDataArray.length) {
   6231      tabsDataArray.length = numTabsInWindow;
   6232    }
   6233 
   6234    // Ensure the tab data array has items for each of the tabs
   6235    this._ensureNoNullsInTabDataList(
   6236      tabbrowser.tabs,
   6237      tabsDataArray,
   6238      numTabsInWindow - 1
   6239    );
   6240 
   6241    if (aSelectTab > 0 && aSelectTab <= aTabs.length) {
   6242      // Update the window state in case we shut down without being notified.
   6243      this._windows[aWindow.__SSi].selected = aSelectTab;
   6244    }
   6245 
   6246    // If we restore the selected tab, make sure it goes first.
   6247    let selectedIndex = aTabs.indexOf(tabbrowser.selectedTab);
   6248    if (selectedIndex > -1) {
   6249      this.restoreTab(tabbrowser.selectedTab, aTabData[selectedIndex]);
   6250    }
   6251 
   6252    // Restore all tabs.
   6253    for (let t = 0; t < aTabs.length; t++) {
   6254      if (t != selectedIndex) {
   6255        this.restoreTab(aTabs[t], aTabData[t]);
   6256      }
   6257    }
   6258  },
   6259 
   6260  // In case we didn't collect/receive data for any tabs yet we'll have to
   6261  // fill the array with at least empty tabData objects until |_tPos| or
   6262  // we'll end up with |null| entries.
   6263  _ensureNoNullsInTabDataList(tabElements, tabDataList, changedTabPos) {
   6264    let initialDataListLength = tabDataList.length;
   6265    if (changedTabPos < initialDataListLength) {
   6266      return;
   6267    }
   6268    // Add items to the end.
   6269    while (tabDataList.length < changedTabPos) {
   6270      let existingTabEl = tabElements[tabDataList.length];
   6271      tabDataList.push({
   6272        entries: [],
   6273        lastAccessed: existingTabEl.lastAccessed,
   6274      });
   6275    }
   6276    // Ensure the pre-existing items are non-null.
   6277    for (let i = 0; i < initialDataListLength; i++) {
   6278      if (!tabDataList[i]) {
   6279        let existingTabEl = tabElements[i];
   6280        tabDataList[i] = {
   6281          entries: [],
   6282          lastAccessed: existingTabEl.lastAccessed,
   6283        };
   6284      }
   6285    }
   6286  },
   6287 
   6288  // Restores the given tab state for a given tab.
   6289  restoreTab(tab, tabData, options = {}) {
   6290    let browser = tab.linkedBrowser;
   6291 
   6292    if (TAB_STATE_FOR_BROWSER.has(browser)) {
   6293      this._log.warn("Must reset tab before calling restoreTab.");
   6294      return;
   6295    }
   6296 
   6297    let loadArguments = options.loadArguments;
   6298    let window = tab.ownerGlobal;
   6299    let tabbrowser = window.gBrowser;
   6300    let forceOnDemand = options.forceOnDemand;
   6301    let isRemotenessUpdate = options.isRemotenessUpdate;
   6302 
   6303    let willRestoreImmediately =
   6304      options.restoreImmediately || tabbrowser.selectedBrowser == browser;
   6305 
   6306    let isBrowserInserted = browser.isConnected;
   6307 
   6308    // Increase the busy state counter before modifying the tab.
   6309    this._setWindowStateBusy(window);
   6310 
   6311    // It's important to set the window state to dirty so that
   6312    // we collect their data for the first time when saving state.
   6313    DirtyWindows.add(window);
   6314 
   6315    if (!tab.hasOwnProperty("_tPos")) {
   6316      throw new Error(
   6317        "Shouldn't be trying to restore a tab that has no position"
   6318      );
   6319    }
   6320    // Update the tab state in case we shut down without being notified.
   6321    this._windows[window.__SSi].tabs[tab._tPos] = tabData;
   6322 
   6323    // Prepare the tab so that it can be properly restored.  We'll also attach
   6324    // a copy of the tab's data in case we close it before it's been restored.
   6325    // Anything that dispatches an event to external consumers must happen at
   6326    // the end of this method, to make sure that the tab/browser object is in a
   6327    // reliable and consistent state.
   6328 
   6329    if (tabData.lastAccessed) {
   6330      tab.updateLastAccessed(tabData.lastAccessed);
   6331    }
   6332 
   6333    if (!tabData.entries) {
   6334      tabData.entries = [];
   6335    }
   6336    if (tabData.extData) {
   6337      TAB_CUSTOM_VALUES.set(tab, Cu.cloneInto(tabData.extData, {}));
   6338    } else {
   6339      TAB_CUSTOM_VALUES.delete(tab);
   6340    }
   6341 
   6342    // Tab is now open.
   6343    delete tabData.closedAt;
   6344 
   6345    // Ensure the index is in bounds.
   6346    let activeIndex = (tabData.index || tabData.entries.length) - 1;
   6347    activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
   6348    activeIndex = Math.max(activeIndex, 0);
   6349 
   6350    // Save the index in case we updated it above.
   6351    tabData.index = activeIndex + 1;
   6352 
   6353    tab.setAttribute("pending", "true");
   6354 
   6355    // If we're restoring this tab, it certainly shouldn't be in
   6356    // the ignored set anymore.
   6357    this._crashedBrowsers.delete(browser.permanentKey);
   6358 
   6359    // If we're in the midst of performing a process flip, then we must
   6360    // have initiated a navigation. This means that these userTyped*
   6361    // values are now out of date.
   6362    if (
   6363      options.restoreContentReason ==
   6364      RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE
   6365    ) {
   6366      delete tabData.userTypedValue;
   6367      delete tabData.userTypedClear;
   6368    }
   6369 
   6370    // Update the persistent tab state cache with |tabData| information.
   6371    lazy.TabStateCache.update(browser.permanentKey, {
   6372      // NOTE: Copy the entries array shallowly, so as to not screw with the
   6373      // original tabData's history when getting history updates.
   6374      history: { entries: [...tabData.entries], index: tabData.index },
   6375      scroll: tabData.scroll || null,
   6376      storage: tabData.storage || null,
   6377      formdata: tabData.formdata || null,
   6378      disallow: tabData.disallow || null,
   6379      userContextId: tabData.userContextId || 0,
   6380 
   6381      // This information is only needed until the tab has finished restoring.
   6382      // When that's done it will be removed from the cache and we always
   6383      // collect it in TabState._collectBaseTabData().
   6384      image: tabData.image || "",
   6385      searchMode: tabData.searchMode || null,
   6386      userTypedValue: tabData.userTypedValue || "",
   6387      userTypedClear: tabData.userTypedClear || 0,
   6388    });
   6389 
   6390    // Restore tab attributes.
   6391    if ("attributes" in tabData) {
   6392      lazy.TabAttributes.set(tab, tabData.attributes);
   6393    }
   6394 
   6395    if (isBrowserInserted) {
   6396      // Start a new epoch to discard all frame script messages relating to a
   6397      // previous epoch. All async messages that are still on their way to chrome
   6398      // will be ignored and don't override any tab data set when restoring.
   6399      let epoch = this.startNextEpoch(browser.permanentKey);
   6400 
   6401      // Ensure that the tab will get properly restored in the event the tab
   6402      // crashes while restoring.  But don't set this on lazy browsers as
   6403      // restoreTab will get called again when the browser is instantiated.
   6404      TAB_STATE_FOR_BROWSER.set(browser, TAB_STATE_NEEDS_RESTORE);
   6405 
   6406      this._sendRestoreHistory(browser, {
   6407        tabData,
   6408        epoch,
   6409        loadArguments,
   6410        isRemotenessUpdate,
   6411      });
   6412 
   6413      // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
   6414      // it ensures each window will have its selected tab loaded.
   6415      if (willRestoreImmediately) {
   6416        this.restoreTabContent(tab, options);
   6417      } else if (!forceOnDemand) {
   6418        TabRestoreQueue.add(tab);
   6419        // Check if a tab is in queue and will be restored
   6420        // after the currently loading tabs. If so, prepare
   6421        // a connection to host to speed up page loading.
   6422        if (TabRestoreQueue.willRestoreSoon(tab)) {
   6423          if (activeIndex in tabData.entries) {
   6424            let url = tabData.entries[activeIndex].url;
   6425            let prepared = this.prepareConnectionToHost(tab, url);
   6426            if (gDebuggingEnabled) {
   6427              tab.__test_connection_prepared = prepared;
   6428              tab.__test_connection_url = url;
   6429            }
   6430          }
   6431        }
   6432        this.restoreNextTab();
   6433      }
   6434    } else {
   6435      // TAB_LAZY_STATES holds data for lazy-browser tabs to proxy for
   6436      // data unobtainable from the unbound browser.  This only applies to lazy
   6437      // browsers and will be removed once the browser is inserted in the document.
   6438      // This must preceed `updateTabLabelAndIcon` call for required data to be present.
   6439      let url = "about:blank";
   6440      let title = "";
   6441 
   6442      if (activeIndex in tabData.entries) {
   6443        url = tabData.entries[activeIndex].url;
   6444        title = tabData.entries[activeIndex].title || url;
   6445      }
   6446      TAB_LAZY_STATES.set(tab, {
   6447        url,
   6448        title,
   6449        userTypedValue: tabData.userTypedValue || "",
   6450        userTypedClear: tabData.userTypedClear || 0,
   6451      });
   6452    }
   6453 
   6454    // Most of tabData has been restored, now continue with restoring
   6455    // attributes that may trigger external events.
   6456 
   6457    if (tabData.pinned) {
   6458      tabbrowser.pinTab(tab);
   6459    } else {
   6460      tabbrowser.unpinTab(tab);
   6461    }
   6462 
   6463    if (tabData.hidden) {
   6464      tabbrowser.hideTab(tab);
   6465    } else {
   6466      tabbrowser.showTab(tab);
   6467    }
   6468 
   6469    if (!!tabData.muted != browser.audioMuted) {
   6470      tab.toggleMuteAudio(tabData.muteReason);
   6471    }
   6472 
   6473    if (tab.hasAttribute("customizemode")) {
   6474      window.gCustomizeMode.setTab(tab);
   6475    }
   6476 
   6477    // Update tab label and icon to show something
   6478    // while we wait for the messages to be processed.
   6479    this.updateTabLabelAndIcon(tab, tabData);
   6480 
   6481    // Decrease the busy state counter after we're done.
   6482    this._setWindowStateReady(window);
   6483  },
   6484 
   6485  /**
   6486   * Kicks off restoring the given tab.
   6487   *
   6488   * @param aTab
   6489   *        the tab to restore
   6490   * @param aOptions
   6491   *        optional arguments used when performing process switch during load
   6492   */
   6493  restoreTabContent(aTab, aOptions = {}) {
   6494    let loadArguments = aOptions.loadArguments;
   6495    if (aTab.hasAttribute("customizemode") && !loadArguments) {
   6496      return;
   6497    }
   6498 
   6499    let browser = aTab.linkedBrowser;
   6500    let window = aTab.ownerGlobal;
   6501    let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
   6502    let activeIndex = tabData.index - 1;
   6503    let activePageData = tabData.entries[activeIndex] || null;
   6504    let uri = activePageData ? activePageData.url || null : null;
   6505 
   6506    this.markTabAsRestoring(aTab);
   6507 
   6508    this._sendRestoreTabContent(browser, {
   6509      loadArguments,
   6510      isRemotenessUpdate: aOptions.isRemotenessUpdate,
   6511      reason:
   6512        aOptions.restoreContentReason || RESTORE_TAB_CONTENT_REASON.SET_STATE,
   6513    });
   6514 
   6515    // Focus the tab's content area, unless the restore is for a new tab URL or
   6516    // was triggered by a DocumentChannel process switch.
   6517    if (
   6518      aTab.selected &&
   6519      !window.isBlankPageURL(uri) &&
   6520      !aOptions.isRemotenessUpdate
   6521    ) {
   6522      browser.focus();
   6523    }
   6524  },
   6525 
   6526  /**
   6527   * Marks a given pending tab as restoring.
   6528   *
   6529   * @param aTab
   6530   *        the pending tab to mark as restoring
   6531   */
   6532  markTabAsRestoring(aTab) {
   6533    let browser = aTab.linkedBrowser;
   6534    if (TAB_STATE_FOR_BROWSER.get(browser) != TAB_STATE_NEEDS_RESTORE) {
   6535      throw new Error("Given tab is not pending.");
   6536    }
   6537 
   6538    // Make sure that this tab is removed from the priority queue.
   6539    TabRestoreQueue.remove(aTab);
   6540 
   6541    // Increase our internal count.
   6542    this._tabsRestoringCount++;
   6543 
   6544    // Set this tab's state to restoring
   6545    TAB_STATE_FOR_BROWSER.set(browser, TAB_STATE_RESTORING);
   6546    aTab.removeAttribute("pending");
   6547    aTab.removeAttribute("discarded");
   6548  },
   6549 
   6550  /**
   6551   * This _attempts_ to restore the next available tab. If the restore fails,
   6552   * then we will attempt the next one.
   6553   * There are conditions where this won't do anything:
   6554   *   if we're in the process of quitting
   6555   *   if there are no tabs to restore
   6556   *   if we have already reached the limit for number of tabs to restore
   6557   */
   6558  restoreNextTab: function ssi_restoreNextTab() {
   6559    // If we call in here while quitting, we don't actually want to do anything
   6560    if (lazy.RunState.isQuitting) {
   6561      return;
   6562    }
   6563 
   6564    // Don't exceed the maximum number of concurrent tab restores.
   6565    if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES) {
   6566      return;
   6567    }
   6568 
   6569    let tab = TabRestoreQueue.shift();
   6570    if (tab) {
   6571      this.restoreTabContent(tab);
   6572    }
   6573  },
   6574 
   6575  /**
   6576   * Restore visibility and dimension features to a window
   6577   *
   6578   * @param aWindow
   6579   *        Window reference
   6580   * @param aWinData
   6581   *        Object containing session data for the window
   6582   */
   6583  restoreWindowFeatures: function ssi_restoreWindowFeatures(aWindow, aWinData) {
   6584    var hidden = aWinData.hidden ? aWinData.hidden.split(",") : [];
   6585    var isTaskbarTab =
   6586      aWindow.document.documentElement.hasAttribute("taskbartab");
   6587    if (!isTaskbarTab) {
   6588      WINDOW_HIDEABLE_FEATURES.forEach(function (aItem) {
   6589        aWindow[aItem].visible = !hidden.includes(aItem);
   6590      });
   6591    }
   6592 
   6593    if (aWinData.isPopup) {
   6594      this._windows[aWindow.__SSi].isPopup = true;
   6595      if (aWindow.gURLBar) {
   6596        aWindow.gURLBar.readOnly = true;
   6597      }
   6598    } else {
   6599      delete this._windows[aWindow.__SSi].isPopup;
   6600      if (aWindow.gURLBar && !isTaskbarTab) {
   6601        aWindow.gURLBar.readOnly = false;
   6602      }
   6603    }
   6604 
   6605    let promiseParts = Promise.withResolvers();
   6606    aWindow.setTimeout(() => {
   6607      this.restoreDimensions(
   6608        aWindow,
   6609        +(aWinData.width || 0),
   6610        +(aWinData.height || 0),
   6611        "screenX" in aWinData ? +aWinData.screenX : NaN,
   6612        "screenY" in aWinData ? +aWinData.screenY : NaN,
   6613        aWinData.sizemode || "",
   6614        aWinData.sizemodeBeforeMinimized || ""
   6615      );
   6616      this.restoreSidebar(aWindow, aWinData.sidebar, aWinData.isPopup);
   6617      promiseParts.resolve(aWindow);
   6618    }, 0);
   6619    return promiseParts.promise;
   6620  },
   6621 
   6622  /**
   6623   * @param aWindow
   6624   *        Window reference
   6625   * @param aSidebar
   6626   *        Object containing command (sidebarcommand/category) and styles
   6627   */
   6628  restoreSidebar(aWindow, aSidebar, isPopup) {
   6629    if (!aSidebar || isPopup) {
   6630      return;
   6631    }
   6632    aWindow.SidebarController.initializeUIState(aSidebar);
   6633  },
   6634 
   6635  /**
   6636   * Restore a window's dimensions
   6637   *
   6638   * @param aWidth
   6639   *        Window width in desktop pixels
   6640   * @param aHeight
   6641   *        Window height in desktop pixels
   6642   * @param aLeft
   6643   *        Window left in desktop pixels
   6644   * @param aTop
   6645   *        Window top in desktop pixels
   6646   * @param aSizeMode
   6647   *        Window size mode (eg: maximized)
   6648   * @param aSizeModeBeforeMinimized
   6649   *        Window size mode before window got minimized (eg: maximized)
   6650   */
   6651  restoreDimensions: function ssi_restoreDimensions(
   6652    aWindow,
   6653    aWidth,
   6654    aHeight,
   6655    aLeft,
   6656    aTop,
   6657    aSizeMode,
   6658    aSizeModeBeforeMinimized
   6659  ) {
   6660    var win = aWindow;
   6661    var _this = this;
   6662    function win_(aName) {
   6663      return _this._getWindowDimension(win, aName);
   6664    }
   6665 
   6666    const dwu = win.windowUtils;
   6667    // find available space on the screen where this window is being placed
   6668    let screen = lazy.gScreenManager.screenForRect(
   6669      aLeft,
   6670      aTop,
   6671      aWidth,
   6672      aHeight
   6673    );
   6674    if (screen) {
   6675      let screenLeft = {},
   6676        screenTop = {},
   6677        screenWidth = {},
   6678        screenHeight = {};
   6679      screen.GetAvailRectDisplayPix(
   6680        screenLeft,
   6681        screenTop,
   6682        screenWidth,
   6683        screenHeight
   6684      );
   6685 
   6686      // We store aLeft / aTop (screenX/Y) in desktop pixels, see
   6687      // _getWindowDimension.
   6688      screenLeft = screenLeft.value;
   6689      screenTop = screenTop.value;
   6690      screenWidth = screenWidth.value;
   6691      screenHeight = screenHeight.value;
   6692 
   6693      let screenBottom = screenTop + screenHeight;
   6694      let screenRight = screenLeft + screenWidth;
   6695 
   6696      // NOTE: contentsScaleFactor is the desktopToDeviceScale of the screen.
   6697      // Naming could be more consistent here.
   6698      let cssToDesktopScale =
   6699        screen.defaultCSSScaleFactor / screen.contentsScaleFactor;
   6700 
   6701      let winSlopX = win.screenEdgeSlopX * cssToDesktopScale;
   6702      let winSlopY = win.screenEdgeSlopY * cssToDesktopScale;
   6703 
   6704      let minSlop = MIN_SCREEN_EDGE_SLOP * cssToDesktopScale;
   6705      let slopX = Math.max(minSlop, winSlopX);
   6706      let slopY = Math.max(minSlop, winSlopY);
   6707 
   6708      // Pull the window within the screen's bounds (allowing a little slop
   6709      // for windows that may be deliberately placed with their border off-screen
   6710      // as when Win10 "snaps" a window to the left/right edge -- bug 1276516).
   6711      // First, ensure the left edge is large enough...
   6712      if (aLeft < screenLeft - slopX) {
   6713        aLeft = screenLeft - winSlopX;
   6714      }
   6715      // Then check the resulting right edge, and reduce it if necessary.
   6716      let right = aLeft + aWidth * cssToDesktopScale;
   6717      if (right > screenRight + slopX) {
   6718        right = screenRight + winSlopX;
   6719        // See if we can move the left edge leftwards to maintain width.
   6720        if (aLeft > screenLeft) {
   6721          aLeft = Math.max(
   6722            right - aWidth * cssToDesktopScale,
   6723            screenLeft - winSlopX
   6724          );
   6725        }
   6726      }
   6727      // Finally, update aWidth to account for the adjusted left and right
   6728      // edges, and convert it back to CSS pixels on the target screen.
   6729      aWidth = (right - aLeft) / cssToDesktopScale;
   6730 
   6731      // And do the same in the vertical dimension.
   6732      if (aTop < screenTop - slopY) {
   6733        aTop = screenTop - winSlopY;
   6734      }
   6735      let bottom = aTop + aHeight * cssToDesktopScale;
   6736      if (bottom > screenBottom + slopY) {
   6737        bottom = screenBottom + winSlopY;
   6738        if (aTop > screenTop) {
   6739          aTop = Math.max(
   6740            bottom - aHeight * cssToDesktopScale,
   6741            screenTop - winSlopY
   6742          );
   6743        }
   6744      }
   6745      aHeight = (bottom - aTop) / cssToDesktopScale;
   6746    }
   6747 
   6748    // Suppress animations.
   6749    dwu.suppressAnimation(true);
   6750 
   6751    // We want to make sure users will get their animations back in case an exception is thrown.
   6752    try {
   6753      // only modify those aspects which aren't correct yet
   6754      if (
   6755        !isNaN(aLeft) &&
   6756        !isNaN(aTop) &&
   6757        (aLeft != win_("screenX") || aTop != win_("screenY"))
   6758      ) {
   6759        // moveTo uses CSS pixels relative to aWindow, while aLeft and aRight
   6760        // are on desktop pixels, undo the conversion we do in
   6761        // _getWindowDimension.
   6762        let desktopToCssScale =
   6763          aWindow.desktopToDeviceScale / aWindow.devicePixelRatio;
   6764        aWindow.moveTo(aLeft * desktopToCssScale, aTop * desktopToCssScale);
   6765      }
   6766      if (
   6767        aWidth &&
   6768        aHeight &&
   6769        (aWidth != win_("width") || aHeight != win_("height")) &&
   6770        !ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null)
   6771      ) {
   6772        // Don't resize the window if it's currently maximized and we would
   6773        // maximize it again shortly after.
   6774        if (aSizeMode != "maximized" || win_("sizemode") != "maximized") {
   6775          aWindow.resizeTo(aWidth, aHeight);
   6776        }
   6777      }
   6778      this._windows[aWindow.__SSi].sizemodeBeforeMinimized =
   6779        aSizeModeBeforeMinimized;
   6780      if (
   6781        aSizeMode &&
   6782        win_("sizemode") != aSizeMode &&
   6783        !ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null)
   6784      ) {
   6785        switch (aSizeMode) {
   6786          case "maximized":
   6787            aWindow.maximize();
   6788            break;
   6789          case "minimized":
   6790            if (aSizeModeBeforeMinimized == "maximized") {
   6791              aWindow.maximize();
   6792            }
   6793            aWindow.minimize();
   6794            break;
   6795          case "normal":
   6796            aWindow.restore();
   6797            break;
   6798        }
   6799      }
   6800      // since resizing/moving a window brings it to the foreground,
   6801      // we might want to re-focus the last focused window
   6802      if (this.windowToFocus) {
   6803        this.windowToFocus.focus();
   6804      }
   6805    } finally {
   6806      // Enable animations.
   6807      dwu.suppressAnimation(false);
   6808    }
   6809  },
   6810 
   6811  /* ........ Disk Access .............. */
   6812 
   6813  /**
   6814   * Save the current session state to disk, after a delay.
   6815   *
   6816   * @param aWindow (optional)
   6817   *        Will mark the given window as dirty so that we will recollect its
   6818   *        data before we start writing.
   6819   */
   6820  saveStateDelayed(aWindow = null) {
   6821    if (aWindow) {
   6822      DirtyWindows.add(aWindow);
   6823    }
   6824 
   6825    lazy.SessionSaver.runDelayed();
   6826  },
   6827 
   6828  /* ........ Auxiliary Functions .............. */
   6829 
   6830  /**
   6831   * Remove a closed window from the list of closed windows and indicate that
   6832   * the change should be notified.
   6833   *
   6834   * @param index
   6835   *        The index of the window in this._closedWindows.
   6836   *
   6837   * @returns Array of closed windows.
   6838   */
   6839  _removeClosedWindow(index) {
   6840    // remove all of the closed tabs from the _lastClosedActions list
   6841    // before removing the window from it
   6842    for (let closedTab of this._closedWindows[index]._closedTabs) {
   6843      this._removeClosedAction(
   6844        this._LAST_ACTION_CLOSED_TAB,
   6845        closedTab.closedId
   6846      );
   6847    }
   6848    this._removeClosedAction(
   6849      this._LAST_ACTION_CLOSED_WINDOW,
   6850      this._closedWindows[index].closedId
   6851    );
   6852    let windows = this._closedWindows.splice(index, 1);
   6853    this._closedObjectsChanged = true;
   6854    return windows;
   6855  },
   6856 
   6857  /**
   6858   * Notifies observers that the list of closed tabs and/or windows has changed.
   6859   * Waits a tick to allow SessionStorage a chance to register the change.
   6860   */
   6861  _notifyOfClosedObjectsChange() {
   6862    if (!this._closedObjectsChanged) {
   6863      return;
   6864    }
   6865    this._closedObjectsChanged = false;
   6866    lazy.setTimeout(() => {
   6867      Services.obs.notifyObservers(null, NOTIFY_CLOSED_OBJECTS_CHANGED);
   6868    }, 0);
   6869  },
   6870 
   6871  /**
   6872   * Notifies observers that the list of saved tab groups has changed.
   6873   * Waits a tick to allow SessionStorage a chance to register the change.
   6874   */
   6875  _notifyOfSavedTabGroupsChange() {
   6876    lazy.setTimeout(() => {
   6877      Services.obs.notifyObservers(null, NOTIFY_SAVED_TAB_GROUPS_CHANGED);
   6878    }, 0);
   6879  },
   6880 
   6881  /**
   6882   * Update the session start time and send a telemetry measurement
   6883   * for the number of days elapsed since the session was started.
   6884   *
   6885   * @param state
   6886   *        The session state.
   6887   */
   6888  _updateSessionStartTime: function ssi_updateSessionStartTime(state) {
   6889    // Attempt to load the session start time from the session state
   6890    if (state.session && state.session.startTime) {
   6891      this._sessionStartTime = state.session.startTime;
   6892    }
   6893  },
   6894 
   6895  /**
   6896   * Iterator that yields all currently opened browser windows.
   6897   * (Might miss the most recent one.)
   6898   * This list is in focus order, but may include minimized windows
   6899   * before non-minimized windows.
   6900   */
   6901  _browserWindows: {
   6902    *[Symbol.iterator]() {
   6903      for (let window of lazy.BrowserWindowTracker.orderedWindows) {
   6904        if (window.__SSi && !window.closed) {
   6905          yield window;
   6906        }
   6907      }
   6908    },
   6909  },
   6910 
   6911  /**
   6912   * Iterator that yields all currently opened browser windows,
   6913   * with minimized windows last.
   6914   * (Might miss the most recent window.)
   6915   */
   6916  _orderedBrowserWindows: {
   6917    *[Symbol.iterator]() {
   6918      let windows = lazy.BrowserWindowTracker.orderedWindows;
   6919      windows.sort((a, b) => {
   6920        if (
   6921          a.windowState == a.STATE_MINIMIZED &&
   6922          b.windowState != b.STATE_MINIMIZED
   6923        ) {
   6924          return 1;
   6925        }
   6926        if (
   6927          a.windowState != a.STATE_MINIMIZED &&
   6928          b.windowState == b.STATE_MINIMIZED
   6929        ) {
   6930          return -1;
   6931        }
   6932        return 0;
   6933      });
   6934      for (let window of windows) {
   6935        if (window.__SSi && !window.closed) {
   6936          yield window;
   6937        }
   6938      }
   6939    },
   6940  },
   6941 
   6942  /**
   6943   * Returns most recent window
   6944   *
   6945   * @param {boolean} [isPrivate]
   6946   *        Optional boolean to get only non-private or private windows
   6947   *        When omitted, we'll return whatever the top-most window is regardless of privateness
   6948   * @returns Window reference
   6949   */
   6950  _getTopWindow: function ssi_getTopWindow(isPrivate) {
   6951    const options = { allowPopups: true };
   6952    if (typeof isPrivate !== "undefined") {
   6953      options.private = isPrivate;
   6954    }
   6955    return lazy.BrowserWindowTracker.getTopWindow(options);
   6956  },
   6957 
   6958  /**
   6959   * Calls onClose for windows that are determined to be closed but aren't
   6960   * destroyed yet, which would otherwise cause getBrowserState and
   6961   * setBrowserState to treat them as open windows.
   6962   */
   6963  _handleClosedWindows: function ssi_handleClosedWindows() {
   6964    let promises = [];
   6965    for (let window of Services.wm.getEnumerator("navigator:browser")) {
   6966      if (window.closed) {
   6967        promises.push(this.onClose(window));
   6968      }
   6969    }
   6970    return Promise.all(promises);
   6971  },
   6972 
   6973  /**
   6974   * Store a restore state of a window to this._statesToRestore. The window
   6975   * will be given an id that can be used to get the restore state from
   6976   * this._statesToRestore.
   6977   *
   6978   * @param window
   6979   *        a reference to a window that has a state to restore
   6980   * @param state
   6981   *        an object containing session data
   6982   */
   6983  _updateWindowRestoreState(window, state) {
   6984    // Store z-index, so that windows can be restored in reversed z-order.
   6985    if ("zIndex" in state.windows[0]) {
   6986      WINDOW_RESTORE_ZINDICES.set(window, state.windows[0].zIndex);
   6987    }
   6988    do {
   6989      var ID = "window" + Math.random();
   6990    } while (ID in this._statesToRestore);
   6991    WINDOW_RESTORE_IDS.set(window, ID);
   6992    this._statesToRestore[ID] = state;
   6993  },
   6994 
   6995  /**
   6996   * open a new browser window for a given session state
   6997   * called when restoring a multi-window session
   6998   *
   6999   * @param aState
   7000   *        Object containing session data
   7001   */
   7002  _openWindowWithState: function ssi_openWindowWithState(aState) {
   7003    // Build arguments string
   7004    let argString;
   7005    // Build feature string
   7006    let features;
   7007    let winState = aState.windows[0];
   7008    if (winState.chromeFlags) {
   7009      features = ["chrome", "suppressanimation"];
   7010      let chromeFlags = winState.chromeFlags;
   7011      const allFlags = Ci.nsIWebBrowserChrome.CHROME_ALL;
   7012      const hasAll = (chromeFlags & allFlags) == allFlags;
   7013      if (hasAll) {
   7014        features.push("all");
   7015      }
   7016      for (let [flag, onValue, offValue] of CHROME_FLAGS_MAP) {
   7017        if (hasAll && allFlags & flag) {
   7018          continue;
   7019        }
   7020        let value = chromeFlags & flag ? onValue : offValue;
   7021        if (value) {
   7022          features.push(value);
   7023        }
   7024      }
   7025    } else {
   7026      // |chromeFlags| is not found. Fallbacks to the old method.
   7027      features = ["chrome", "dialog=no", "suppressanimation"];
   7028      let hidden = winState.hidden?.split(",") || [];
   7029      if (!hidden.length) {
   7030        features.push("all");
   7031      } else {
   7032        features.push("resizable");
   7033        WINDOW_HIDEABLE_FEATURES.forEach(aFeature => {
   7034          if (!hidden.includes(aFeature)) {
   7035            features.push(WINDOW_OPEN_FEATURES_MAP[aFeature] || aFeature);
   7036          }
   7037        });
   7038      }
   7039    }
   7040    WINDOW_ATTRIBUTES.forEach(aFeature => {
   7041      // Use !isNaN as an easy way to ignore sizemode and check for numbers
   7042      if (aFeature in winState && !isNaN(winState[aFeature])) {
   7043        features.push(aFeature + "=" + winState[aFeature]);
   7044      }
   7045    });
   7046 
   7047    // A window CANNOT be both a Private Window and an AI Window
   7048    if (winState.isPrivate) {
   7049      features.push("private");
   7050    } else if (winState.isAIWindow) {
   7051      argString = lazy.AIWindow.handleAIWindowOptions({
   7052        openerWindow: null,
   7053        args: argString,
   7054        aiWindow: winState.isAIWindow,
   7055        restoreSession: true,
   7056      });
   7057    }
   7058 
   7059    if (!argString) {
   7060      argString = Cc["@mozilla.org/supports-string;1"].createInstance(
   7061        Ci.nsISupportsString
   7062      );
   7063      argString.data = "";
   7064    }
   7065 
   7066    this._log.debug(
   7067      `Opening window:${winState.closedId} with features: ${features.join(
   7068        ","
   7069      )}, argString: ${argString}.`
   7070    );
   7071    var window = Services.ww.openWindow(
   7072      null,
   7073      AppConstants.BROWSER_CHROME_URL,
   7074      "_blank",
   7075      features.join(","),
   7076      argString
   7077    );
   7078 
   7079    this._updateWindowRestoreState(window, aState);
   7080    WINDOW_SHOWING_PROMISES.set(window, Promise.withResolvers());
   7081 
   7082    return window;
   7083  },
   7084 
   7085  /**
   7086   * whether the user wants to load any other page at startup
   7087   * (except the homepage) - needed for determining whether to overwrite the current tabs
   7088   * C.f.: nsBrowserContentHandler's defaultArgs implementation.
   7089   *
   7090   * @returns bool
   7091   */
   7092  _isCmdLineEmpty: function ssi_isCmdLineEmpty(aWindow, aState) {
   7093    var pinnedOnly =
   7094      aState.windows &&
   7095      aState.windows.every(win => win.tabs.every(tab => tab.pinned));
   7096 
   7097    let hasFirstArgument = aWindow.arguments && aWindow.arguments[0];
   7098    if (!pinnedOnly) {
   7099      let defaultArgs = Cc["@mozilla.org/browser/clh;1"].getService(
   7100        Ci.nsIBrowserHandler
   7101      ).defaultArgs;
   7102      if (
   7103        aWindow.arguments &&
   7104        aWindow.arguments[0] &&
   7105        aWindow.arguments[0] == defaultArgs
   7106      ) {
   7107        hasFirstArgument = false;
   7108      }
   7109    }
   7110 
   7111    return !hasFirstArgument;
   7112  },
   7113 
   7114  /**
   7115   * on popup windows, the AppWindow's attributes seem not to be set correctly
   7116   * we use thus JSDOMWindow attributes for sizemode and normal window attributes
   7117   * (and hope for reasonable values when maximized/minimized - since then
   7118   * outerWidth/outerHeight aren't the dimensions of the restored window)
   7119   *
   7120   * @param aWindow
   7121   *        Window reference
   7122   * @param aAttribute
   7123   *        String sizemode | width | height | other window attribute
   7124   * @returns string
   7125   */
   7126  _getWindowDimension: function ssi_getWindowDimension(aWindow, aAttribute) {
   7127    if (aAttribute == "sizemode") {
   7128      switch (aWindow.windowState) {
   7129        case aWindow.STATE_FULLSCREEN:
   7130        case aWindow.STATE_MAXIMIZED:
   7131          return "maximized";
   7132        case aWindow.STATE_MINIMIZED:
   7133          return "minimized";
   7134        default:
   7135          return "normal";
   7136      }
   7137    }
   7138 
   7139    // We want to persist the size / position in normal state, so that
   7140    // we can restore to them even if the window is currently maximized
   7141    // or minimized. However, attributes on window object only reflect
   7142    // the current state of the window, so when it isn't in the normal
   7143    // sizemode, their values aren't what we want the window to restore
   7144    // to. In that case, try to read from the attributes of the root
   7145    // element first instead.
   7146    if (aWindow.windowState != aWindow.STATE_NORMAL) {
   7147      let docElem = aWindow.document.documentElement;
   7148      let attr = parseInt(docElem.getAttribute(aAttribute), 10);
   7149      if (attr) {
   7150        if (aAttribute != "width" && aAttribute != "height") {
   7151          return attr;
   7152        }
   7153        // Width and height attribute report the inner size, but we want
   7154        // to store the outer size, so add the difference.
   7155        let appWin = aWindow.docShell.treeOwner
   7156          .QueryInterface(Ci.nsIInterfaceRequestor)
   7157          .getInterface(Ci.nsIAppWindow);
   7158        let diff =
   7159          aAttribute == "width"
   7160            ? appWin.outerToInnerWidthDifferenceInCSSPixels
   7161            : appWin.outerToInnerHeightDifferenceInCSSPixels;
   7162        return attr + diff;
   7163      }
   7164    }
   7165 
   7166    switch (aAttribute) {
   7167      case "width":
   7168        return aWindow.outerWidth;
   7169      case "height":
   7170        return aWindow.outerHeight;
   7171      case "screenX":
   7172      case "screenY":
   7173        // We use desktop pixels rather than CSS pixels to store window
   7174        // positions, see bug 1247335.  This allows proper multi-monitor
   7175        // positioning in mixed-DPI situations.
   7176        // screenX/Y are in CSS pixels for the current window, so, convert them
   7177        // to desktop pixels.
   7178        return (
   7179          (aWindow[aAttribute] * aWindow.devicePixelRatio) /
   7180          aWindow.desktopToDeviceScale
   7181        );
   7182      default:
   7183        return aAttribute in aWindow ? aWindow[aAttribute] : "";
   7184    }
   7185  },
   7186 
   7187  /**
   7188   * @param aState is a session state
   7189   * @param aRecentCrashes is the number of consecutive crashes
   7190   * @returns whether a restore page will be needed for the session state
   7191   */
   7192  _needsRestorePage: function ssi_needsRestorePage(aState, aRecentCrashes) {
   7193    const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000;
   7194 
   7195    // don't display the page when there's nothing to restore
   7196    let winData = aState.windows || null;
   7197    if (!winData || !winData.length) {
   7198      return false;
   7199    }
   7200 
   7201    // don't wrap a single about:sessionrestore page
   7202    if (
   7203      this._hasSingleTabWithURL(winData, "about:sessionrestore") ||
   7204      this._hasSingleTabWithURL(winData, "about:welcomeback")
   7205    ) {
   7206      return false;
   7207    }
   7208 
   7209    // don't automatically restore in Safe Mode
   7210    if (Services.appinfo.inSafeMode) {
   7211      return true;
   7212    }
   7213 
   7214    let max_resumed_crashes = this._prefBranch.getIntPref(
   7215      "sessionstore.max_resumed_crashes"
   7216    );
   7217    let sessionAge =
   7218      aState.session &&
   7219      aState.session.lastUpdate &&
   7220      Date.now() - aState.session.lastUpdate;
   7221 
   7222    let decision =
   7223      max_resumed_crashes != -1 &&
   7224      (aRecentCrashes > max_resumed_crashes ||
   7225        (sessionAge && sessionAge >= SIX_HOURS_IN_MS));
   7226    if (decision) {
   7227      let key;
   7228      if (aRecentCrashes > max_resumed_crashes) {
   7229        if (sessionAge && sessionAge >= SIX_HOURS_IN_MS) {
   7230          key = "shown_many_crashes_old_session";
   7231        } else {
   7232          key = "shown_many_crashes";
   7233        }
   7234      } else {
   7235        key = "shown_old_session";
   7236      }
   7237      Glean.browserEngagement.sessionrestoreInterstitial[key].add(1);
   7238    }
   7239    return decision;
   7240  },
   7241 
   7242  /**
   7243   * @param aWinData is the set of windows in session state
   7244   * @param aURL is the single URL we're looking for
   7245   * @returns whether the window data contains only the single URL passed
   7246   */
   7247  _hasSingleTabWithURL(aWinData, aURL) {
   7248    if (
   7249      aWinData &&
   7250      aWinData.length == 1 &&
   7251      aWinData[0].tabs &&
   7252      aWinData[0].tabs.length == 1 &&
   7253      aWinData[0].tabs[0].entries &&
   7254      aWinData[0].tabs[0].entries.length == 1
   7255    ) {
   7256      return aURL == aWinData[0].tabs[0].entries[0].url;
   7257    }
   7258    return false;
   7259  },
   7260 
   7261  /**
   7262   * Determine if the tab state we're passed is something we should save. This
   7263   * is used when closing a tab, tab group, or closing a window with a single tab
   7264   *
   7265   * @param aTabState
   7266   *        The current tab state
   7267   * @returns boolean
   7268   */
   7269  _shouldSaveTabState: function ssi_shouldSaveTabState(aTabState) {
   7270    // If the tab has only a transient about: history entry, no other
   7271    // session history, and no userTypedValue, then we don't actually want to
   7272    // store this tab's data.
   7273    const entryUrl = aTabState.entries[0]?.url;
   7274    return (
   7275      entryUrl &&
   7276      !(
   7277        aTabState.entries.length == 1 &&
   7278        (entryUrl == "about:blank" ||
   7279          entryUrl == "about:home" ||
   7280          entryUrl == "about:newtab" ||
   7281          entryUrl == "about:privatebrowsing") &&
   7282        !aTabState.userTypedValue
   7283      )
   7284    );
   7285  },
   7286 
   7287  /**
   7288   * @param {MozTabbrowserTab[]} tabs
   7289   * @returns {boolean}
   7290   */
   7291  shouldSaveTabsToGroup: function ssi_shouldSaveTabsToGroup(tabs) {
   7292    if (!tabs) {
   7293      return false;
   7294    }
   7295    for (let tab of tabs) {
   7296      let tabState = lazy.TabState.collect(tab);
   7297      if (this._shouldSaveTabState(tabState)) {
   7298        return true;
   7299      }
   7300    }
   7301    return false;
   7302  },
   7303 
   7304  /**
   7305   * Determine if the tab state we're passed is something we should keep to be
   7306   * reopened at session restore. This is used when we are saving the current
   7307   * session state to disk. This method is very similar to _shouldSaveTabState,
   7308   * however, "about:blank" and "about:newtab" tabs will still be saved to disk.
   7309   *
   7310   * @param aTabState
   7311   *        The current tab state
   7312   * @returns boolean
   7313   */
   7314  _shouldSaveTab: function ssi_shouldSaveTab(aTabState) {
   7315    // If the tab has one of the following transient about: history entry, no
   7316    // userTypedValue, and no customizemode attribute, then we don't actually
   7317    // want to write this tab's data to disk.
   7318    return (
   7319      aTabState.userTypedValue ||
   7320      (aTabState.attributes && aTabState.attributes.customizemode == "true") ||
   7321      (aTabState.entries.length &&
   7322        aTabState.entries[0].url != "about:privatebrowsing")
   7323    );
   7324  },
   7325 
   7326  /**
   7327   * This is going to take a state as provided at startup (via
   7328   * SessionStartup.state) and split it into 2 parts. The first part
   7329   * (defaultState) will be a state that should still be restored at startup,
   7330   * while the second part (state) is a state that should be saved for later.
   7331   * defaultState is derived from a clone of startupState,
   7332   * and will be comprised of:
   7333   *   - windows with only pinned tabs,
   7334   *   - window position information, and
   7335   *   - saved groups, including groups that were open at last shutdown.
   7336   *
   7337   * defaultState will be restored at startup. state will be passed into
   7338   * LastSession and will be kept in case the user explicitly wants
   7339   * to restore the previous session (publicly exposed as restoreLastSession).
   7340   *
   7341   * @param state
   7342   *        The startupState, presumably from SessionStartup.state
   7343   * @returns [defaultState, state]
   7344   */
   7345  _prepDataForDeferredRestore: function ssi_prepDataForDeferredRestore(
   7346    startupState
   7347  ) {
   7348    // Make sure that we don't modify the global state as provided by
   7349    // SessionStartup.state.
   7350    let state = Cu.cloneInto(startupState, {});
   7351    let hasPinnedTabs = false;
   7352    let defaultState = {
   7353      windows: [],
   7354      selectedWindow: 1,
   7355      savedGroups: state.savedGroups || [],
   7356    };
   7357    state.selectedWindow = state.selectedWindow || 1;
   7358 
   7359    // Fixes bug1954488
   7360    // This solves a case where a user had open tab groups and then quit and
   7361    // restarted the browser at least twice. In this case the saved groups
   7362    // would still be marked as removeAfterRestore groups even though there was
   7363    // no longer an open group associated with them in the lastSessionState.
   7364    // To fix this we clear this property if we see it on saved groups,
   7365    // converting them into permanently saved groups.
   7366    for (let group of defaultState.savedGroups) {
   7367      delete group.removeAfterRestore;
   7368    }
   7369 
   7370    // Look at each window, remove pinned tabs, adjust selectedindex,
   7371    // remove window if necessary.
   7372    for (let wIndex = 0; wIndex < state.windows.length; ) {
   7373      let window = state.windows[wIndex];
   7374      window.selected = window.selected || 1;
   7375      // We're going to put the state of the window into this object, but for closedTabs
   7376      // we want to preserve the original closedTabs since that will be saved as the lastSessionState
   7377      let newWindowState = {
   7378        tabs: [],
   7379      };
   7380      if (PERSIST_SESSIONS) {
   7381        newWindowState._closedTabs = Cu.cloneInto(window._closedTabs, {});
   7382        newWindowState.closedGroups = Cu.cloneInto(window.closedGroups, {});
   7383      }
   7384 
   7385      // We want to preserve the sidebar if previously open in the window
   7386      if (window.sidebar) {
   7387        newWindowState.sidebar = window.sidebar;
   7388      }
   7389 
   7390      let groupsToSave = new Map();
   7391      for (let tIndex = 0; tIndex < window.tabs.length; ) {
   7392        if (window.tabs[tIndex].pinned) {
   7393          // Adjust window.selected
   7394          if (tIndex + 1 < window.selected) {
   7395            window.selected -= 1;
   7396          } else if (tIndex + 1 == window.selected) {
   7397            newWindowState.selected = newWindowState.tabs.length + 1;
   7398          }
   7399          // + 1 because the tab isn't actually in the array yet
   7400 
   7401          // Now add the pinned tab to our window
   7402          newWindowState.tabs = newWindowState.tabs.concat(
   7403            window.tabs.splice(tIndex, 1)
   7404          );
   7405          // We don't want to increment tIndex here.
   7406          continue;
   7407        } else if (window.tabs[tIndex].groupId) {
   7408          // Convert any open groups into saved groups.
   7409          let groupStateToSave = window.groups.find(
   7410            groupState => groupState.id == window.tabs[tIndex].groupId
   7411          );
   7412          let groupToSave = groupsToSave.get(groupStateToSave.id);
   7413          if (!groupToSave) {
   7414            groupToSave =
   7415              lazy.TabGroupState.savedInClosedWindow(groupStateToSave);
   7416            // If the session is manually restored, these groups will be removed from the saved groups list
   7417            // to prevent duplication.
   7418            groupToSave.removeAfterRestore = true;
   7419            groupsToSave.set(groupStateToSave.id, groupToSave);
   7420          }
   7421          let tabToAdd = window.tabs[tIndex];
   7422          groupToSave.tabs.push(this._formatTabStateForSavedGroup(tabToAdd));
   7423        } else if (!window.tabs[tIndex].hidden && PERSIST_SESSIONS) {
   7424          // Add any previously open tabs that aren't pinned or hidden to the recently closed tabs list
   7425          // which we want to persist between sessions; if the session is manually restored, they will
   7426          // be filtered out of the closed tabs list (due to removeAfterRestore property) and reopened
   7427          // per expected session restore behavior.
   7428 
   7429          let tabState = window.tabs[tIndex];
   7430 
   7431          // Ensure the index is in bounds.
   7432          let activeIndex = tabState.index;
   7433          activeIndex = Math.min(activeIndex, tabState.entries.length - 1);
   7434          activeIndex = Math.max(activeIndex, 0);
   7435 
   7436          if (activeIndex in tabState.entries) {
   7437            let title =
   7438              tabState.entries[activeIndex].title ||
   7439              tabState.entries[activeIndex].url;
   7440 
   7441            let tabData = {
   7442              state: tabState,
   7443              title,
   7444              image: tabState.image,
   7445              pos: tIndex,
   7446              closedAt: Date.now(),
   7447              closedInGroup: false,
   7448              removeAfterRestore: true,
   7449            };
   7450 
   7451            if (this._shouldSaveTabState(tabState)) {
   7452              let closedTabsList = newWindowState._closedTabs;
   7453              this.saveClosedTabData(window, closedTabsList, tabData, false);
   7454            }
   7455          }
   7456        }
   7457        tIndex++;
   7458      }
   7459 
   7460      // Any tab groups that were in the tab strip at the end of the last
   7461      // session should be saved. If any tab groups were present in both
   7462      // saved groups and open groups in the last session, set the saved
   7463      // group's `removeAfterRestore` so that if the last session is restored,
   7464      // the group will be opened to the tab strip and removed from the list
   7465      // of saved tab groups.
   7466      groupsToSave.forEach(groupState => {
   7467        const alreadySavedGroup = defaultState.savedGroups.find(
   7468          existingGroup => existingGroup.id == groupState.id
   7469        );
   7470        if (alreadySavedGroup) {
   7471          alreadySavedGroup.removeAfterRestore = true;
   7472        } else {
   7473          defaultState.savedGroups.push(groupState);
   7474        }
   7475      });
   7476 
   7477      hasPinnedTabs ||= !!newWindowState.tabs.length;
   7478 
   7479      // Only transfer over window attributes for pinned tabs, which has
   7480      // already been extracted into newWindowState.tabs.
   7481      if (newWindowState.tabs.length) {
   7482        WINDOW_ATTRIBUTES.forEach(function (attr) {
   7483          if (attr in window) {
   7484            newWindowState[attr] = window[attr];
   7485            delete window[attr];
   7486          }
   7487        });
   7488        // We're just copying position data into the window for pinned tabs.
   7489        // Not copying over:
   7490        // - extData
   7491        // - isPopup
   7492        // - hidden
   7493 
   7494        // Assign a unique ID to correlate the window to be opened with the
   7495        // remaining data
   7496        window.__lastSessionWindowID = newWindowState.__lastSessionWindowID =
   7497          "" + Date.now() + Math.random();
   7498      }
   7499 
   7500      // If this newWindowState contains pinned tabs (stored in tabs) or
   7501      // closed tabs, add it to the defaultState so they're available immediately.
   7502      if (
   7503        newWindowState.tabs.length ||
   7504        (PERSIST_SESSIONS &&
   7505          (newWindowState._closedTabs.length ||
   7506            newWindowState.closedGroups.length))
   7507      ) {
   7508        defaultState.windows.push(newWindowState);
   7509        // Remove the window from the state if it doesn't have any tabs
   7510        if (!window.tabs.length) {
   7511          if (wIndex + 1 <= state.selectedWindow) {
   7512            state.selectedWindow -= 1;
   7513          } else if (wIndex + 1 == state.selectedWindow) {
   7514            defaultState.selectedIndex = defaultState.windows.length + 1;
   7515          }
   7516 
   7517          state.windows.splice(wIndex, 1);
   7518          // We don't want to increment wIndex here.
   7519          continue;
   7520        }
   7521      }
   7522      wIndex++;
   7523    }
   7524 
   7525    if (hasPinnedTabs) {
   7526      // Move cookies over from so that they're restored right away and pinned tabs will load correctly.
   7527      defaultState.cookies = state.cookies;
   7528      delete state.cookies;
   7529    }
   7530    // we return state here rather than startupState so as to avoid duplicating
   7531    // pinned tabs that we add to the defaultState (when a user restores a session)
   7532    return [defaultState, state];
   7533  },
   7534 
   7535  _sendRestoreCompletedNotifications:
   7536    function ssi_sendRestoreCompletedNotifications() {
   7537      // not all windows restored, yet
   7538      if (this._restoreCount > 1) {
   7539        this._restoreCount--;
   7540        this._log.warn(
   7541          `waiting on ${this._restoreCount} windows to be restored before sending restore complete notifications.`
   7542        );
   7543        return;
   7544      }
   7545 
   7546      // observers were already notified
   7547      if (this._restoreCount == -1) {
   7548        return;
   7549      }
   7550 
   7551      // This was the last window restored at startup, notify observers.
   7552      if (!this._browserSetState) {
   7553        Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
   7554        this._log.debug(`All ${this._restoreCount} windows restored`);
   7555        this._deferredAllWindowsRestored.resolve();
   7556      } else {
   7557        // _browserSetState is used only by tests, and it uses an alternate
   7558        // notification in order not to retrigger startup observers that
   7559        // are listening for NOTIFY_WINDOWS_RESTORED.
   7560        Services.obs.notifyObservers(null, NOTIFY_BROWSER_STATE_RESTORED);
   7561      }
   7562 
   7563      // If all windows are on other virtual desktops (on Windows), open a new
   7564      // window on this desktop so the user isn't left wondering where their
   7565      // session went. See bug 1812489.
   7566      let anyWindowNotCloaked = this._browserWindows[Symbol.iterator]().some(
   7567        window => !window.isCloaked
   7568      );
   7569      if (!anyWindowNotCloaked) {
   7570        lazy.BrowserWindowTracker.openWindow();
   7571      }
   7572 
   7573      this._browserSetState = false;
   7574      this._restoreCount = -1;
   7575    },
   7576 
   7577  /**
   7578   * Set the given window's busy state
   7579   *
   7580   * @param aWindow the window
   7581   * @param aValue the window's busy state
   7582   */
   7583  _setWindowStateBusyValue: function ssi_changeWindowStateBusyValue(
   7584    aWindow,
   7585    aValue
   7586  ) {
   7587    this._windows[aWindow.__SSi].busy = aValue;
   7588 
   7589    // Keep the to-be-restored state in sync because that is returned by
   7590    // getWindowState() as long as the window isn't loaded, yet.
   7591    if (!this._isWindowLoaded(aWindow)) {
   7592      let stateToRestore =
   7593        this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)].windows[0];
   7594      stateToRestore.busy = aValue;
   7595    }
   7596  },
   7597 
   7598  /**
   7599   * Set the given window's state to 'not busy'.
   7600   *
   7601   * @param aWindow the window
   7602   */
   7603  _setWindowStateReady: function ssi_setWindowStateReady(aWindow) {
   7604    let newCount = (this._windowBusyStates.get(aWindow) || 0) - 1;
   7605    if (newCount < 0) {
   7606      throw new Error("Invalid window busy state (less than zero).");
   7607    }
   7608    this._windowBusyStates.set(aWindow, newCount);
   7609 
   7610    if (newCount == 0) {
   7611      this._setWindowStateBusyValue(aWindow, false);
   7612      this._sendWindowStateReadyEvent(aWindow);
   7613    }
   7614  },
   7615 
   7616  /**
   7617   * Set the given window's state to 'busy'.
   7618   *
   7619   * @param aWindow the window
   7620   */
   7621  _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) {
   7622    let newCount = (this._windowBusyStates.get(aWindow) || 0) + 1;
   7623    this._windowBusyStates.set(aWindow, newCount);
   7624 
   7625    if (newCount == 1) {
   7626      this._setWindowStateBusyValue(aWindow, true);
   7627      this._sendWindowStateBusyEvent(aWindow);
   7628    }
   7629  },
   7630 
   7631  /**
   7632   * Dispatch an SSWindowStateReady event for the given window.
   7633   *
   7634   * @param aWindow the window
   7635   */
   7636  _sendWindowStateReadyEvent: function ssi_sendWindowStateReadyEvent(aWindow) {
   7637    let event = aWindow.document.createEvent("Events");
   7638    event.initEvent("SSWindowStateReady", true, false);
   7639    aWindow.dispatchEvent(event);
   7640  },
   7641 
   7642  /**
   7643   * Dispatch an SSWindowStateBusy event for the given window.
   7644   *
   7645   * @param aWindow the window
   7646   */
   7647  _sendWindowStateBusyEvent: function ssi_sendWindowStateBusyEvent(aWindow) {
   7648    let event = aWindow.document.createEvent("Events");
   7649    event.initEvent("SSWindowStateBusy", true, false);
   7650    aWindow.dispatchEvent(event);
   7651  },
   7652 
   7653  /**
   7654   * Dispatch the SSWindowRestoring event for the given window.
   7655   *
   7656   * @param aWindow
   7657   *        The window which is going to be restored
   7658   */
   7659  _sendWindowRestoringNotification(aWindow) {
   7660    let event = aWindow.document.createEvent("Events");
   7661    event.initEvent("SSWindowRestoring", true, false);
   7662    aWindow.dispatchEvent(event);
   7663  },
   7664 
   7665  /**
   7666   * Dispatch the SSWindowRestored event for the given window.
   7667   *
   7668   * @param aWindow
   7669   *        The window which has been restored
   7670   */
   7671  _sendWindowRestoredNotification(aWindow) {
   7672    let event = aWindow.document.createEvent("Events");
   7673    event.initEvent("SSWindowRestored", true, false);
   7674    aWindow.dispatchEvent(event);
   7675  },
   7676 
   7677  /**
   7678   * Dispatch the SSTabRestored event for the given tab.
   7679   *
   7680   * @param aTab
   7681   *        The tab which has been restored
   7682   * @param aIsRemotenessUpdate
   7683   *        True if this tab was restored due to flip from running from
   7684   *        out-of-main-process to in-main-process or vice-versa.
   7685   */
   7686  _sendTabRestoredNotification(aTab, aIsRemotenessUpdate) {
   7687    let event = aTab.ownerDocument.createEvent("CustomEvent");
   7688    event.initCustomEvent("SSTabRestored", true, false, {
   7689      isRemotenessUpdate: aIsRemotenessUpdate,
   7690    });
   7691    aTab.dispatchEvent(event);
   7692  },
   7693 
   7694  /**
   7695   * @param aWindow
   7696   *        Window reference
   7697   * @returns whether this window's data is still cached in _statesToRestore
   7698   *          because it's not fully loaded yet
   7699   */
   7700  _isWindowLoaded: function ssi_isWindowLoaded(aWindow) {
   7701    return !WINDOW_RESTORE_IDS.has(aWindow);
   7702  },
   7703 
   7704  /**
   7705   * Resize this._closedWindows to the value of the pref, except in the case
   7706   * where we don't have any non-popup windows on Windows and Linux. Then we must
   7707   * resize such that we have at least one non-popup window.
   7708   */
   7709  _capClosedWindows: function ssi_capClosedWindows() {
   7710    if (this._closedWindows.length <= this._max_windows_undo) {
   7711      return;
   7712    }
   7713    let spliceTo = this._max_windows_undo;
   7714    if (AppConstants.platform != "macosx") {
   7715      let normalWindowIndex = 0;
   7716      // try to find a non-popup window in this._closedWindows
   7717      while (
   7718        normalWindowIndex < this._closedWindows.length &&
   7719        !!this._closedWindows[normalWindowIndex].isPopup
   7720      ) {
   7721        normalWindowIndex++;
   7722      }
   7723      if (normalWindowIndex >= this._max_windows_undo) {
   7724        spliceTo = normalWindowIndex + 1;
   7725      }
   7726    }
   7727    if (spliceTo < this._closedWindows.length) {
   7728      this._closedWindows.splice(spliceTo, this._closedWindows.length);
   7729      this._closedObjectsChanged = true;
   7730    }
   7731  },
   7732 
   7733  /**
   7734   * Clears the set of windows that are "resurrected" before writing to disk to
   7735   * make closing windows one after the other until shutdown work as expected.
   7736   *
   7737   * This function should only be called when we are sure that there has been
   7738   * a user action that indicates the browser is actively being used and all
   7739   * windows that have been closed before are not part of a series of closing
   7740   * windows.
   7741   */
   7742  _clearRestoringWindows: function ssi_clearRestoringWindows() {
   7743    for (let i = 0; i < this._closedWindows.length; i++) {
   7744      delete this._closedWindows[i]._shouldRestore;
   7745    }
   7746  },
   7747 
   7748  /**
   7749   * Reset state to prepare for a new session state to be restored.
   7750   */
   7751  _resetRestoringState: function ssi_initRestoringState() {
   7752    TabRestoreQueue.reset();
   7753    this._tabsRestoringCount = 0;
   7754  },
   7755 
   7756  /**
   7757   * Reset the restoring state for a particular tab. This will be called when
   7758   * removing a tab or when a tab needs to be reset (it's being overwritten).
   7759   *
   7760   * @param aTab
   7761   *        The tab that will be "reset"
   7762   */
   7763  _resetLocalTabRestoringState(aTab) {
   7764    let browser = aTab.linkedBrowser;
   7765 
   7766    // Keep the tab's previous state for later in this method
   7767    let previousState = TAB_STATE_FOR_BROWSER.get(browser);
   7768 
   7769    if (!previousState) {
   7770      console.error("Given tab is not restoring.");
   7771      return;
   7772    }
   7773 
   7774    // The browser is no longer in any sort of restoring state.
   7775    TAB_STATE_FOR_BROWSER.delete(browser);
   7776 
   7777    this._restoreListeners.get(browser.permanentKey)?.unregister();
   7778    browser.browsingContext.clearRestoreState();
   7779 
   7780    aTab.removeAttribute("pending");
   7781    aTab.removeAttribute("discarded");
   7782 
   7783    if (previousState == TAB_STATE_RESTORING) {
   7784      if (this._tabsRestoringCount) {
   7785        this._tabsRestoringCount--;
   7786      }
   7787    } else if (previousState == TAB_STATE_NEEDS_RESTORE) {
   7788      // Make sure that the tab is removed from the list of tabs to restore.
   7789      // Again, this is normally done in restoreTabContent, but that isn't being called
   7790      // for this tab.
   7791      TabRestoreQueue.remove(aTab);
   7792    }
   7793  },
   7794 
   7795  _resetTabRestoringState(tab) {
   7796    let browser = tab.linkedBrowser;
   7797 
   7798    if (!TAB_STATE_FOR_BROWSER.has(browser)) {
   7799      console.error("Given tab is not restoring.");
   7800      return;
   7801    }
   7802 
   7803    this._resetLocalTabRestoringState(tab);
   7804  },
   7805 
   7806  /**
   7807   * Each fresh tab starts out with epoch=0. This function can be used to
   7808   * start a next epoch by incrementing the current value. It will enables us
   7809   * to ignore stale messages sent from previous epochs. The function returns
   7810   * the new epoch ID for the given |browser|.
   7811   */
   7812  startNextEpoch(permanentKey) {
   7813    let next = this.getCurrentEpoch(permanentKey) + 1;
   7814    this._browserEpochs.set(permanentKey, next);
   7815    return next;
   7816  },
   7817 
   7818  /**
   7819   * Returns the current epoch for the given <browser>. If we haven't assigned
   7820   * a new epoch this will default to zero for new tabs.
   7821   */
   7822  getCurrentEpoch(permanentKey) {
   7823    return this._browserEpochs.get(permanentKey) || 0;
   7824  },
   7825 
   7826  /**
   7827   * Each time a <browser> element is restored, we increment its "epoch". To
   7828   * check if a message from content-sessionStore.js is out of date, we can
   7829   * compare the epoch received with the message to the <browser> element's
   7830   * epoch. This function does that, and returns true if |epoch| is up-to-date
   7831   * with respect to |browser|.
   7832   */
   7833  isCurrentEpoch(permanentKey, epoch) {
   7834    return this.getCurrentEpoch(permanentKey) == epoch;
   7835  },
   7836 
   7837  /**
   7838   * Resets the epoch for a given <browser>. We need to this every time we
   7839   * receive a hint that a new docShell has been loaded into the browser as
   7840   * the frame script starts out with epoch=0.
   7841   */
   7842  resetEpoch(permanentKey, frameLoader = null) {
   7843    this._browserEpochs.delete(permanentKey);
   7844    if (frameLoader) {
   7845      frameLoader.requestEpochUpdate(0);
   7846    }
   7847  },
   7848 
   7849  /**
   7850   * Countdown for a given duration, skipping beats if the computer is too busy,
   7851   * sleeping or otherwise unavailable.
   7852   *
   7853   * @param {number} delay An approximate delay to wait in milliseconds (rounded
   7854   * up to the closest second).
   7855   *
   7856   * @return Promise
   7857   */
   7858  looseTimer(delay) {
   7859    let DELAY_BEAT = 1000;
   7860    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   7861    let beats = Math.ceil(delay / DELAY_BEAT);
   7862    let deferred = Promise.withResolvers();
   7863    timer.initWithCallback(
   7864      function () {
   7865        if (beats <= 0) {
   7866          this._log.debug(`looseTimer of ${delay} timed out`);
   7867          Glean.sessionRestore.shutdownFlushAllOutcomes.timed_out.add(1);
   7868          deferred.resolve();
   7869        }
   7870        --beats;
   7871      },
   7872      DELAY_BEAT,
   7873      Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP
   7874    );
   7875    // Ensure that the timer is both canceled once we are done with it
   7876    // and not garbage-collected until then.
   7877    deferred.promise.then(
   7878      () => timer.cancel(),
   7879      () => timer.cancel()
   7880    );
   7881    return deferred;
   7882  },
   7883 
   7884  _waitForStateStop(browser, expectedURL = null) {
   7885    const deferred = Promise.withResolvers();
   7886 
   7887    const listener = {
   7888      unregister(reject = true) {
   7889        if (reject) {
   7890          deferred.reject();
   7891        }
   7892 
   7893        SessionStoreInternal._restoreListeners.delete(browser.permanentKey);
   7894 
   7895        try {
   7896          browser.removeProgressListener(
   7897            this,
   7898            Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
   7899          );
   7900        } catch {} // May have already gotten rid of the browser's webProgress.
   7901      },
   7902 
   7903      onStateChange(webProgress, request, stateFlags) {
   7904        if (
   7905          webProgress.isTopLevel &&
   7906          stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
   7907          stateFlags & Ci.nsIWebProgressListener.STATE_STOP
   7908        ) {
   7909          // FIXME: We sometimes see spurious STATE_STOP events for about:blank
   7910          // loads, so we have to account for that here.
   7911          let aboutBlankOK = !expectedURL || expectedURL === "about:blank";
   7912          let url = request.QueryInterface(Ci.nsIChannel).originalURI.spec;
   7913          if (url !== "about:blank" || aboutBlankOK) {
   7914            this.unregister(false);
   7915            deferred.resolve();
   7916          }
   7917        }
   7918      },
   7919 
   7920      QueryInterface: ChromeUtils.generateQI([
   7921        "nsIWebProgressListener",
   7922        "nsISupportsWeakReference",
   7923      ]),
   7924    };
   7925 
   7926    this._restoreListeners.get(browser.permanentKey)?.unregister();
   7927    this._restoreListeners.set(browser.permanentKey, listener);
   7928 
   7929    browser.addProgressListener(
   7930      listener,
   7931      Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
   7932    );
   7933 
   7934    return deferred.promise;
   7935  },
   7936 
   7937  _listenForNavigations(browser, callbacks) {
   7938    const listener = {
   7939      unregister() {
   7940        browser.browsingContext?.sessionHistory?.removeSHistoryListener(this);
   7941 
   7942        try {
   7943          browser.removeProgressListener(
   7944            this,
   7945            Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
   7946          );
   7947        } catch {} // May have already gotten rid of the browser's webProgress.
   7948 
   7949        SessionStoreInternal._restoreListeners.delete(browser.permanentKey);
   7950      },
   7951 
   7952      OnHistoryReload() {
   7953        this.unregister();
   7954        return callbacks.onHistoryReload();
   7955      },
   7956 
   7957      // TODO(kashav): ContentRestore.sys.mjs handles OnHistoryNewEntry
   7958      // separately, so we should eventually support that here as well.
   7959      OnHistoryNewEntry() {},
   7960      OnHistoryGotoIndex() {},
   7961      OnHistoryPurge() {},
   7962      OnHistoryReplaceEntry() {},
   7963 
   7964      onStateChange(webProgress, request, stateFlags) {
   7965        if (
   7966          webProgress.isTopLevel &&
   7967          stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
   7968          stateFlags & Ci.nsIWebProgressListener.STATE_START
   7969        ) {
   7970          this.unregister();
   7971          callbacks.onStartRequest();
   7972        }
   7973      },
   7974 
   7975      QueryInterface: ChromeUtils.generateQI([
   7976        "nsISHistoryListener",
   7977        "nsIWebProgressListener",
   7978        "nsISupportsWeakReference",
   7979      ]),
   7980    };
   7981 
   7982    this._restoreListeners.get(browser.permanentKey)?.unregister();
   7983    this._restoreListeners.set(browser.permanentKey, listener);
   7984 
   7985    browser.browsingContext?.sessionHistory?.addSHistoryListener(listener);
   7986 
   7987    browser.addProgressListener(
   7988      listener,
   7989      Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
   7990    );
   7991  },
   7992 
   7993  /**
   7994   * This mirrors ContentRestore.restoreHistory() for parent process session
   7995   * history restores.
   7996   */
   7997  _restoreHistory(browser, data) {
   7998    this._tabStateToRestore.set(browser.permanentKey, data);
   7999 
   8000    // In case about:blank isn't done yet.
   8001    // XXX(kashav): Does this actually accomplish anything? Can we remove?
   8002    browser.stop();
   8003 
   8004    lazy.SessionHistory.restoreFromParent(
   8005      browser.browsingContext.sessionHistory,
   8006      data.tabData
   8007    );
   8008 
   8009    let url = data.tabData?.entries[data.tabData.index - 1]?.url;
   8010    let disallow = data.tabData?.disallow;
   8011 
   8012    let promise = SessionStoreUtils.restoreDocShellState(
   8013      browser.browsingContext,
   8014      url,
   8015      disallow
   8016    );
   8017    this._tabStateRestorePromises.set(browser.permanentKey, promise);
   8018 
   8019    const onResolve = () => {
   8020      if (TAB_STATE_FOR_BROWSER.get(browser) !== TAB_STATE_RESTORING) {
   8021        this._listenForNavigations(browser, {
   8022          // The history entry was reloaded before we began restoring tab
   8023          // content, just proceed as we would normally.
   8024          onHistoryReload: () => {
   8025            this._restoreTabContent(browser);
   8026            return false;
   8027          },
   8028 
   8029          // Some foreign code, like an extension, loaded a new URI on the
   8030          // browser. We no longer want to restore saved tab data, but may
   8031          // still have browser state that needs to be restored.
   8032          onStartRequest: () => {
   8033            this._tabStateToRestore.delete(browser.permanentKey);
   8034            this._restoreTabContent(browser);
   8035          },
   8036        });
   8037      }
   8038 
   8039      this._tabStateRestorePromises.delete(browser.permanentKey);
   8040 
   8041      this._restoreHistoryComplete(browser);
   8042    };
   8043 
   8044    promise.then(onResolve).catch(() => {});
   8045  },
   8046 
   8047  /**
   8048   * Either load the saved typed value or restore the active history entry.
   8049   * If neither is possible, just load an empty document.
   8050   */
   8051  _restoreTabEntry(browser, tabData) {
   8052    let haveUserTypedValue = tabData.userTypedValue && tabData.userTypedClear;
   8053    // First take care of the common case where we load the history entry.
   8054    if (!haveUserTypedValue && tabData.entries.length) {
   8055      return SessionStoreUtils.initializeRestore(
   8056        browser.browsingContext,
   8057        lazy.SessionStoreHelper.buildRestoreData(
   8058          tabData.formdata,
   8059          tabData.scroll
   8060        )
   8061      );
   8062    }
   8063    // Here, we need to load user data or about:blank instead.
   8064    // As it's user-typed (or blank), it gets system triggering principal:
   8065    let triggeringPrincipal =
   8066      Services.scriptSecurityManager.getSystemPrincipal();
   8067    // Bypass all the fixup goop for about:blank:
   8068    if (!haveUserTypedValue) {
   8069      let blankPromise = this._waitForStateStop(browser, "about:blank");
   8070      browser.browsingContext.loadURI(lazy.blankURI, {
   8071        triggeringPrincipal,
   8072        loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
   8073      });
   8074      return blankPromise;
   8075    }
   8076 
   8077    // We have a user typed value, load that with fixup:
   8078    let loadPromise = this._waitForStateStop(browser, tabData.userTypedValue);
   8079    browser.browsingContext.fixupAndLoadURIString(tabData.userTypedValue, {
   8080      loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
   8081      triggeringPrincipal,
   8082    });
   8083 
   8084    return loadPromise;
   8085  },
   8086 
   8087  /**
   8088   * This mirrors ContentRestore.restoreTabContent() for parent process session
   8089   * history restores.
   8090   */
   8091  _restoreTabContent(browser, options = {}) {
   8092    this._restoreListeners.get(browser.permanentKey)?.unregister();
   8093 
   8094    this._restoreTabContentStarted(browser, options);
   8095 
   8096    let state = this._tabStateToRestore.get(browser.permanentKey);
   8097    this._tabStateToRestore.delete(browser.permanentKey);
   8098 
   8099    let promises = [this._tabStateRestorePromises.get(browser.permanentKey)];
   8100 
   8101    if (state) {
   8102      promises.push(this._restoreTabEntry(browser, state.tabData));
   8103    } else {
   8104      // The browser started another load, so we decided to not restore
   8105      // saved tab data. We should still wait for that new load to finish
   8106      // before proceeding.
   8107      promises.push(this._waitForStateStop(browser));
   8108    }
   8109 
   8110    Promise.allSettled(promises).then(() => {
   8111      this._restoreTabContentComplete(browser, options);
   8112    });
   8113  },
   8114 
   8115  _sendRestoreTabContent(browser, options) {
   8116    this._restoreTabContent(browser, options);
   8117  },
   8118 
   8119  _restoreHistoryComplete(browser) {
   8120    let win = browser.ownerGlobal;
   8121    let tab = win?.gBrowser.getTabForBrowser(browser);
   8122    if (!tab) {
   8123      return;
   8124    }
   8125 
   8126    // Notify the tabbrowser that the tab chrome has been restored.
   8127    let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
   8128 
   8129    // Update tab label and icon again after the tab history was updated.
   8130    this.updateTabLabelAndIcon(tab, tabData);
   8131 
   8132    let event = win.document.createEvent("Events");
   8133    event.initEvent("SSTabRestoring", true, false);
   8134    tab.dispatchEvent(event);
   8135  },
   8136 
   8137  _restoreTabContentStarted(browser, data) {
   8138    let win = browser.ownerGlobal;
   8139    let tab = win?.gBrowser.getTabForBrowser(browser);
   8140    if (!tab) {
   8141      return;
   8142    }
   8143 
   8144    let initiatedBySessionStore =
   8145      TAB_STATE_FOR_BROWSER.get(browser) != TAB_STATE_NEEDS_RESTORE;
   8146    let isNavigateAndRestore =
   8147      data.reason == RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE;
   8148 
   8149    // We need to be careful when restoring the urlbar's search mode because
   8150    // we race a call to gURLBar.setURI due to the location change.  setURI
   8151    // will exit search mode and set gURLBar.value to the restored URL,
   8152    // clobbering any search mode and userTypedValue we restore here.  If
   8153    // this is a typical restore -- restoring on startup or restoring a
   8154    // closed tab for example -- then we need to restore search mode after
   8155    // that setURI call, and so we wait until restoreTabContentComplete, at
   8156    // which point setURI will have been called.  If this is not a typical
   8157    // restore -- it was not initiated by session store or it's due to a
   8158    // remoteness change -- then we do not want to restore search mode at
   8159    // all, and so we remove it from the tab state cache.  In particular, if
   8160    // the restore is due to a remoteness change, then the user is loading a
   8161    // new URL and the current search mode should not be carried over to it.
   8162    let cacheState = lazy.TabStateCache.get(browser.permanentKey);
   8163    if (cacheState.searchMode) {
   8164      if (!initiatedBySessionStore || isNavigateAndRestore) {
   8165        lazy.TabStateCache.update(browser.permanentKey, {
   8166          searchMode: null,
   8167          userTypedValue: null,
   8168        });
   8169      }
   8170      return;
   8171    }
   8172 
   8173    if (!initiatedBySessionStore) {
   8174      // If a load not initiated by sessionstore was started in a
   8175      // previously pending tab. Mark the tab as no longer pending.
   8176      this.markTabAsRestoring(tab);
   8177    } else if (!isNavigateAndRestore) {
   8178      // If the user was typing into the URL bar when we crashed, but hadn't hit
   8179      // enter yet, then we just need to write that value to the URL bar without
   8180      // loading anything. This must happen after the load, as the load will clear
   8181      // userTypedValue.
   8182      //
   8183      // Note that we only want to do that if we're restoring state for reasons
   8184      // _other_ than a navigateAndRestore remoteness-flip, as such a flip
   8185      // implies that the user was navigating.
   8186      let tabData = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
   8187      if (
   8188        tabData.userTypedValue &&
   8189        !tabData.userTypedClear &&
   8190        !browser.userTypedValue
   8191      ) {
   8192        browser.userTypedValue = tabData.userTypedValue;
   8193        if (tab.selected) {
   8194          win.gURLBar.setURI();
   8195        }
   8196      }
   8197 
   8198      // Remove state we don't need any longer.
   8199      lazy.TabStateCache.update(browser.permanentKey, {
   8200        userTypedValue: null,
   8201        userTypedClear: null,
   8202      });
   8203    }
   8204  },
   8205 
   8206  _restoreTabContentComplete(browser, data) {
   8207    let win = browser.ownerGlobal;
   8208    let tab = browser.ownerGlobal?.gBrowser.getTabForBrowser(browser);
   8209    if (!tab) {
   8210      return;
   8211    }
   8212    // Restore search mode and its search string in userTypedValue, if
   8213    // appropriate.
   8214    let cacheState = lazy.TabStateCache.get(browser.permanentKey);
   8215    if (cacheState.searchMode) {
   8216      win.gURLBar.setSearchMode(cacheState.searchMode, browser);
   8217      browser.userTypedValue = cacheState.userTypedValue;
   8218      if (tab.selected) {
   8219        win.gURLBar.setURI();
   8220      }
   8221      lazy.TabStateCache.update(browser.permanentKey, {
   8222        searchMode: null,
   8223        userTypedValue: null,
   8224      });
   8225    }
   8226 
   8227    // This callback is used exclusively by tests that want to
   8228    // monitor the progress of network loads.
   8229    if (gDebuggingEnabled) {
   8230      Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED);
   8231    }
   8232 
   8233    SessionStoreInternal._resetLocalTabRestoringState(tab);
   8234    SessionStoreInternal.restoreNextTab();
   8235 
   8236    this._sendTabRestoredNotification(tab, data.isRemotenessUpdate);
   8237 
   8238    Services.obs.notifyObservers(null, "sessionstore-one-or-no-tab-restored");
   8239  },
   8240 
   8241  /**
   8242   * Send the "SessionStore:restoreHistory" message to content, triggering a
   8243   * content restore. This method is intended to be used internally by
   8244   * SessionStore, as it also ensures that permissions are avaliable in the
   8245   * content process before triggering the history restore in the content
   8246   * process.
   8247   *
   8248   * @param browser The browser to transmit the permissions for
   8249   * @param options The options data to send to content.
   8250   */
   8251  _sendRestoreHistory(browser, options) {
   8252    if (options.tabData.storage) {
   8253      SessionStoreUtils.restoreSessionStorageFromParent(
   8254        browser.browsingContext,
   8255        options.tabData.storage
   8256      );
   8257      delete options.tabData.storage;
   8258    }
   8259 
   8260    this._restoreHistory(browser, options);
   8261 
   8262    if (browser && browser.frameLoader) {
   8263      browser.frameLoader.requestEpochUpdate(options.epoch);
   8264    }
   8265  },
   8266 
   8267  /**
   8268   * @param {MozTabbrowserTabGroup} tabGroup
   8269   */
   8270  addSavedTabGroup(tabGroup) {
   8271    if (PrivateBrowsingUtils.isWindowPrivate(tabGroup.ownerGlobal)) {
   8272      throw new Error("Refusing to save tab group from private window");
   8273    }
   8274 
   8275    let tabGroupState = lazy.TabGroupState.savedInOpenWindow(
   8276      tabGroup,
   8277      tabGroup.ownerGlobal.__SSi
   8278    );
   8279    tabGroupState.tabs = this._collectClosedTabsForTabGroup(
   8280      tabGroup.tabs,
   8281      tabGroup.ownerGlobal
   8282    );
   8283    this._recordSavedTabGroupState(tabGroupState);
   8284  },
   8285 
   8286  /**
   8287   * @param {string} tabGroupId
   8288   * @param {MozTabbrowserTab[]} tabs
   8289   * @param {TabMetricsContext} [metricsContext]
   8290   * @returns {SavedTabGroupStateData}
   8291   */
   8292  addTabsToSavedGroup(tabGroupId, tabs, metricsContext) {
   8293    let tabGroupState = this.getSavedTabGroup(tabGroupId);
   8294    if (!tabGroupState) {
   8295      throw new Error(`No tab group found with id ${tabGroupId}`);
   8296    }
   8297 
   8298    const win = tabs[0].ownerGlobal;
   8299    if (!tabs.every(tab => tab.ownerGlobal === win)) {
   8300      throw new Error(`All tabs must be part of the same window`);
   8301    }
   8302 
   8303    if (PrivateBrowsingUtils.isWindowPrivate(win)) {
   8304      throw new Error(
   8305        "Refusing to add tabs from private window to a saved tab group"
   8306      );
   8307    }
   8308 
   8309    let newTabState = this._collectClosedTabsForTabGroup(tabs, win, {
   8310      updateTabGroupId: tabGroupId,
   8311    });
   8312    tabGroupState.tabs.push(...newTabState);
   8313 
   8314    let isVerticalMode = win.gBrowser.tabContainer.verticalMode;
   8315    Glean.tabgroup.addTab.record({
   8316      source:
   8317        metricsContext?.telemetrySource || TabMetrics.METRIC_SOURCE.UNKNOWN,
   8318      tabs: tabs.length,
   8319      layout: isVerticalMode
   8320        ? TabMetrics.METRIC_TABS_LAYOUT.VERTICAL
   8321        : TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL,
   8322      group_type: TabMetrics.METRIC_GROUP_TYPE.SAVED,
   8323    });
   8324 
   8325    this._notifyOfSavedTabGroupsChange();
   8326    return tabGroupState;
   8327  },
   8328 
   8329  /**
   8330   * @param {SavedTabGroupStateData} savedTabGroupState
   8331   * @returns {void}
   8332   */
   8333  _recordSavedTabGroupState(savedTabGroupState) {
   8334    if (
   8335      !savedTabGroupState.tabs.length ||
   8336      this.getSavedTabGroup(savedTabGroupState.id)
   8337    ) {
   8338      return;
   8339    }
   8340    this._savedGroups.push(savedTabGroupState);
   8341    this._notifyOfSavedTabGroupsChange();
   8342  },
   8343 
   8344  /**
   8345   * @param {string} tabGroupId
   8346   * @returns {SavedTabGroupStateData|undefined}
   8347   */
   8348  getSavedTabGroup(tabGroupId) {
   8349    return this._savedGroups.find(
   8350      savedTabGroup => savedTabGroup.id == tabGroupId
   8351    );
   8352  },
   8353 
   8354  /**
   8355   * Returns all tab groups that were saved in this session.
   8356   *
   8357   * @returns {SavedTabGroupStateData[]}
   8358   */
   8359  getSavedTabGroups() {
   8360    return Cu.cloneInto(this._savedGroups, {});
   8361  },
   8362 
   8363  /**
   8364   * @param {Window|{sourceWindowId: string}|{sourceClosedId: number}} source
   8365   * @param {string} tabGroupId
   8366   * @returns {ClosedTabGroupStateData|undefined}
   8367   */
   8368  getClosedTabGroup(source, tabGroupId) {
   8369    let winData = this._resolveClosedDataSource(source);
   8370    return winData?.closedGroups.find(
   8371      closedGroup => closedGroup.id == tabGroupId
   8372    );
   8373  },
   8374 
   8375  /**
   8376   * @param {Window | object} source
   8377   * @param {string} tabGroupId
   8378   * @param {Window} [targetWindow]
   8379   * @returns {MozTabbrowserTabGroup}
   8380   */
   8381  undoCloseTabGroup(source, tabGroupId, targetWindow) {
   8382    const sourceWinData = this._resolveClosedDataSource(source);
   8383    const isPrivateSource = Boolean(sourceWinData.isPrivate);
   8384    if (targetWindow && !targetWindow.__SSi) {
   8385      throw Components.Exception(
   8386        "Target window is not tracked",
   8387        Cr.NS_ERROR_INVALID_ARG
   8388      );
   8389    } else if (!targetWindow) {
   8390      targetWindow = this._getTopWindow(isPrivateSource);
   8391    }
   8392    if (
   8393      isPrivateSource !== PrivateBrowsingUtils.isWindowPrivate(targetWindow)
   8394    ) {
   8395      throw Components.Exception(
   8396        "Target window doesn't have the same privateness as the source window",
   8397        Cr.NS_ERROR_INVALID_ARG
   8398      );
   8399    }
   8400 
   8401    let tabGroupData = this.getClosedTabGroup(source, tabGroupId);
   8402    if (!tabGroupData) {
   8403      throw Components.Exception(
   8404        "Tab group not found in source",
   8405        Cr.NS_ERROR_INVALID_ARG
   8406      );
   8407    }
   8408 
   8409    let group = this._createTabsForSavedOrClosedTabGroup(
   8410      tabGroupData,
   8411      targetWindow
   8412    );
   8413    this.forgetClosedTabGroup(source, tabGroupId);
   8414    sourceWinData.lastClosedTabGroupId = null;
   8415 
   8416    Glean.tabgroup.groupInteractions.open_recent.add(1);
   8417 
   8418    let isVerticalMode = targetWindow.gBrowser.tabContainer.verticalMode;
   8419    Glean.tabgroup.reopen.record({
   8420      id: tabGroupId,
   8421      source: TabMetrics.METRIC_SOURCE.RECENT_TABS,
   8422      type: TabMetrics.METRIC_REOPEN_TYPE.DELETED,
   8423      layout: isVerticalMode
   8424        ? TabMetrics.METRIC_TABS_LAYOUT.VERTICAL
   8425        : TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL,
   8426    });
   8427 
   8428    group.select();
   8429    return group;
   8430  },
   8431 
   8432  /**
   8433   * @param {string} tabGroupId
   8434   * @param {Window} [targetWindow]
   8435   * @returns {MozTabbrowserTabGroup}
   8436   */
   8437  openSavedTabGroup(tabGroupId, targetWindow) {
   8438    if (!targetWindow) {
   8439      targetWindow = this._getTopWindow();
   8440    }
   8441    if (!targetWindow.__SSi) {
   8442      throw Components.Exception(
   8443        "Target window is not tracked",
   8444        Cr.NS_ERROR_INVALID_ARG
   8445      );
   8446    }
   8447    if (PrivateBrowsingUtils.isWindowPrivate(targetWindow)) {
   8448      throw Components.Exception(
   8449        "Cannot open a saved tab group in a private window",
   8450        Cr.NS_ERROR_INVALID_ARG
   8451      );
   8452    }
   8453 
   8454    let tabGroupData = this.getSavedTabGroup(tabGroupId);
   8455    if (!tabGroupData) {
   8456      throw Components.Exception(
   8457        "No saved tab group with specified id",
   8458        Cr.NS_ERROR_INVALID_ARG
   8459      );
   8460    }
   8461 
   8462    // If this saved tab group is present in a closed window, then we need to
   8463    // remove references to this saved tab group from that closed window. The
   8464    // result should be as if the saved tab group "moved" from the closed window
   8465    // into the `targetWindow`.
   8466    if (tabGroupData.windowClosedId) {
   8467      let closedWinData = this.getClosedWindowDataByClosedId(
   8468        tabGroupData.windowClosedId
   8469      );
   8470      if (closedWinData) {
   8471        this._removeSavedTabGroupFromClosedWindow(
   8472          closedWinData,
   8473          tabGroupData.id
   8474        );
   8475      }
   8476    }
   8477 
   8478    let group = this._createTabsForSavedOrClosedTabGroup(
   8479      tabGroupData,
   8480      targetWindow
   8481    );
   8482    this.forgetSavedTabGroup(tabGroupId);
   8483 
   8484    group.select();
   8485    return group;
   8486  },
   8487 
   8488  /**
   8489   * @param {ClosedTabGroupStateData|SavedTabGroupStateData} tabGroupData
   8490   * @param {Window} targetWindow
   8491   * @returns {MozTabbrowserTabGroup}
   8492   */
   8493  _createTabsForSavedOrClosedTabGroup(tabGroupData, targetWindow) {
   8494    let tabDataList = tabGroupData.tabs.map(tab => tab.state);
   8495    let tabs = targetWindow.gBrowser.createTabsForSessionRestore(
   8496      true,
   8497      0, // TODO Bug 1933113 - Save tab group position and selected tab with saved tab group data
   8498      tabDataList,
   8499      [tabGroupData]
   8500    );
   8501 
   8502    this.restoreTabs(targetWindow, tabs, tabDataList, 0);
   8503    return tabs[0].group;
   8504  },
   8505 
   8506  /**
   8507   * Remove tab groups from the closedGroups list that have no tabs associated
   8508   * with them.
   8509   *
   8510   * This can sometimes happen because tab groups are immediately
   8511   * added to closedGroups on closing, before the complete history of the tabs
   8512   * within the group have been processed. If it is later determined that none
   8513   * of the tabs in the group were "worth saving", the group will be empty.
   8514   * This can also happen if a user "undoes" the last closed tab in a closed tab
   8515   * group.
   8516   *
   8517   * See: bug1933966, bug1933485
   8518   *
   8519   * @param {WindowStateData} winData
   8520   */
   8521  _cleanupOrphanedClosedGroups(winData) {
   8522    if (!winData.closedGroups) {
   8523      return;
   8524    }
   8525    for (let index = winData.closedGroups.length - 1; index >= 0; index--) {
   8526      if (winData.closedGroups[index].tabs.length === 0) {
   8527        winData.closedGroups.splice(index, 1);
   8528        this._closedObjectsChanged = true;
   8529      }
   8530    }
   8531  },
   8532 
   8533  /**
   8534   * @param {WindowStateData} closedWinData
   8535   * @param {string} tabGroupId
   8536   * @returns {void} modifies the data in argument `closedWinData`
   8537   */
   8538  _removeSavedTabGroupFromClosedWindow(closedWinData, tabGroupId) {
   8539    removeWhere(closedWinData.groups, tabGroup => tabGroup.id == tabGroupId);
   8540    removeWhere(closedWinData.tabs, tab => tab.groupId == tabGroupId);
   8541    this._closedObjectsChanged = true;
   8542  },
   8543 
   8544  /**
   8545   * Validates that a state object matches the schema
   8546   * defined in browser/components/sessionstore/session.schema.json
   8547   *
   8548   * @param {object} [state] State object to validate. If not provided,
   8549   *   will validate the current session state.
   8550   * @returns {Promise} A promise which resolves to a validation result object
   8551   */
   8552  async validateState(state) {
   8553    if (!state) {
   8554      state = this.getCurrentState();
   8555      // Don't include the last session state in getBrowserState().
   8556      delete state.lastSessionState;
   8557      // Don't include any deferred initial state.
   8558      delete state.deferredInitialState;
   8559    }
   8560    const schema = await fetch(
   8561      "resource:///modules/sessionstore/session.schema.json"
   8562    ).then(rsp => rsp.json());
   8563 
   8564    let result;
   8565    try {
   8566      result = lazy.JsonSchema.validate(state, schema);
   8567      if (!result.valid) {
   8568        console.warn(
   8569          "Session state didn't validate against the schema",
   8570          result.errors
   8571        );
   8572      }
   8573    } catch (ex) {
   8574      console.error(`Error validating session state: ${ex.message}`, ex);
   8575    }
   8576    return result;
   8577  },
   8578 };
   8579 
   8580 /**
   8581 * Priority queue that keeps track of a list of tabs to restore and returns
   8582 * the tab we should restore next, based on priority rules. We decide between
   8583 * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only
   8584 * restored with restore_hidden_tabs=true.
   8585 */
   8586 var TabRestoreQueue = {
   8587  // The separate buckets used to store tabs.
   8588  tabs: { priority: [], visible: [], hidden: [] },
   8589 
   8590  // Preferences used by the TabRestoreQueue to determine which tabs
   8591  // are restored automatically and which tabs will be on-demand.
   8592  prefs: {
   8593    // Lazy getter that returns whether tabs are restored on demand.
   8594    get restoreOnDemand() {
   8595      let updateValue = () => {
   8596        let value = Services.prefs.getBoolPref(PREF);
   8597        let definition = { value, configurable: true };
   8598        Object.defineProperty(this, "restoreOnDemand", definition);
   8599        return value;
   8600      };
   8601 
   8602      const PREF = "browser.sessionstore.restore_on_demand";
   8603      Services.prefs.addObserver(PREF, updateValue);
   8604      return updateValue();
   8605    },
   8606 
   8607    // Lazy getter that returns whether pinned tabs are restored on demand.
   8608    get restorePinnedTabsOnDemand() {
   8609      let updateValue = () => {
   8610        let value = Services.prefs.getBoolPref(PREF);
   8611        let definition = { value, configurable: true };
   8612        Object.defineProperty(this, "restorePinnedTabsOnDemand", definition);
   8613        return value;
   8614      };
   8615 
   8616      const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand";
   8617      Services.prefs.addObserver(PREF, updateValue);
   8618      return updateValue();
   8619    },
   8620 
   8621    // Lazy getter that returns whether we should restore hidden tabs.
   8622    get restoreHiddenTabs() {
   8623      let updateValue = () => {
   8624        let value = Services.prefs.getBoolPref(PREF);
   8625        let definition = { value, configurable: true };
   8626        Object.defineProperty(this, "restoreHiddenTabs", definition);
   8627        return value;
   8628      };
   8629 
   8630      const PREF = "browser.sessionstore.restore_hidden_tabs";
   8631      Services.prefs.addObserver(PREF, updateValue);
   8632      return updateValue();
   8633    },
   8634  },
   8635 
   8636  // Resets the queue and removes all tabs.
   8637  reset() {
   8638    this.tabs = { priority: [], visible: [], hidden: [] };
   8639  },
   8640 
   8641  // Adds a tab to the queue and determines its priority bucket.
   8642  add(tab) {
   8643    let { priority, hidden, visible } = this.tabs;
   8644 
   8645    if (tab.pinned) {
   8646      priority.push(tab);
   8647    } else if (tab.hidden) {
   8648      hidden.push(tab);
   8649    } else {
   8650      visible.push(tab);
   8651    }
   8652  },
   8653 
   8654  // Removes a given tab from the queue, if it's in there.
   8655  remove(tab) {
   8656    let { priority, hidden, visible } = this.tabs;
   8657 
   8658    // We'll always check priority first since we don't
   8659    // have an indicator if a tab will be there or not.
   8660    let set = priority;
   8661    let index = set.indexOf(tab);
   8662 
   8663    if (index == -1) {
   8664      set = tab.hidden ? hidden : visible;
   8665      index = set.indexOf(tab);
   8666    }
   8667 
   8668    if (index > -1) {
   8669      set.splice(index, 1);
   8670    }
   8671  },
   8672 
   8673  // Returns and removes the tab with the highest priority.
   8674  shift() {
   8675    let set;
   8676    let { priority, hidden, visible } = this.tabs;
   8677 
   8678    let { restoreOnDemand, restorePinnedTabsOnDemand } = this.prefs;
   8679    let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
   8680    if (restorePinned && priority.length) {
   8681      set = priority;
   8682    } else if (!restoreOnDemand) {
   8683      if (visible.length) {
   8684        set = visible;
   8685      } else if (this.prefs.restoreHiddenTabs && hidden.length) {
   8686        set = hidden;
   8687      }
   8688    }
   8689 
   8690    return set && set.shift();
   8691  },
   8692 
   8693  // Moves a given tab from the 'hidden' to the 'visible' bucket.
   8694  hiddenToVisible(tab) {
   8695    let { hidden, visible } = this.tabs;
   8696    let index = hidden.indexOf(tab);
   8697 
   8698    if (index > -1) {
   8699      hidden.splice(index, 1);
   8700      visible.push(tab);
   8701    }
   8702  },
   8703 
   8704  // Moves a given tab from the 'visible' to the 'hidden' bucket.
   8705  visibleToHidden(tab) {
   8706    let { visible, hidden } = this.tabs;
   8707    let index = visible.indexOf(tab);
   8708 
   8709    if (index > -1) {
   8710      visible.splice(index, 1);
   8711      hidden.push(tab);
   8712    }
   8713  },
   8714 
   8715  /**
   8716   * Returns true if the passed tab is in one of the sets that we're
   8717   * restoring content in automatically.
   8718   *
   8719   * @param tab (<xul:tab>)
   8720   *        The tab to check
   8721   * @returns bool
   8722   */
   8723  willRestoreSoon(tab) {
   8724    let { priority, hidden, visible } = this.tabs;
   8725    let { restoreOnDemand, restorePinnedTabsOnDemand, restoreHiddenTabs } =
   8726      this.prefs;
   8727    let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
   8728    let candidateSet = [];
   8729 
   8730    if (restorePinned && priority.length) {
   8731      candidateSet.push(...priority);
   8732    }
   8733 
   8734    if (!restoreOnDemand) {
   8735      if (visible.length) {
   8736        candidateSet.push(...visible);
   8737      }
   8738 
   8739      if (restoreHiddenTabs && hidden.length) {
   8740        candidateSet.push(...hidden);
   8741      }
   8742    }
   8743 
   8744    return candidateSet.indexOf(tab) > -1;
   8745  },
   8746 };
   8747 
   8748 // A map storing a closed window's state data until it goes aways (is GC'ed).
   8749 // This ensures that API clients can still read (but not write) states of
   8750 // windows they still hold a reference to but we don't.
   8751 var DyingWindowCache = {
   8752  _data: new WeakMap(),
   8753 
   8754  has(window) {
   8755    return this._data.has(window);
   8756  },
   8757 
   8758  get(window) {
   8759    return this._data.get(window);
   8760  },
   8761 
   8762  set(window, data) {
   8763    this._data.set(window, data);
   8764  },
   8765 
   8766  remove(window) {
   8767    this._data.delete(window);
   8768  },
   8769 };
   8770 
   8771 // A weak set of dirty windows. We use it to determine which windows we need to
   8772 // recollect data for when getCurrentState() is called.
   8773 var DirtyWindows = {
   8774  _data: new WeakMap(),
   8775 
   8776  has(window) {
   8777    return this._data.has(window);
   8778  },
   8779 
   8780  add(window) {
   8781    return this._data.set(window, true);
   8782  },
   8783 
   8784  remove(window) {
   8785    this._data.delete(window);
   8786  },
   8787 
   8788  clear(_window) {
   8789    this._data = new WeakMap();
   8790  },
   8791 };
   8792 
   8793 // The state from the previous session (after restoring pinned tabs). This
   8794 // state is persisted and passed through to the next session during an app
   8795 // restart to make the third party add-on warning not trash the deferred
   8796 // session
   8797 var LastSession = {
   8798  _state: null,
   8799 
   8800  get canRestore() {
   8801    return !!this._state;
   8802  },
   8803 
   8804  getState() {
   8805    return this._state;
   8806  },
   8807 
   8808  setState(state) {
   8809    this._state = state;
   8810  },
   8811 
   8812  clear(silent = false) {
   8813    if (this._state) {
   8814      this._state = null;
   8815      if (!silent) {
   8816        Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED);
   8817      }
   8818    }
   8819  },
   8820 };
   8821 
   8822 /**
   8823 * @template T
   8824 * @param {T[]} array
   8825 * @param {function(T):boolean} predicate
   8826 */
   8827 function removeWhere(array, predicate) {
   8828  for (let i = array.length - 1; i >= 0; i--) {
   8829    if (predicate(array[i])) {
   8830      array.splice(i, 1);
   8831    }
   8832  }
   8833 }
   8834 
   8835 // Exposed for tests
   8836 export const _LastSession = LastSession;