fix: Make sure to save the state instantly on some occations, b=bug #12152, c=scripts

This commit is contained in:
mr. m
2026-02-11 11:59:05 +01:00
parent bffc16ba29
commit af4f336bf5
3 changed files with 69 additions and 39 deletions

View File

@@ -11,6 +11,7 @@ npm run import
IGNORE_FILES=(
"shared.nsh"
"ignorePrefs.json"
"toolkit/moz.configure"
)
# Recursively find all .patch files in the current directory and its subdirectories

View File

@@ -262,6 +262,18 @@ export class nsZenSessionManager {
}
return initialState;
}
const allowRestoreUnsynced = Services.prefs.getBoolPref(
"zen.session-store.restore-unsynced-windows",
true
);
if (initialState?.windows?.length && !allowRestoreUnsynced) {
initialState.windows = initialState.windows.filter((win) => {
if (win.isZenUnsynced) {
this.log("Skipping unsynced window during restore");
}
return !win.isZenUnsynced;
});
}
// If there are no windows, we create an empty one. By default,
// firefox would create simply a new empty window, but we want
// to make sure that the sidebar object is properly initialized.
@@ -300,27 +312,18 @@ export 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.
const allowRestoreUnsynced = Services.prefs.getBoolPref(
"zen.session-store.restore-unsynced-windows",
true
);
if (!this._shouldRunMigration) {
this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`);
for (let i = 0; i < initialState.windows.length; i++) {
let winData = initialState.windows[i];
if (winData.isZenUnsynced) {
if (!allowRestoreUnsynced) {
// We don't wan't to restore any unsynced windows with the sidebar data.
this.log("Skipping restore of unsynced window");
delete initialState.windows[i];
}
continue;
}
this.#restoreWindowData(winData);
}
} else if (initialState) {
this.log("Saving windata state after migration");
this.saveState(Cu.cloneInto(initialState, {}));
this.saveState(Cu.cloneInto(initialState, {}), true);
}
delete this._shouldRunMigration;
}
@@ -438,8 +441,11 @@ export class nsZenSessionManager {
* Saves the current session state. Collects data and writes to disk.
*
* @param {object} state The current session state.
* @param {boolean} soon Whether to save the file soon or immediately.
* If true, the file will be saved asynchronously or when quitting
* the app. If false, the file will be saved immediately.
*/
saveState(state) {
saveState(state, soon = false) {
let windows = state?.windows || [];
windows = windows.filter((win) => this.#isWindowSaveable(win));
if (!windows.length) {
@@ -448,11 +454,14 @@ export class nsZenSessionManager {
return;
}
this.#collectWindowData(windows);
// This would save the data to disk asynchronously or when
// quitting the app.
// This would save the data to disk asynchronously or when quitting the app.
let sidebar = this.#sidebar;
this.#file.data = sidebar;
this.#file.saveSoon();
if (soon) {
this.#file.saveSoon();
} else {
this.#file._save();
}
this.#debounceRegeneration();
this.log(`Saving Zen session data with ${sidebar.tabs?.length || 0} tabs`);
}
@@ -533,7 +542,7 @@ export class nsZenSessionManager {
return;
}
this.log("Saving closed window session data into Zen session store");
this.saveState({ windows: [aWinData] });
this.saveState({ windows: [aWinData] }, true);
}
/**

View File

@@ -28,7 +28,7 @@ XPCOMUtils.defineLazyPreferenceGetter(
XPCOMUtils.defineLazyPreferenceGetter(lazy, "gShouldLog", "zen.window-sync.log", true);
const OBSERVING = ["browser-window-before-show", "sessionstore-windows-restored"];
const INSTANT_EVENTS = ["SSWindowClosing"];
const INSTANT_EVENTS = ["SSWindowClosing", "TabSelect", "focus"];
const UNSYNCED_WINDOW_EVENTS = ["TabOpen"];
const EVENTS = [
"TabClose",
@@ -50,9 +50,6 @@ const EVENTS = [
"ZenTabRemovedFromSplit",
"ZenSplitViewTabsSplit",
"TabSelect",
"focus",
...INSTANT_EVENTS,
...UNSYNCED_WINDOW_EVENTS,
];
@@ -81,6 +78,13 @@ class nsZenWindowSync {
lastHandlerPromise: Promise.resolve(),
};
/**
* Promise that resolves when the current docshell swap operation is finished.
* Used to avoid multiple simultaneous swap operations that could interfere with each other.
* For example, when focusing a window AND selecting a tab at the same time.
*/
#docShellSwitchPromise = Promise.resolve();
/**
* Map of sync handlers for different event types.
* Each handler is a function that takes the event as an argument.
@@ -814,15 +818,17 @@ class nsZenWindowSync {
};
});
await promiseToWait;
this.#createPseudoImageForBrowser(otherBrowser, mySrc);
this.#maybeRemovePseudoImageForBrowser(ourBrowser);
ourBrowser.removeAttribute("zen-pseudo-hidden");
otherBrowser.setAttribute("zen-pseudo-hidden", "true");
let promise = this.#createPseudoImageForBrowser(otherBrowser, mySrc);
await Promise.all([promiseToWait, promise]);
aOurTab.ownerGlobal.requestAnimationFrame(() => {
otherBrowser.setAttribute("zen-pseudo-hidden", "true");
ourBrowser.removeAttribute("zen-pseudo-hidden");
this.#maybeRemovePseudoImageForBrowser(ourBrowser);
});
callback();
} else {
this.#maybeRemovePseudoImageForBrowser(ourBrowser);
ourBrowser.removeAttribute("zen-pseudo-hidden");
this.#maybeRemovePseudoImageForBrowser(ourBrowser);
}
resolve();
@@ -837,10 +843,25 @@ class nsZenWindowSync {
*/
#createPseudoImageForBrowser(aBrowser, aSrc) {
const doc = aBrowser.ownerDocument;
const win = aBrowser.ownerGlobal;
const img = doc.createElement("img");
img.className = "zen-pseudo-browser-image";
img.src = aSrc;
let promise = new Promise((resolve) => {
if (img.complete) {
resolve();
return;
}
let finish = () => {
win.requestAnimationFrame(() => {
resolve();
});
};
img.onload = finish;
img.onerror = finish;
});
aBrowser.after(img);
return promise;
}
/**
@@ -914,14 +935,8 @@ class nsZenWindowSync {
*
* @param {Window} aWindow - The window that triggered the event.
* @param {object} aPreviousTab - The previously selected tab.
* @param {boolean} ignoreSameTab - Indicates if the same tab should be ignored.
*/
async #onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null, ignoreSameTab = false) {
// On some occasions, such as when closing a window, this
// function might be called multiple times for the same tab.
if (aWindow.gBrowser.selectedTab === this.#lastSelectedTab && !ignoreSameTab) {
return;
}
async #onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) {
let activeBrowsers = aWindow.gBrowser.selectedBrowsers;
let activeTabs = activeBrowsers.map((browser) => aWindow.gBrowser.getTabForBrowser(browser));
// Ignore previous tabs that are still "active". These scenarios could happen for example,
@@ -1214,11 +1229,12 @@ class nsZenWindowSync {
});
}
on_focus(aEvent) {
async on_focus(aEvent) {
if (typeof aEvent.target !== "object") {
return;
}
const { ownerGlobal: window } = aEvent.target;
await this.#docShellSwitchPromise;
const window = Services.focus.activeWindow;
if (
!window?.gBrowser ||
this.#lastFocusedWindow?.deref() === window ||
@@ -1229,17 +1245,21 @@ class nsZenWindowSync {
}
this.#lastFocusedWindow = new WeakRef(window);
this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab);
return this.#onTabSwitchOrWindowFocus(window);
return (this.#docShellSwitchPromise = this.#onTabSwitchOrWindowFocus(window));
}
on_TabSelect(aEvent) {
async on_TabSelect(aEvent) {
await this.#docShellSwitchPromise;
const tab = aEvent.target;
if (this.#lastSelectedTab?.deref() === tab) {
return;
}
this.#lastSelectedTab = new WeakRef(tab);
const previousTab = aEvent.detail.previousTab;
return this.#onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab);
return (this.#docShellSwitchPromise = this.#onTabSwitchOrWindowFocus(
aEvent.target.ownerGlobal,
previousTab
));
}
on_SSWindowClosing(aEvent) {
@@ -1270,7 +1290,7 @@ class nsZenWindowSync {
// If the page has a title, set it. When doing a swap and we still didn't
// flush the tab state, the title might not be correct.
if (activePageData) {
if (activePageData && win.gBrowser) {
win.gBrowser.setInitialTabTitle(tab, activePageData.title, {
isContentTitle: activePageData.title && activePageData.title != activePageData.url,
});
@@ -1375,7 +1395,7 @@ class nsZenWindowSync {
return new Promise((resolve) => {
lazy.setTimeout(() => {
this.#onTabSwitchOrWindowFocus(window, null, /* ignoreSameTab = */ true).finally(resolve);
this.#onTabSwitchOrWindowFocus(window, null).finally(resolve);
}, 0);
});
}