Improve memory retention in query results (#395)
This commit is contained in:
parent
e1fc3a4fc1
commit
b23988161a
|
|
@ -103,7 +103,7 @@ const showSettingsDialog = ref(false);
|
|||
const showDriverStore = ref(false);
|
||||
const agentDriverUpdateCount = ref(0);
|
||||
const showHistory = ref(false);
|
||||
const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") !== "false");
|
||||
const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") === "true");
|
||||
const aiPanelReady = ref(false);
|
||||
const { sidebarWidth, aiPanelWidth, historyWidth, startSidebarResize, startAiPanelResize, startHistoryResize } =
|
||||
usePanelResize();
|
||||
|
|
|
|||
|
|
@ -62,6 +62,22 @@ export const useQueryStore = defineStore("query", () => {
|
|||
}
|
||||
}
|
||||
|
||||
function clearResultPayload(tab: QueryTab, options: { evicted?: boolean } = {}) {
|
||||
tab.result = undefined;
|
||||
tab.results = undefined;
|
||||
tab.activeResultIndex = undefined;
|
||||
tab.resultSessionId = undefined;
|
||||
tab.queryAnalysis = undefined;
|
||||
tab.querySourceColumns = undefined;
|
||||
tab.queryEditabilityReason = undefined;
|
||||
tab.resultEvicted = options.evicted ? true : undefined;
|
||||
}
|
||||
|
||||
async function evictCachedResult(tab: QueryTab) {
|
||||
await closeResultSession(tab);
|
||||
clearResultPayload(tab, { evicted: true });
|
||||
}
|
||||
|
||||
const _persistSnapshot = computed(() =>
|
||||
tabs.value.map((t) => ({
|
||||
id: t.id,
|
||||
|
|
@ -164,8 +180,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
if (tabs.value[idx].isExecuting) void cancelTabExecution(id);
|
||||
if (tabs.value[idx].isExplaining) void cancelTabExplain(id);
|
||||
void closeResultSession(tabs.value[idx]);
|
||||
tabs.value[idx].result = undefined;
|
||||
tabs.value[idx].results = undefined;
|
||||
clearResultPayload(tabs.value[idx]);
|
||||
tabs.value.splice(idx, 1);
|
||||
if (activeTabId.value === id) {
|
||||
activeTabId.value = tabs.value[Math.min(idx, tabs.value.length - 1)]?.id ?? null;
|
||||
|
|
@ -248,13 +263,11 @@ export const useQueryStore = defineStore("query", () => {
|
|||
tab.database = database;
|
||||
tab.schema = undefined;
|
||||
tab.objectBrowser = undefined;
|
||||
tab.result = undefined;
|
||||
void closeResultSession(tab);
|
||||
clearResultPayload(tab);
|
||||
tab.lastExecutedSql = undefined;
|
||||
tab.resultBaseSql = undefined;
|
||||
tab.resultSortedSql = undefined;
|
||||
tab.queryAnalysis = undefined;
|
||||
tab.querySourceColumns = undefined;
|
||||
tab.queryEditabilityReason = undefined;
|
||||
clearExplain(tab);
|
||||
tab.tableMeta = undefined;
|
||||
}
|
||||
|
|
@ -272,13 +285,11 @@ export const useQueryStore = defineStore("query", () => {
|
|||
tab.connectionId = connectionId;
|
||||
tab.database = database;
|
||||
tab.schema = undefined;
|
||||
tab.result = undefined;
|
||||
void closeResultSession(tab);
|
||||
clearResultPayload(tab);
|
||||
tab.lastExecutedSql = undefined;
|
||||
tab.resultBaseSql = undefined;
|
||||
tab.resultSortedSql = undefined;
|
||||
tab.queryAnalysis = undefined;
|
||||
tab.querySourceColumns = undefined;
|
||||
tab.queryEditabilityReason = undefined;
|
||||
clearExplain(tab);
|
||||
tab.tableMeta = undefined;
|
||||
}
|
||||
|
|
@ -325,6 +336,9 @@ export const useQueryStore = defineStore("query", () => {
|
|||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab) return;
|
||||
tab.result = toErrorResult(e);
|
||||
tab.results = undefined;
|
||||
tab.activeResultIndex = undefined;
|
||||
tab.resultSessionId = undefined;
|
||||
tab.isExecuting = false;
|
||||
tab.isCancelling = false;
|
||||
tab.executionId = undefined;
|
||||
|
|
@ -608,7 +622,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
});
|
||||
}
|
||||
}
|
||||
trimResultCache();
|
||||
await trimResultCache();
|
||||
}
|
||||
|
||||
async function explainTabSql(id: string, sql: string, databaseType?: DatabaseType) {
|
||||
|
|
@ -707,17 +721,11 @@ export const useQueryStore = defineStore("query", () => {
|
|||
tab.queryEditabilityReason = undefined;
|
||||
}
|
||||
|
||||
function trimResultCache() {
|
||||
const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && t.result);
|
||||
async function trimResultCache() {
|
||||
const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && (t.result || t.results));
|
||||
if (inactive.length > MAX_CACHED_RESULTS) {
|
||||
const toEvict = inactive.slice(0, inactive.length - MAX_CACHED_RESULTS);
|
||||
toEvict.forEach((t) => {
|
||||
t.result = undefined;
|
||||
t.resultEvicted = true;
|
||||
t.queryAnalysis = undefined;
|
||||
t.querySourceColumns = undefined;
|
||||
t.queryEditabilityReason = undefined;
|
||||
});
|
||||
await Promise.all(toEvict.map((t) => evictCachedResult(t)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,25 @@ import { strict as assert } from "node:assert";
|
|||
import test from "node:test";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { useQueryStore } from "../../apps/desktop/src/stores/queryStore.ts";
|
||||
import type { QueryResult } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
function installMemoryStorage() {
|
||||
const values = new Map<string, string>();
|
||||
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: (key: string) => values.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => values.set(key, value),
|
||||
removeItem: (key: string) => values.delete(key),
|
||||
clear: () => values.clear(),
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
if (original) Object.defineProperty(globalThis, "localStorage", original);
|
||||
else Reflect.deleteProperty(globalThis, "localStorage");
|
||||
};
|
||||
}
|
||||
|
||||
test("setErrorResult stops loading and shows the error result", () => {
|
||||
setActivePinia(createPinia());
|
||||
|
|
@ -18,3 +37,65 @@ test("setErrorResult stops loading and shows the error result", () => {
|
|||
assert.deepEqual(tab?.result?.columns, ["Error"]);
|
||||
assert.deepEqual(tab?.result?.rows, [["Error: metadata failed"]]);
|
||||
});
|
||||
|
||||
test("evicting cached tab results releases multi-result payloads and sessions", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
const store = useQueryStore();
|
||||
const originalFetch = globalThis.fetch;
|
||||
let executeCount = 0;
|
||||
const closedSessions: string[] = [];
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/query/execute-multi") {
|
||||
executeCount++;
|
||||
const results: QueryResult[] = [
|
||||
{
|
||||
columns: ["id"],
|
||||
rows: [[executeCount]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
session_id: `session-${executeCount}`,
|
||||
},
|
||||
{
|
||||
columns: ["detail"],
|
||||
rows: [[`payload-${executeCount}`]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
},
|
||||
];
|
||||
return new Response(JSON.stringify(results), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url === "/api/query/close-session") {
|
||||
const body = JSON.parse(String(init?.body ?? "{}"));
|
||||
closedSessions.push(body.sessionId);
|
||||
return new Response(JSON.stringify(true), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response("unexpected request", { status: 500 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const tabIds: string[] = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const tabId = store.createTab("conn-1", "db", `Query ${i + 1}`);
|
||||
tabIds.push(tabId);
|
||||
await store.executeTabSql(tabId, `select ${i + 1}; select ${i + 1} as detail`);
|
||||
}
|
||||
|
||||
const evicted = store.tabs.find((tab) => tab.id === tabIds[0]);
|
||||
assert.equal(executeCount, 7);
|
||||
assert.equal(evicted?.result, undefined);
|
||||
assert.equal(evicted?.results, undefined);
|
||||
assert.equal(evicted?.activeResultIndex, undefined);
|
||||
assert.equal(evicted?.resultSessionId, undefined);
|
||||
assert.equal(evicted?.resultEvicted, true);
|
||||
assert.deepEqual(closedSessions, ["session-1"]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
restoreStorage();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ test("app defers cold-start side panels and modal pages behind async components"
|
|||
}
|
||||
});
|
||||
|
||||
test("AI assistant panel stays closed on first launch to preserve startup memory", () => {
|
||||
assert.match(appSource, /const showAiPanel = ref\(localStorage\.getItem\("dbx-ai-panel-open"\) === "true"\)/);
|
||||
});
|
||||
|
||||
test("app dialogs keep non-primary dialogs out of the startup chunk", () => {
|
||||
for (const component of ["ConnectionDialog", "EditorSettingsDialog", "DangerConfirmDialog"]) {
|
||||
assert.doesNotMatch(appDialogsSource, new RegExp(`import ${component} from`));
|
||||
|
|
|
|||
Loading…
Reference in New Issue