From daf6bce72b4d1a737301222679bdd9fe17afacef Mon Sep 17 00:00:00 2001 From: guoyongchang Date: Thu, 30 Jul 2026 18:42:33 +0800 Subject: [PATCH] fix(desktop): preserve SQL file tabs on cold start --- apps/desktop/src/App.vue | 25 +++++- apps/desktop/src/lib/app/openTabsStartup.ts | 41 +++++++++ .../app-tests/externalSqlFileStartup.test.ts | 85 +++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/lib/app/openTabsStartup.ts create mode 100644 packages/app-tests/externalSqlFileStartup.test.ts diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 39eb08419..7bd1c2ccd 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -34,6 +34,7 @@ import { useVisibilityChange } from "@/composables/useVisibilityChange"; import { useWebDavAutoUpload } from "@/composables/useWebDavAutoUpload"; import { useScheduledDatabaseBackups } from "@/composables/useScheduledDatabaseBackups"; import { shouldDrawDesktopWindowFrame } from "@/composables/useWindowControls"; +import { createOpenTabsRestorationBarrier, initializeDesktopOpenTabs, type OpenTabsRestorationBarrier } from "@/lib/app/openTabsStartup"; import { useSaveSqlFolderSelection } from "@/composables/useSaveSqlFolderSelection"; import "@/i18n"; import { translateBackendError } from "@/i18n/backend-errors"; @@ -1126,9 +1127,15 @@ function pasteClipboardAsSqlInCondition() { void contentAreaRef.value?.pasteClipboardAsSqlInCondition?.(); } +// Cold-start file arguments can arrive while persisted tabs are still being +// restored. Keep external SQL tabs behind that phase only, so unrelated +// initialization cannot permanently block files opened from the OS. +let desktopOpenTabsRestorationBarrier: OpenTabsRestorationBarrier | null = null; + async function openSqlFilePath(path: string) { if (!isTauriRuntime()) return; try { + await desktopOpenTabsRestorationBarrier?.settled; const content = await api.readExternalSqlFile(path); const connectionId = connectionStore.activeConnectionId || activeTab.value?.connectionId || connectionStore.connections[0]?.id || ""; const connection = connectionId ? connectionStore.getConfig(connectionId) : undefined; @@ -1991,14 +1998,27 @@ function onLoginSuccess() { async function initApp() { const t0 = performance.now(); console.log("[STARTUP] initApp begin"); - await settingsStore.initAiConfigs(); - try { + const restoreOpenTabs = async () => { await settingsStore.initEditorSettings(); console.log(`[STARTUP] settingsStore.initEditorSettings: ${(performance.now() - t0).toFixed(0)}ms`); await connectionStore.initFromDisk(); console.log(`[STARTUP] connectionStore.initFromDisk: ${(performance.now() - t0).toFixed(0)}ms`); await queryStore.initOpenTabs({ validConnectionIds: connectionStore.connections.map((connection) => connection.id) }); console.log(`[STARTUP] queryStore.initOpenTabs: ${(performance.now() - t0).toFixed(0)}ms`); + }; + + if (!desktopOpenTabsRestorationBarrier) await settingsStore.initAiConfigs(); + try { + if (desktopOpenTabsRestorationBarrier) { + await initializeDesktopOpenTabs({ + barrier: desktopOpenTabsRestorationBarrier, + initializeOptionalState: () => settingsStore.initAiConfigs(), + restoreOpenTabs, + onOptionalStateError: (error) => console.error("[STARTUP] settingsStore.initAiConfigs failed", error), + }); + } else { + await restoreOpenTabs(); + } await settingsStore.initDesktopSettings().catch(() => {}); void promptTemplateStore.init(); @@ -2129,6 +2149,7 @@ onMounted(async () => { .catch(() => {}); return; } + desktopOpenTabsRestorationBarrier = createOpenTabsRestorationBarrier(); void initApp(); setupFileDrop().catch(() => {}); setTimeout(() => { diff --git a/apps/desktop/src/lib/app/openTabsStartup.ts b/apps/desktop/src/lib/app/openTabsStartup.ts new file mode 100644 index 000000000..4ecd524a4 --- /dev/null +++ b/apps/desktop/src/lib/app/openTabsStartup.ts @@ -0,0 +1,41 @@ +export interface OpenTabsRestorationBarrier { + readonly settled: Promise; + settle: () => void; +} + +export function createOpenTabsRestorationBarrier(): OpenTabsRestorationBarrier { + let resolveBarrier: () => void; + let isSettled = false; + const settled = new Promise((resolve) => { + resolveBarrier = resolve; + }); + + return { + settled, + settle: () => { + if (isSettled) return; + isSettled = true; + resolveBarrier(); + }, + }; +} + +interface InitializeDesktopOpenTabsOptions { + barrier: OpenTabsRestorationBarrier; + initializeOptionalState: () => Promise; + restoreOpenTabs: () => Promise; + onOptionalStateError: (error: unknown) => void; +} + +export async function initializeDesktopOpenTabs({ barrier, initializeOptionalState, restoreOpenTabs, onOptionalStateError }: InitializeDesktopOpenTabsOptions): Promise { + try { + try { + await initializeOptionalState(); + } catch (error) { + onOptionalStateError(error); + } + await restoreOpenTabs(); + } finally { + barrier.settle(); + } +} diff --git a/packages/app-tests/externalSqlFileStartup.test.ts b/packages/app-tests/externalSqlFileStartup.test.ts new file mode 100644 index 000000000..f5603dd92 --- /dev/null +++ b/packages/app-tests/externalSqlFileStartup.test.ts @@ -0,0 +1,85 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "vitest"; +import { createOpenTabsRestorationBarrier, initializeDesktopOpenTabs } from "../../apps/desktop/src/lib/app/openTabsStartup.ts"; + +const appSource = readFileSync("apps/desktop/src/App.vue", "utf8"); + +test("desktop SQL file opening survives unrelated initialization failure and follows restored tabs", async () => { + const events: string[] = []; + const barrier = createOpenTabsRestorationBarrier(); + const fileOpen = (async () => { + await barrier.settled; + events.push("read-file"); + events.push("open-tab"); + })(); + + await initializeDesktopOpenTabs({ + barrier, + initializeOptionalState: async () => { + events.push("initialize-ai-configs"); + throw new Error("AI config storage unavailable"); + }, + restoreOpenTabs: async () => { + events.push("initialize-editor-settings"); + events.push("initialize-connections"); + events.push("restore-tabs"); + }, + onOptionalStateError: () => events.push("ignore-ai-config-error"), + }); + await fileOpen; + + assert.deepEqual(events, ["initialize-ai-configs", "ignore-ai-config-error", "initialize-editor-settings", "initialize-connections", "restore-tabs", "read-file", "open-tab"]); +}); + +test("desktop SQL file opening is released when persisted tab restoration rejects", async () => { + const events: string[] = []; + const barrier = createOpenTabsRestorationBarrier(); + const fileOpen = barrier.settled.then(() => events.push("open-file")); + + await assert.rejects( + initializeDesktopOpenTabs({ + barrier, + initializeOptionalState: async () => {}, + restoreOpenTabs: async () => { + events.push("restore-tabs"); + throw new Error("corrupt persisted tabs"); + }, + onOptionalStateError: () => {}, + }), + /corrupt persisted tabs/, + ); + await fileOpen; + + assert.deepEqual(events, ["restore-tabs", "open-file"]); +}); + +test("cold-start SQL files wait for restored tabs before opening", () => { + const openPathStart = appSource.indexOf("async function openSqlFilePath"); + const openPathEnd = appSource.indexOf("async function openPendingSqlFiles", openPathStart); + assert.ok(openPathStart >= 0 && openPathEnd > openPathStart); + + const openPathSource = appSource.slice(openPathStart, openPathEnd); + const initializationWait = openPathSource.indexOf("await desktopOpenTabsRestorationBarrier?.settled"); + const fileRead = openPathSource.indexOf("api.readExternalSqlFile(path)"); + const tabOpen = openPathSource.indexOf("queryStore.openExternalSqlFile"); + assert.ok(initializationWait >= 0); + assert.ok(initializationWait < fileRead); + assert.ok(fileRead < tabOpen); + + const mountedStart = appSource.indexOf("onMounted(async () =>"); + const mountedEnd = appSource.indexOf("onUnmounted(", mountedStart); + assert.ok(mountedStart >= 0 && mountedEnd > mountedStart); + + const mountedSource = appSource.slice(mountedStart, mountedEnd); + const barrierCreation = mountedSource.indexOf("desktopOpenTabsRestorationBarrier = createOpenTabsRestorationBarrier()"); + const initializationStart = mountedSource.indexOf("void initApp()", barrierCreation); + const listenerSetup = mountedSource.indexOf("setupTauriListeners()", initializationStart); + const pendingFileOpen = mountedSource.indexOf("openPendingSqlFiles()"); + assert.ok(barrierCreation >= 0); + assert.ok(initializationStart >= 0); + assert.ok(barrierCreation < initializationStart); + assert.ok(initializationStart < listenerSetup); + assert.ok(listenerSetup < pendingFileOpen); + assert.ok(initializationStart < pendingFileOpen); +});