refactor(desktop): organize lib modules by domain

This commit is contained in:
t8y2 committed 2026-07-04 13:53:52 +08:00
1 parent 90f0594e93
commit 7e7c4944e9
700 files changed
+1292 -1264

No files matched your search

@@ -0,0 +1,140 @@
import { decode, encode } from "@msgpack/msgpack";
import type { QueryTab } from "@/types/database";
import { decodeTabResultSnapshot, encodeTabResultSnapshot, type TabResultSnapshot } from "@/lib/tabs/tabResultCache";
const ARCHIVE_MAGIC = "DBX_QUERY_RESULT_ARCHIVE";
const ARCHIVE_VERSION = 1;
const ARCHIVE_CODEC = "msgpack-tab-result-snapshot";
export interface QueryResultArchiveTab {
title: string;
connectionId: string;
database: string;
schema?: string;
sql: string;
lastExecutedSql?: string;
resultBaseSql?: string;
resultSortedSql?: string;
}
export interface DecodedQueryResultArchive {
createdAt: number;
tab: QueryResultArchiveTab;
snapshot: TabResultSnapshot;
}
interface QueryResultArchiveEnvelope {
magic: typeof ARCHIVE_MAGIC;
version: typeof ARCHIVE_VERSION;
codec: typeof ARCHIVE_CODEC;
createdAt: number;
tab: QueryResultArchiveTab;
snapshot: Uint8Array;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isBinaryLike(value: unknown): boolean {
return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
}
function removeUndefinedFields<T>(value: T): T {
if (Array.isArray(value)) return value.map((item) => removeUndefinedFields(item)) as T;
if (isBinaryLike(value)) return value;
if (!isRecord(value)) return value;
return Object.fromEntries(
Object.entries(value)
.filter(([, entryValue]) => entryValue !== undefined)
.map(([key, entryValue]) => [key, removeUndefinedFields(entryValue)]),
) as T;
}
function archiveTabMetadata(tab: QueryTab): QueryResultArchiveTab {
return removeUndefinedFields({
title: tab.title,
connectionId: tab.connectionId,
database: tab.database,
schema: tab.schema,
sql: tab.sql,
lastExecutedSql: tab.lastExecutedSql,
resultBaseSql: tab.resultBaseSql,
resultSortedSql: tab.resultSortedSql,
});
}
function isArchiveTab(value: unknown): value is QueryResultArchiveTab {
if (!isRecord(value)) return false;
return typeof value.title === "string" && typeof value.connectionId === "string" && typeof value.database === "string" && typeof value.sql === "string";
}
function binaryPayload(value: unknown): Uint8Array | undefined {
if (value instanceof Uint8Array) return value;
if (value instanceof ArrayBuffer) return new Uint8Array(value);
return undefined;
}
async function transformBytes(bytes: Uint8Array, stream: CompressionStream | DecompressionStream): Promise<Uint8Array> {
const output = new Response(stream.readable).arrayBuffer();
const writer = stream.writable.getWriter();
await writer.write(bytes.slice());
await writer.close();
return new Uint8Array(await output);
}
async function gzipBytes(bytes: Uint8Array): Promise<Uint8Array> {
if (typeof CompressionStream === "undefined") return bytes;
try {
return await transformBytes(bytes, new CompressionStream("gzip"));
} catch {
return bytes;
}
}
async function gunzipBytes(bytes: Uint8Array): Promise<Uint8Array> {
if (bytes[0] !== 0x1f || bytes[1] !== 0x8b || typeof DecompressionStream === "undefined") return bytes;
return transformBytes(bytes, new DecompressionStream("gzip"));
}
export function defaultQueryResultArchiveFileName(title: string | undefined): string {
const base = (title ?? "")
.trim()
.replace(/[^A-Za-z0-9._-]+/g, "_")
.replace(/^_+|_+$/g, "")
.slice(0, 80);
return `${base || "query-results"}.dbxresults`;
}
export async function encodeQueryResultArchive(tab: QueryTab, snapshot: TabResultSnapshot): Promise<Uint8Array> {
const envelope: QueryResultArchiveEnvelope = {
magic: ARCHIVE_MAGIC,
version: ARCHIVE_VERSION,
codec: ARCHIVE_CODEC,
createdAt: Date.now(),
tab: archiveTabMetadata(tab),
snapshot: encodeTabResultSnapshot(snapshot),
};
return gzipBytes(encode(removeUndefinedFields(envelope)));
}
export async function decodeQueryResultArchive(bytes: Uint8Array | ArrayBuffer): Promise<DecodedQueryResultArchive | undefined> {
try {
const rawBytes = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
const decoded = decode(await gunzipBytes(rawBytes));
if (!isRecord(decoded)) return undefined;
if (decoded.magic !== ARCHIVE_MAGIC || decoded.version !== ARCHIVE_VERSION || decoded.codec !== ARCHIVE_CODEC) return undefined;
if (!isArchiveTab(decoded.tab)) return undefined;
const snapshotBytes = binaryPayload(decoded.snapshot);
if (!snapshotBytes) return undefined;
const snapshot = decodeTabResultSnapshot(snapshotBytes);
if (!snapshot) return undefined;
return {
createdAt: typeof decoded.createdAt === "number" ? decoded.createdAt : Date.now(),
tab: decoded.tab,
snapshot,
};
} catch {
return undefined;
}
}
@@ -0,0 +1,62 @@
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
const ARCHIVE_MIME_TYPE = "application/vnd.dbx.results";
const ARCHIVE_EXTENSIONS = ["dbxresults"];
function downloadArchiveFile(fileName: string, bytes: Uint8Array): string {
const blob = new Blob([bytes.slice().buffer], { type: ARCHIVE_MIME_TYPE });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
return fileName;
}
function openArchiveFileInBrowser(): Promise<Uint8Array | undefined> {
return new Promise((resolve, reject) => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".dbxresults,application/octet-stream,application/vnd.dbx.results";
input.onchange = async () => {
try {
const file = input.files?.[0];
if (!file) {
resolve(undefined);
return;
}
resolve(new Uint8Array(await file.arrayBuffer()));
} catch (error) {
reject(error);
}
};
input.click();
});
}
export async function saveQueryResultArchiveFile(fileName: string, bytes: Uint8Array): Promise<string | undefined> {
if (!isTauriRuntime()) return downloadArchiveFile(fileName, bytes);
const [{ save }, { writeFile }] = await Promise.all([import("@tauri-apps/plugin-dialog"), import("@tauri-apps/plugin-fs")]);
const path = await save({
defaultPath: fileName,
filters: [{ name: "DBX Result Archive", extensions: ARCHIVE_EXTENSIONS }],
});
if (!path) return undefined;
await writeFile(path, bytes);
return path;
}
export async function openQueryResultArchiveFile(): Promise<Uint8Array | undefined> {
if (!isTauriRuntime()) return openArchiveFileInBrowser();
const [{ open }, { readFile }] = await Promise.all([import("@tauri-apps/plugin-dialog"), import("@tauri-apps/plugin-fs")]);
const selected = await open({
multiple: false,
filters: [{ name: "DBX Result Archive", extensions: ARCHIVE_EXTENSIONS }],
});
const path = Array.isArray(selected) ? selected[0] : selected;
if (!path) return undefined;
return readFile(path);
}
@@ -0,0 +1,13 @@
import type { QueryResult } from "@/types/database";
// Lake/external tables (e.g. Paimon in StarRocks) return this error on a data
// read when no snapshot exists yet, while metadata reads (DESC/SHOW CREATE)
// still succeed. executeTabSql surfaces query failures as an "Error" result, so
// callers can detect this case and fall back to a structure-only (LIMIT 0)
// preview instead of showing a cryptic server error.
const NO_SNAPSHOT_ERROR_PATTERN = /there is currently no snapshot/i;
export function isNoSnapshotErrorResult(result: QueryResult | undefined | null): boolean {
if (!result || !result.columns.includes("Error") || result.rows.length === 0) return false;
return NO_SNAPSHOT_ERROR_PATTERN.test(String(result.rows[0]?.[0] ?? ""));
}