fix(desktop-client): prevent duplicate window creation and serialize mode switches
Frontend CI / Frontend (push) Successful in 2m24s

- Add creation promise guards (mainWindowCreatePromise etc.) so concurrent
  ensureXxxWindow() calls share a single in-flight BrowserWindow, avoiding
  duplicate windows on rapid IPC
- Add windowModeSwitchQueue to serialize desktop:set-window-mode calls and
  the initial startup switch, preventing races when login/logout fire quickly
- Fix retireInactiveAppWindows: also retire existing login windows when
  switching to login mode, so stale login windows don't accumulate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Xu Liang
2026-05-01 18:05:12 +08:00
parent 5cd5d6685b
commit ba6ebec892
+52 -12
View File
@@ -97,6 +97,10 @@ let loginWindow: ElectronBrowserWindow | null = null;
let settingsWindow: ElectronBrowserWindow | null = null;
let mainRendererContents: ElectronWebContents | null = null;
let quitReleaseInFlight = false;
let mainWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null;
let loginWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null;
let settingsWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null;
let windowModeSwitchQueue: Promise<void> = Promise.resolve();
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>();
const appWindowModes = new WeakMap<ElectronBrowserWindow, RendererWindowMode>();
@@ -281,7 +285,7 @@ function retireInactiveAppWindows(mode: WindowMode, target: ElectronBrowserWindo
const candidateMode = rendererModeFromWindow(candidate);
if (
(mode === "main" && (candidateMode === "login" || candidateMode === "main"))
|| (mode === "login" && (candidateMode === "main" || candidateMode === "settings"))
|| (mode === "login" && (candidateMode === "login" || candidateMode === "main" || candidateMode === "settings"))
) {
retireWindowAfterIpcReturn(candidate);
}
@@ -333,9 +337,17 @@ async function ensureMainWindow(): Promise<ElectronBrowserWindow> {
if (existing) {
return existing;
}
const window = await createAppWindow("main");
mainWindow = window;
return window;
if (!mainWindowCreatePromise) {
mainWindowCreatePromise = createAppWindow("main")
.then((window) => {
mainWindow = window;
return window;
})
.finally(() => {
mainWindowCreatePromise = null;
});
}
return mainWindowCreatePromise;
}
async function ensureLoginWindow(): Promise<ElectronBrowserWindow> {
@@ -343,9 +355,17 @@ async function ensureLoginWindow(): Promise<ElectronBrowserWindow> {
if (existing) {
return existing;
}
const window = await createAppWindow("login");
loginWindow = window;
return window;
if (!loginWindowCreatePromise) {
loginWindowCreatePromise = createAppWindow("login")
.then((window) => {
loginWindow = window;
return window;
})
.finally(() => {
loginWindowCreatePromise = null;
});
}
return loginWindowCreatePromise;
}
async function ensureSettingsWindow(): Promise<ElectronBrowserWindow> {
@@ -353,9 +373,17 @@ async function ensureSettingsWindow(): Promise<ElectronBrowserWindow> {
if (existing) {
return existing;
}
const window = await createSettingsWindow();
settingsWindow = window;
return window;
if (!settingsWindowCreatePromise) {
settingsWindowCreatePromise = createSettingsWindow()
.then((window) => {
settingsWindow = window;
return window;
})
.finally(() => {
settingsWindowCreatePromise = null;
});
}
return settingsWindowCreatePromise;
}
async function ensureWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
@@ -424,6 +452,18 @@ async function switchWindowMode(
retireInactiveAppWindows(mode, target);
}
function queueWindowModeSwitch(
mode: WindowMode,
request: WindowModeRequest = {},
sourceWindow: ElectronBrowserWindow | null = null,
): Promise<void> {
const nextSwitch = windowModeSwitchQueue
.catch(() => undefined)
.then(() => switchWindowMode(mode, request, sourceWindow));
windowModeSwitchQueue = nextSwitch.catch(() => undefined);
return nextSwitch;
}
async function openSettingsWindow(): Promise<void> {
const window = await ensureSettingsWindow();
await revealWindow(window);
@@ -722,7 +762,7 @@ function registerBridgeHandlers(): void {
});
ipcMain.handle("desktop:set-window-mode", async (event, mode: WindowMode, request?: unknown) => {
if (mode === "login" || mode === "main") {
await switchWindowMode(mode, normalizeWindowModeRequest(request), windowFromIpcEvent(event));
await queueWindowModeSwitch(mode, normalizeWindowModeRequest(request), windowFromIpcEvent(event));
}
return null;
});
@@ -755,7 +795,7 @@ if (!hasSingleInstanceLock) {
}
mainRendererContents.send("desktop:runtime-invalidated", event);
});
await switchWindowMode(currentWindowMode);
await queueWindowModeSwitch(currentWindowMode);
initTray(() => {
revealActiveWindowSafely("tray");
});