import type { QueryResult, QueryTab } from "@/types/database"; import { decode, encode } from "@msgpack/msgpack"; import { toRaw } from "vue"; import { isTauriRuntime } from "@/lib/tauriRuntime"; const DB_NAME = "dbx-tab-runtime-cache"; const DB_VERSION = 1; const RESULT_STORE = "resultSnapshots"; const PAYLOAD_MAGIC = "DBX_TAB_RESULT_CACHE"; const PAYLOAD_VERSION = 1; const PAYLOAD_CODEC = "msgpack-columnar"; type CellValue = QueryResult["rows"][number][number]; export interface TabResultSnapshot { result?: QueryResult; results?: QueryResult[]; activeResultIndex?: number; resultRuns?: QueryTab["resultRuns"]; activeResultRunId?: string; queryAnalysis?: QueryTab["queryAnalysis"]; querySourceColumns?: QueryTab["querySourceColumns"]; queryEditabilityReason?: QueryTab["queryEditabilityReason"]; mongoEditTarget?: QueryTab["mongoEditTarget"]; tableMeta?: QueryTab["tableMeta"]; resultPageSql?: string; resultPageLimit?: number; resultPageOffset?: number; resultCountSql?: string; resultTotalRowCount?: number; cachedAt: number; } interface ColumnarQueryResult { columns: string[]; column_types?: string[]; columnValues: CellValue[][]; rowCount: number; affected_rows: number; execution_time_ms: number; truncated?: boolean; has_more?: boolean; sourceLabel?: string; sourceStatement?: string; } type QueryResultRunSnapshot = NonNullable[number]; interface ColumnarQueryResultRun extends Omit { result?: ColumnarQueryResult; results?: ColumnarQueryResult[]; } interface TabResultSnapshotPayload extends Omit { result?: ColumnarQueryResult; results?: ColumnarQueryResult[]; resultRuns?: ColumnarQueryResultRun[]; } interface TabResultCacheEnvelope { magic: typeof PAYLOAD_MAGIC; version: typeof PAYLOAD_VERSION; codec: typeof PAYLOAD_CODEC; cachedAt: number; rowCount: number; columnCount: number; payload: TabResultSnapshotPayload; } function indexedDb(): IDBFactory | undefined { return typeof globalThis.indexedDB === "undefined" ? undefined : globalThis.indexedDB; } function requestToPromise(request: IDBRequest): Promise { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed")); }); } let dbPromise: Promise | undefined; const cacheKeyVersions = new Map(); function bumpCacheKeyVersion(key: string): number { const version = (cacheKeyVersions.get(key) ?? 0) + 1; cacheKeyVersions.set(key, version); return version; } function isCurrentCacheKeyVersion(key: string, version: number): boolean { return cacheKeyVersions.get(key) === version; } function clearCacheKeyVersionIfCurrent(key: string, version: number) { if (isCurrentCacheKeyVersion(key, version)) cacheKeyVersions.delete(key); } function openCacheDb(): Promise { if (dbPromise) return dbPromise; const idb = indexedDb(); if (!idb) return Promise.resolve(null); dbPromise = new Promise((resolve) => { const request = idb.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(RESULT_STORE)) db.createObjectStore(RESULT_STORE); }; request.onsuccess = () => resolve(request.result); request.onerror = () => { console.warn("[DBX][tab-result-cache:open:error]", request.error); resolve(null); }; request.onblocked = () => resolve(null); }); return dbPromise; } function clonePlain(value: T): T { const raw = toRaw(value); if (typeof structuredClone === "function") return structuredClone(raw); return JSON.parse(JSON.stringify(raw)) as T; } function stripSessionIds(result: QueryResult | undefined): QueryResult | undefined { if (!result) return undefined; return { columns: [...result.columns], column_types: result.column_types ? [...result.column_types] : undefined, rows: result.rows.map((row) => [...row]), affected_rows: result.affected_rows, execution_time_ms: result.execution_time_ms, truncated: result.truncated, session_id: undefined, has_more: result.has_more, sourceLabel: result.sourceLabel, sourceStatement: result.sourceStatement, }; } function stripResultSessionIds(results: QueryResult[] | undefined): QueryResult[] | undefined { return results?.map((result) => stripSessionIds(result)!); } function stripResultRunSessionIds(resultRuns: QueryTab["resultRuns"]): QueryTab["resultRuns"] { return resultRuns?.map((run) => ({ ...run, result: stripSessionIds(run.result), results: stripResultSessionIds(run.results), resultSessionId: undefined, })); } function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult | undefined { if (!result) return undefined; const columnValues = result.columns.map((_, colIndex) => result.rows.map((row) => row[colIndex] ?? null)); return removeUndefinedFields({ columns: [...result.columns], column_types: result.column_types ? [...result.column_types] : undefined, columnValues, rowCount: result.rows.length, affected_rows: result.affected_rows, execution_time_ms: result.execution_time_ms, truncated: result.truncated, has_more: result.has_more, sourceLabel: result.sourceLabel, sourceStatement: result.sourceStatement, }); } function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResult | undefined { if (!result) return undefined; const rows = Array.from({ length: result.rowCount }, (_, rowIndex) => result.columnValues.map((values) => values[rowIndex] ?? null)); return { columns: [...result.columns], column_types: result.column_types ? [...result.column_types] : undefined, rows, affected_rows: result.affected_rows, execution_time_ms: result.execution_time_ms, truncated: result.truncated, session_id: undefined, has_more: result.has_more, sourceLabel: result.sourceLabel, sourceStatement: result.sourceStatement, }; } function snapshotToPayload(snapshot: TabResultSnapshot): TabResultSnapshotPayload { return removeUndefinedFields({ ...snapshot, result: toColumnarResult(snapshot.result), results: snapshot.results?.map((result) => toColumnarResult(result)!), resultRuns: snapshot.resultRuns?.map((run) => removeUndefinedFields({ ...run, result: toColumnarResult(run.result), results: run.results?.map((result) => toColumnarResult(result)!), }), ), }); } function payloadToSnapshot(payload: TabResultSnapshotPayload): TabResultSnapshot { return { ...payload, result: fromColumnarResult(payload.result), results: payload.results?.map((result) => fromColumnarResult(result)!), resultRuns: payload.resultRuns?.map((run) => ({ ...run, result: fromColumnarResult(run.result), results: run.results?.map((result) => fromColumnarResult(result)!), })), }; } function resultStats(snapshot: TabResultSnapshot): { rowCount: number; columnCount: number } { const activeRun = snapshot.resultRuns?.find((run) => run.id === snapshot.activeResultRunId) ?? snapshot.resultRuns?.[0]; const result = snapshot.result ?? snapshot.results?.[snapshot.activeResultIndex ?? 0] ?? snapshot.results?.[0] ?? activeRun?.result ?? activeRun?.results?.[activeRun.activeResultIndex ?? 0] ?? activeRun?.results?.[0]; return { rowCount: result?.rows.length ?? 0, columnCount: result?.columns.length ?? 0, }; } function bytesToBase64(bytes: Uint8Array): string { let binary = ""; const chunkSize = 0x8000; for (let offset = 0; offset < bytes.length; offset += chunkSize) { binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)); } return btoa(binary); } function base64ToBytes(value: string): Uint8Array { const binary = atob(value); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); return bytes; } function canUseRemoteRuntimeCache(): boolean { return typeof btoa !== "undefined" && typeof atob !== "undefined" && (isTauriRuntime() || typeof fetch !== "undefined"); } async function writeRemoteRuntimeCache(key: string, bytes: Uint8Array, stats: { rowCount: number; columnCount: number }): Promise { if (!canUseRemoteRuntimeCache()) return false; try { if (isTauriRuntime()) { const { invoke } = await import("@tauri-apps/api/core"); await invoke("save_tab_runtime_cache", { key, payloadBase64: bytesToBase64(bytes), rowCount: stats.rowCount, columnCount: stats.columnCount, }); return true; } const response = await fetch("/api/tab-runtime-cache", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, payloadBase64: bytesToBase64(bytes), rowCount: stats.rowCount, columnCount: stats.columnCount, }), }); return response.ok; } catch (error) { console.warn("[DBX][tab-result-cache:remote-write:error]", { key, error }); return false; } } async function readRemoteRuntimeCache(key: string): Promise { if (!canUseRemoteRuntimeCache()) return undefined; try { if (isTauriRuntime()) { const { invoke } = await import("@tauri-apps/api/core"); const entry = await invoke<{ payloadBase64?: string } | null>("load_tab_runtime_cache", { key }); return entry?.payloadBase64 ? base64ToBytes(entry.payloadBase64) : undefined; } const response = await fetch(`/api/tab-runtime-cache?key=${encodeURIComponent(key)}`); if (!response.ok) return undefined; const entry = (await response.json()) as { payloadBase64?: string } | null; return entry?.payloadBase64 ? base64ToBytes(entry.payloadBase64) : undefined; } catch (error) { console.warn("[DBX][tab-result-cache:remote-read:error]", { key, error }); return undefined; } } async function deleteRemoteRuntimeCache(key: string): Promise { if (!canUseRemoteRuntimeCache()) return; try { if (isTauriRuntime()) { const { invoke } = await import("@tauri-apps/api/core"); await invoke("delete_tab_runtime_cache", { key }); return; } await fetch(`/api/tab-runtime-cache?key=${encodeURIComponent(key)}`, { method: "DELETE" }); } catch (error) { console.warn("[DBX][tab-result-cache:remote-delete:error]", { key, error }); } } function scheduleRemoteRuntimeCacheWrite(key: string, bytes: Uint8Array, stats: { rowCount: number; columnCount: number }, version: number) { window.setTimeout(async () => { if (!isCurrentCacheKeyVersion(key, version)) return; await writeRemoteRuntimeCache(key, bytes, stats); clearCacheKeyVersionIfCurrent(key, version); }, 0); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function removeUndefinedFields(value: T): T { if (Array.isArray(value)) return value.map((item) => removeUndefinedFields(item)) as T; if (!isRecord(value)) return value; return Object.fromEntries( Object.entries(value) .filter(([, entryValue]) => entryValue !== undefined) .map(([key, entryValue]) => [key, removeUndefinedFields(entryValue)]), ) as T; } function isBinaryPayload(value: unknown): value is Uint8Array { return value instanceof Uint8Array || value instanceof ArrayBuffer; } export function encodeTabResultSnapshot(snapshot: TabResultSnapshot): Uint8Array { const stats = resultStats(snapshot); const envelope: TabResultCacheEnvelope = { magic: PAYLOAD_MAGIC, version: PAYLOAD_VERSION, codec: PAYLOAD_CODEC, cachedAt: snapshot.cachedAt, rowCount: stats.rowCount, columnCount: stats.columnCount, payload: snapshotToPayload(snapshot), }; return encode(removeUndefinedFields(envelope)); } export function decodeTabResultSnapshot(bytes: Uint8Array | ArrayBuffer): TabResultSnapshot | undefined { const decoded = decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)); if (!isRecord(decoded)) return undefined; if (decoded.magic !== PAYLOAD_MAGIC || decoded.version !== PAYLOAD_VERSION || decoded.codec !== PAYLOAD_CODEC) { return undefined; } if (!isRecord(decoded.payload)) return undefined; return payloadToSnapshot(decoded.payload as unknown as TabResultSnapshotPayload); } export function tabResultCacheKey(tabId: string): string { return `tab:${tabId}:result`; } export function buildTabResultSnapshot(tab: QueryTab): TabResultSnapshot | undefined { if (!tab.result && !tab.results && !tab.resultRuns?.length) return undefined; return { result: stripSessionIds(tab.result), results: stripResultSessionIds(tab.results), activeResultIndex: tab.activeResultIndex, resultRuns: stripResultRunSessionIds(tab.resultRuns), activeResultRunId: tab.activeResultRunId, queryAnalysis: tab.queryAnalysis ? clonePlain(tab.queryAnalysis) : undefined, querySourceColumns: tab.querySourceColumns ? [...tab.querySourceColumns] : undefined, queryEditabilityReason: tab.queryEditabilityReason, mongoEditTarget: tab.mongoEditTarget ? clonePlain(tab.mongoEditTarget) : undefined, tableMeta: tab.tableMeta ? clonePlain(tab.tableMeta) : undefined, resultPageSql: tab.resultPageSql, resultPageLimit: tab.resultPageLimit, resultPageOffset: tab.resultPageOffset, resultCountSql: tab.resultCountSql, resultTotalRowCount: tab.resultTotalRowCount, cachedAt: Date.now(), }; } export async function writeTabResultSnapshot(key: string, snapshot: TabResultSnapshot | undefined): Promise { if (!snapshot) return false; const version = bumpCacheKeyVersion(key); const encoded = encodeTabResultSnapshot(snapshot); const stats = resultStats(snapshot); let wroteLocal = false; try { const db = await openCacheDb(); if (db) { const tx = db.transaction(RESULT_STORE, "readwrite"); await requestToPromise(tx.objectStore(RESULT_STORE).put(encoded, key)); wroteLocal = true; } } catch (error) { console.warn("[DBX][tab-result-cache:write:error]", { key, error }); } if (wroteLocal) { if (typeof window === "undefined") { void writeRemoteRuntimeCache(key, encoded, stats).finally(() => clearCacheKeyVersionIfCurrent(key, version)); } else { scheduleRemoteRuntimeCacheWrite(key, encoded, stats, version); } return true; } if (!isCurrentCacheKeyVersion(key, version)) return false; try { return await writeRemoteRuntimeCache(key, encoded, stats); } finally { clearCacheKeyVersionIfCurrent(key, version); } } export async function readTabResultSnapshot(key: string): Promise { try { const db = await openCacheDb(); if (db) { const value = await requestToPromise(db.transaction(RESULT_STORE, "readonly").objectStore(RESULT_STORE).get(key)); if (isBinaryPayload(value)) return decodeTabResultSnapshot(value); if (value) return value as TabResultSnapshot; } const remoteBytes = await readRemoteRuntimeCache(key); const remoteSnapshot = remoteBytes ? decodeTabResultSnapshot(remoteBytes) : undefined; if (remoteBytes && remoteSnapshot && db) { void requestToPromise(db.transaction(RESULT_STORE, "readwrite").objectStore(RESULT_STORE).put(remoteBytes, key)); } return remoteSnapshot; } catch (error) { console.warn("[DBX][tab-result-cache:read:error]", { key, error }); const remoteBytes = await readRemoteRuntimeCache(key); return remoteBytes ? decodeTabResultSnapshot(remoteBytes) : undefined; } } export async function deleteTabResultSnapshot(key: string): Promise { const version = bumpCacheKeyVersion(key); const clearVersionLater = () => { const cleanup = () => clearCacheKeyVersionIfCurrent(key, version); if (typeof window !== "undefined") window.setTimeout(cleanup, 5000); else setTimeout(cleanup, 5000); }; try { const db = await openCacheDb(); if (db) await requestToPromise(db.transaction(RESULT_STORE, "readwrite").objectStore(RESULT_STORE).delete(key)); await deleteRemoteRuntimeCache(key); clearVersionLater(); } catch (error) { console.warn("[DBX][tab-result-cache:delete:error]", { key, error }); await deleteRemoteRuntimeCache(key); clearVersionLater(); } }