fix(desktop): preserve SQL file tabs on cold start

This commit is contained in:
guoyongchang 2026-07-30 18:42:33 +08:00 committed by GitHub
parent 6c3c6864f3
commit daf6bce72b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 149 additions and 2 deletions

View File

@ -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(() => {

View File

@ -0,0 +1,41 @@
export interface OpenTabsRestorationBarrier {
readonly settled: Promise<void>;
settle: () => void;
}
export function createOpenTabsRestorationBarrier(): OpenTabsRestorationBarrier {
let resolveBarrier: () => void;
let isSettled = false;
const settled = new Promise<void>((resolve) => {
resolveBarrier = resolve;
});
return {
settled,
settle: () => {
if (isSettled) return;
isSettled = true;
resolveBarrier();
},
};
}
interface InitializeDesktopOpenTabsOptions {
barrier: OpenTabsRestorationBarrier;
initializeOptionalState: () => Promise<void>;
restoreOpenTabs: () => Promise<void>;
onOptionalStateError: (error: unknown) => void;
}
export async function initializeDesktopOpenTabs({ barrier, initializeOptionalState, restoreOpenTabs, onOptionalStateError }: InitializeDesktopOpenTabsOptions): Promise<void> {
try {
try {
await initializeOptionalState();
} catch (error) {
onOptionalStateError(error);
}
await restoreOpenTabs();
} finally {
barrier.settle();
}
}

View File

@ -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);
});