fix(desktop): preserve SQL file tabs on cold start
This commit is contained in:
parent
6c3c6864f3
commit
daf6bce72b
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
Loading…
Reference in New Issue