diff options
Diffstat (limited to 'comm/mail/modules/SessionStoreManager.jsm')
-rw-r--r-- | comm/mail/modules/SessionStoreManager.jsm | 302 |
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 + ); + } + }, +}; |