From 5ce257de83234daebe5cde39f02748bc39f48de1 Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Wed, 10 Dec 2025 18:09:07 +0100 Subject: [PATCH] feat: Allow unload to run instantly and fix closing windows on mac, b=no-bug, c=no-component --- prefs/zen/session-store.yaml | 9 ++ prefs/zen/window-sync.yaml | 3 + .../sessionstore/SessionComponents.manifest | 1 - src/zen/sessionstore/ZenSessionFile.sys.mjs | 40 ------ .../sessionstore/ZenSessionManager.sys.mjs | 134 +++++++++++------- src/zen/sessionstore/ZenWindowSync.sys.mjs | 27 +++- src/zen/sessionstore/moz.build | 1 - 7 files changed, 117 insertions(+), 98 deletions(-) create mode 100644 prefs/zen/session-store.yaml delete mode 100644 src/zen/sessionstore/ZenSessionFile.sys.mjs diff --git a/prefs/zen/session-store.yaml b/prefs/zen/session-store.yaml new file mode 100644 index 000000000..ede2bf75f --- /dev/null +++ b/prefs/zen/session-store.yaml @@ -0,0 +1,9 @@ +# 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/. + +- name: zen.session-store.backup-file + value: true + +- name: zen.session-store.log + value: true diff --git a/prefs/zen/window-sync.yaml b/prefs/zen/window-sync.yaml index f7ba5b071..010d357f2 100644 --- a/prefs/zen/window-sync.yaml +++ b/prefs/zen/window-sync.yaml @@ -4,3 +4,6 @@ - name: zen.window-sync.enabled value: true + +- name: zen.window-sync.log + value: true diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest index 9da9dc105..8e3b0f9b0 100644 --- a/src/zen/sessionstore/SessionComponents.manifest +++ b/src/zen/sessionstore/SessionComponents.manifest @@ -7,5 +7,4 @@ category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init # App shutdown consumers -category browser-quit-application-granted resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.uninit category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit diff --git a/src/zen/sessionstore/ZenSessionFile.sys.mjs b/src/zen/sessionstore/ZenSessionFile.sys.mjs deleted file mode 100644 index 526f7ab04..000000000 --- a/src/zen/sessionstore/ZenSessionFile.sys.mjs +++ /dev/null @@ -1,40 +0,0 @@ -// 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/. - -// Note that changing this hidden pref will make the previous session file -// unused, causing a new session file to be created on next write. -const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true); - -const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json'; - -export class nsZenSessionFile { - #path = PathUtils.join(PathUtils.profileDir, FILE_NAME); - #sidebar = []; - - async read() { - try { - const data = await IOUtils.readJSON(this.#path, { compress: SHOULD_COMPRESS_FILE }); - this.#sidebar = data.sidebar || []; - } catch { - // File doesn't exist yet, that's fine. - } - } - - get sidebar() { - return this.#sidebar; - } - - set sidebar(data) { - this.#sidebar = data; - } - - async #write(data) { - await IOUtils.writeJSON(this.#path, data, { compress: SHOULD_COMPRESS_FILE }); - } - - async store() { - const data = { sidebar: this.#sidebar }; - await this.#write(data); - } -} diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 8ebb5c49d..2f14f5350 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -1,11 +1,13 @@ -// 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/. +/* 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/. */ + +import { JSONFile } from 'resource://gre/modules/JSONFile.sys.mjs'; +import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - nsZenSessionFile: 'resource:///modules/zen/ZenSessionFile.sys.mjs', PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs', BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs', @@ -14,31 +16,67 @@ ChromeUtils.defineESModuleGetters(lazy, { setTimeout: 'resource://gre/modules/Timer.sys.mjs', }); +XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.session-store.log', true); + +// Note that changing this hidden pref will make the previous session file +// unused, causing a new session file to be created on next write. +const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true); +const SHOULD_BACKUP_FILE = Services.prefs.getBoolPref('zen.session-store.backup-file', true); + +const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json'; const MIGRATION_PREF = 'zen.ui.migration.session-manager-restore'; -const OBSERVING = ['browser-window-before-show']; -class nsZenSessionManager { - #file; +/** + * Class representing the sidebar object stored in the session file. + * This object holds all the data related to tabs, groups, folders + * and split view state. + */ +class nsZenSidebarObject { + #sidebar = {}; - constructor() { - this.#file = new lazy.nsZenSessionFile(); + get data() { + return { ...this.#sidebar }; } + set data(data) { + console.log(data); + this.#sidebar = data; + } +} + +export class nsZenSessionManager { + #file; + #sidebarObject = new nsZenSidebarObject(); + // Called from SessionComponents.manifest on app-startup init() { - for (let topic of OBSERVING) { - Services.obs.addObserver(this, topic); + let profileDir = Services.dirsvc.get('ProfD', Ci.nsIFile).path; + let backupFile = null; + if (SHOULD_BACKUP_FILE) { + backupFile = PathUtils.join(profileDir, 'zen-sessions-backup', FILE_NAME); } + let filePath = PathUtils.join(profileDir, FILE_NAME); + this.#file = new JSONFile({ + path: filePath, + compression: SHOULD_COMPRESS_FILE ? 'lz4' : undefined, + backupFile, + }); } - uninit() { - for (let topic of OBSERVING) { - Services.obs.removeObserver(this, topic); + log(...args) { + if (lazy.gShouldLog) { + console.info('ZenSessionManager:', ...args); } } async readFile() { - await this.#file.read(); + console.log(await this.#file.load()); + try { + this.#sidebar = (await this.#file.load()) || {}; + } catch (e) { + console.error('ZenSessionManager: Failed to read session file', e); + this.#sidebar = {}; + } } onFileRead(initialState) { @@ -53,33 +91,18 @@ class nsZenSessionManager { // Restore all windows with the same sidebar object, this will // guarantee that all tabs, groups, folders and split view data // are properly synced across all windows. + this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`); for (const winData of initialState.windows || []) { this.restoreWindowData(winData); } } get #sidebar() { - return this.#file.sidebar; + return { ...this.#sidebarObject.data }; } set #sidebar(data) { - this.#file.sidebar = data; - } - - observe(aSubject, aTopic) { - switch (aTopic) { - case 'browser-window-before-show': // catch new windows - this.#onBeforeBrowserWindowShown(aSubject); - break; - default: - break; - } - } - - /** Handles the browser-window-before-show observer notification. */ - #onBeforeBrowserWindowShown(aWindow) { - // TODO: Initialize new window - void aWindow; + this.#sidebarObject.data = data; } /** @@ -95,7 +118,10 @@ class nsZenSessionManager { return; } this.#collectWindowData(state); - this.#file.store(); + // This would save the data to disk asynchronously. + this.#file.data = this.#sidebar; + this.#file.saveSoon(); + this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`); } /** @@ -153,7 +179,8 @@ class nsZenSessionManager { } restoreWindowData(aWindowData) { - const sidebar = this.#file.sidebar; + const sidebar = this.#sidebar; + console.log(sidebar); if (!sidebar) { return; } @@ -167,25 +194,30 @@ class nsZenSessionManager { if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) { return; } + this.log('Restoring new window with Zen session data'); aWindow._zenPromiseNewWindowRestored = new Promise((resolve) => { - lazy.SessionSaver.run().then(() => { - lazy.setTimeout(() => { - const state = lazy.SessionStore.getCurrentState(true); - const windows = state.windows || []; - let windowToClone = - windows.find( - (win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced - ) || {}; - let newWindow = Cu.cloneInto(windowToClone, {}); + lazy.setTimeout(() => { + const state = lazy.SessionStore.getCurrentState(true); + const windows = (state.windows || []).find( + (win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced + ); + let windowToClone = windows[0]; + let newWindow = Cu.cloneInto(windowToClone, {}); + if (windows.length < 2) { + // We only want to restore the sidebar object if we found + // only one normal window to clone from (which is the one + // we are opening). + this.log('Restoring sidebar data into new window'); this.restoreWindowData(newWindow); - newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []); - delete newWindow.selected; - const newState = { windows: [newWindow] }; - SessionStoreInternal.restoreWindows(aWindow, newState, { - firstWindow: true, - }); - resolve(); + } + newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []); + delete newWindow.selected; + const newState = { windows: [newWindow] }; + this.log(`Cloning window with ${newWindow.tabs.length} tabs`); + SessionStoreInternal.restoreWindows(aWindow, newState, { + firstWindow: true, }); + resolve(); }); }); } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index c02656265..89248877d 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -1,6 +1,6 @@ -// 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/. +/* 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/. */ import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; @@ -14,8 +14,10 @@ ChromeUtils.defineESModuleGetters(lazy, { }); XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gWindowSyncEnabled', 'zen.window-sync.enabled'); +XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.window-sync.log', true); const OBSERVING = ['browser-window-before-show']; +const INSTANT_EVENTS = ['unload']; const EVENTS = [ 'TabOpen', 'TabClose', @@ -37,7 +39,7 @@ const EVENTS = [ 'TabSelect', 'focus', - 'unload', + ...INSTANT_EVENTS, ]; // Flags acting as an enum for sync types. @@ -115,6 +117,12 @@ class nsZenWindowSync { } } + log(...args) { + if (lazy.gShouldLog) { + console.info('ZenWindowSync:', ...args); + } + } + /** * Called when a browser window is about to be shown. * Adds event listeners for the specified events. @@ -143,6 +151,7 @@ class nsZenWindowSync { aWindow.arguments.length > 1 && [...this.#browserWindows].length > 0)) ) { + this.log('Not syncing new window due to unsynced argument or existing synced windows'); aWindow.document.documentElement.setAttribute('zen-unsynced-window', 'true'); return; } @@ -219,6 +228,9 @@ class nsZenWindowSync { if (!window.gZenStartup.isReady || window.gZenWorkspaces?.privateWindowOrDisabled) { return; } + if (INSTANT_EVENTS.includes(aEvent.type)) { + return this.#handleNextEvent(aEvent); + } if (this.#eventHandlingContext.window && this.#eventHandlingContext.window !== window) { // We're already handling an event for another window. // To avoid re-entrancy issues, we skip this event. @@ -813,7 +825,12 @@ class nsZenWindowSync { on_focus(aEvent) { const { ownerGlobal: window } = aEvent.target; - if (!window.gBrowser || this.#lastFocusedWindow?.deref() === window) { + if ( + !window.gBrowser || + this.#lastFocusedWindow?.deref() === window || + window.closing || + !window.toolbar.visible + ) { return; } this.#lastFocusedWindow = new WeakRef(window); diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build index 902ae2a30..188f4c27c 100644 --- a/src/zen/sessionstore/moz.build +++ b/src/zen/sessionstore/moz.build @@ -3,7 +3,6 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_JS_MODULES.zen += [ - "ZenSessionFile.sys.mjs", "ZenSessionManager.sys.mjs", "ZenWindowSync.sys.mjs", ]