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