summaryrefslogtreecommitdiffstats
path: root/comm/mail/modules/SessionStoreManager.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/modules/SessionStoreManager.jsm')
-rw-r--r--comm/mail/modules/SessionStoreManager.jsm302
1 files changed, 302 insertions, 0 deletions
diff --git a/comm/mail/modules/SessionStoreManager.jsm b/comm/mail/modules/SessionStoreManager.jsm
new file mode 100644
index 0000000000..1c8ea6bec6
--- /dev/null
+++ b/comm/mail/modules/SessionStoreManager.jsm
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["SessionStoreManager"];
+
+const { JSONFile } = ChromeUtils.importESModule(
+ "resource://gre/modules/JSONFile.sys.mjs"
+);
+
+/**
+ * asuth arbitrarily chose this value to trade-off powersaving,
+ * processor usage, and recency of state in the face of the impossibility of
+ * our crashing; he also worded this.
+ */
+var SESSION_AUTO_SAVE_DEFAULT_MS = 300000; // 5 minutes
+
+var SessionStoreManager = {
+ _initialized: false,
+
+ /**
+ * Session restored successfully on startup; use this to test for an early
+ * failed startup which does not restore user tab state to ensure a session
+ * save on close will not overwrite the last good session state.
+ */
+ _restored: false,
+
+ _sessionAutoSaveTimer: null,
+
+ _sessionAutoSaveTimerIntervalMS: SESSION_AUTO_SAVE_DEFAULT_MS,
+
+ /**
+ * The persisted state of the previous session. This is resurrected
+ * from disk when the module is initialized and cleared when all
+ * required windows have been restored.
+ */
+ _initialState: null,
+
+ /**
+ * A flag indicating whether the state "just before shutdown" of the current
+ * session has been persisted to disk. See |observe| and |unloadingWindow|
+ * for justification on why we need this.
+ */
+ _shutdownStateSaved: false,
+
+ /**
+ * The JSONFile store object.
+ */
+ get store() {
+ if (this._store) {
+ return this._store;
+ }
+
+ return (this._store = new JSONFile({
+ path: this.sessionFile.path,
+ backupTo: this.sessionFile.path + ".backup",
+ }));
+ },
+
+ /**
+ * Gets the nsIFile used for session storage.
+ */
+ get sessionFile() {
+ let sessionFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ sessionFile.append("session.json");
+ return sessionFile;
+ },
+
+ /**
+ * This is called on startup, and when a new 3 pane window is opened after
+ * the last 3 pane window was closed (e.g., on the mac, closing the last
+ * window doesn't shut down the app).
+ */
+ async _init() {
+ await this._loadSessionFile();
+
+ // we listen for "quit-application-granted" instead of
+ // "quit-application-requested" because other observers of the
+ // latter can cancel the shutdown.
+ Services.obs.addObserver(this, "quit-application-granted");
+
+ this.startPeriodicSave();
+
+ this._initialized = true;
+ },
+
+ /**
+ * Loads the session file into _initialState. This should only be called by
+ * _init and a unit test.
+ */
+ async _loadSessionFile() {
+ if (!this.sessionFile.exists()) {
+ return;
+ }
+
+ // Read the session state data from file, asynchronously.
+ // An error on the json file returns an empty object which corresponds
+ // to a null |_initialState|.
+ await this.store.load();
+ this._initialState =
+ this.store.data.toSource() == {}.toSource() ? null : this.store.data;
+ },
+
+ /**
+ * Opens the windows that were open in the previous session.
+ */
+ _openOtherRequiredWindows(aWindow) {
+ // XXX we might want to display a restore page and let the user decide
+ // whether to restore the other windows, just like Firefox does.
+
+ if (!this._initialState || !this._initialState.windows || !aWindow) {
+ return;
+ }
+
+ for (var i = 0; i < this._initialState.windows.length; ++i) {
+ aWindow.open(
+ "chrome://messenger/content/messenger.xhtml",
+ "_blank",
+ "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"
+ );
+ }
+ },
+
+ /**
+ * Writes the state object to disk.
+ */
+ _saveStateObject(aStateObj) {
+ if (!this.store) {
+ console.error(
+ "SessionStoreManager: could not create data store from file"
+ );
+ return;
+ }
+
+ let currentStateString = JSON.stringify(aStateObj);
+ let storedStateString =
+ this.store.dataReady && this.store.data
+ ? JSON.stringify(this.store.data)
+ : null;
+
+ // Do not save state (overwrite last good state) in case of a failed startup.
+ // Write async to disk only if state changed since last write.
+ if (!this._restored || currentStateString == storedStateString) {
+ return;
+ }
+
+ this.store.data = aStateObj;
+ this.store.saveSoon();
+ },
+
+ /**
+ * @returns an empty state object that can be populated with window states.
+ */
+ _createStateObject() {
+ return {
+ rev: 0,
+ windows: [],
+ };
+ },
+
+ /**
+ * Writes the state of all currently open 3pane windows to disk.
+ */
+ _saveState() {
+ let state = this._createStateObject();
+
+ // XXX we'd like to support other window types in future, but for now
+ // only get the 3pane windows.
+ for (let win of Services.wm.getEnumerator("mail:3pane")) {
+ if (
+ win &&
+ "complete" == win.document.readyState &&
+ win.getWindowStateForSessionPersistence
+ ) {
+ state.windows.push(win.getWindowStateForSessionPersistence());
+ }
+ }
+
+ this._saveStateObject(state);
+ },
+
+ // Timer Callback
+ _sessionAutoSaveTimerCallback() {
+ SessionStoreManager._saveState();
+ },
+
+ // Observer Notification Handler
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ // This is observed before any windows start unloading if something other
+ // than the last 3pane window closing requested the application be
+ // shutdown. For example, when the user quits via the file menu.
+ case "quit-application-granted":
+ if (!this._shutdownStateSaved) {
+ this.stopPeriodicSave();
+ this._saveState();
+
+ // this is to ensure we don't clobber the saved state when the
+ // 3pane windows unload.
+ this._shutdownStateSaved = true;
+ }
+ break;
+ }
+ },
+
+ // Public API
+
+ /**
+ * Called by each 3pane window instance when it loads.
+ *
+ * @returns a window state object if aWindow was opened as a result of a
+ * session restoration, null otherwise.
+ */
+ async loadingWindow(aWindow) {
+ let firstWindow = !this._initialized || this._shutdownStateSaved;
+ if (firstWindow) {
+ await this._init();
+ }
+
+ // If we are seeing a new 3-pane, we are obviously not in a shutdown
+ // state anymore. (This would happen if all the 3panes got closed but
+ // we did not quit because another window was open and then a 3pane showed
+ // up again. This can happen in both unit tests and real life.)
+ // We treat this case like the first window case, and do a session restore.
+ this._shutdownStateSaved = false;
+
+ let windowState = null;
+ if (this._initialState && this._initialState.windows) {
+ windowState = this._initialState.windows.pop();
+ if (0 == this._initialState.windows.length) {
+ this._initialState = null;
+ }
+ }
+
+ if (firstWindow) {
+ this._openOtherRequiredWindows(aWindow);
+ }
+
+ return windowState;
+ },
+
+ /**
+ * Called by each 3pane window instance when it unloads. If aWindow is the
+ * last 3pane window, its state is persisted. The last 3pane window unloads
+ * first before the "quit-application-granted" event is generated.
+ */
+ unloadingWindow(aWindow) {
+ if (!this._shutdownStateSaved) {
+ // determine whether aWindow is the last open window
+ let lastWindow = true;
+ for (let win of Services.wm.getEnumerator("mail:3pane")) {
+ if (win != aWindow) {
+ lastWindow = false;
+ }
+ }
+
+ if (lastWindow) {
+ // last chance to save any state for the current session since
+ // aWindow is the last 3pane window and the "quit-application-granted"
+ // event is observed AFTER this.
+ this.stopPeriodicSave();
+
+ let state = this._createStateObject();
+ state.windows.push(aWindow.getWindowStateForSessionPersistence());
+ this._saveStateObject(state);
+
+ // XXX this is to ensure we don't clobber the saved state when we
+ // observe the "quit-application-granted" event.
+ this._shutdownStateSaved = true;
+ }
+ }
+ },
+
+ /**
+ * Stops periodic session persistence.
+ */
+ stopPeriodicSave() {
+ if (this._sessionAutoSaveTimer) {
+ this._sessionAutoSaveTimer.cancel();
+
+ delete this._sessionAutoSaveTimer;
+ this._sessionAutoSaveTimer = null;
+ }
+ },
+
+ /**
+ * Starts periodic session persistence.
+ */
+ startPeriodicSave() {
+ if (!this._sessionAutoSaveTimer) {
+ this._sessionAutoSaveTimer = Cc["@mozilla.org/timer;1"].createInstance(
+ Ci.nsITimer
+ );
+
+ this._sessionAutoSaveTimer.initWithCallback(
+ this._sessionAutoSaveTimerCallback,
+ this._sessionAutoSaveTimerIntervalMS,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ }
+ },
+};