// src/lib/db/adapters/sqljsAdapter.ts import fs from "node:fs"; import path from "node:path"; import type { SqliteAdapter, PreparedStatement, RunResult } from "./types"; const SAVE_DEBOUNCE_MS = 100; const CHECKPOINT_INTERVAL_MS = 60_000; let _sqlJsLib: Awaited> | null = null; function resolveSqlJsWasmPath(): string { const candidatePaths = [ path.join(process.cwd(), "node_modules", "sql.js", "dist", "sql-wasm.wasm"), path.join( process.cwd(), ".next", "standalone", "node_modules", "sql.js", "dist", "sql-wasm.wasm" ), ]; for (const candidatePath of candidatePaths) { if (fs.existsSync(candidatePath)) { return candidatePath; } } return candidatePaths[0]; } async function loadSqlJs(): Promise { if (_sqlJsLib) return _sqlJsLib; const initSqlJs = ((await import("sql.js")) as { default: (typeof import("sql.js"))["default"] }) .default; const wasmPath = resolveSqlJsWasmPath(); _sqlJsLib = await initSqlJs({ locateFile(fileName) { if (fileName === "sql-wasm.wasm") { return wasmPath; } return fileName; }, }); return _sqlJsLib; } export async function createSqlJsAdapter(filePath: string): Promise { const SQLLib = await loadSqlJs(); if (!SQLLib) throw new Error("[sqljsAdapter] Failed to load sql.js"); const buf = filePath !== ":memory:" && fs.existsSync(filePath) ? fs.readFileSync(filePath) : null; const db = new SQLLib.Database(buf ? new Uint8Array(buf) : undefined); let dirty = false; let saveTimer: ReturnType | null = null; let _isOpen = true; function persist(): void { if (filePath === ":memory:") return; const data = db.export(); fs.writeFileSync(filePath, Buffer.from(data)); dirty = false; } function scheduleSave(): void { dirty = true; if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { saveTimer = null; if (dirty) { try { persist(); } catch (e) { console.error("[sqljsAdapter] save failed:", e); } } }, SAVE_DEBOUNCE_MS); } function runSavepoint(fn: (...args: unknown[]) => T, ...args: unknown[]): T { const sp = `sp_${Math.random().toString(36).slice(2)}`; db.run(`SAVEPOINT "${sp}"`); try { const result = fn(...args); db.run(`RELEASE "${sp}"`); scheduleSave(); return result; } catch (err) { try { db.run(`ROLLBACK TO "${sp}"`); db.run(`RELEASE "${sp}"`); } catch {} throw err; } } function makeStatement(sql: string): PreparedStatement { return { run(...params: unknown[]): RunResult { const stmt = db.prepare(sql); try { if (params.length) stmt.bind(params as unknown[]); stmt.step(); const changes = db.getRowsModified(); const lastRows = db.exec("SELECT last_insert_rowid() as id"); const lastInsertRowid = (lastRows[0]?.values?.[0]?.[0] as number | null | undefined) ?? 0; scheduleSave(); return { changes, lastInsertRowid }; } finally { stmt.free(); } }, get(...params: unknown[]): unknown { const stmt = db.prepare(sql); try { if (params.length) stmt.bind(params as unknown[]); if (stmt.step()) return stmt.getAsObject(); return undefined; } finally { stmt.free(); } }, all(...params: unknown[]): unknown[] { const stmt = db.prepare(sql); try { if (params.length) stmt.bind(params as unknown[]); const rows: unknown[] = []; while (stmt.step()) rows.push(stmt.getAsObject()); return rows; } finally { stmt.free(); } }, }; } const checkpointTimer = setInterval(() => { if (dirty) try { persist(); } catch {} }, CHECKPOINT_INTERVAL_MS); (checkpointTimer as unknown as NodeJS.Timeout).unref?.(); function gracefulClose(): void { clearInterval(checkpointTimer as unknown as NodeJS.Timeout); if (saveTimer) clearTimeout(saveTimer); if (dirty) try { persist(); } catch {} try { db.close(); } catch {} _isOpen = false; } const flush = (): void => { if (dirty) try { persist(); } catch {} }; process.on("beforeExit", flush); process.on("SIGINT", flush); process.on("SIGTERM", flush); return { driver: "sql.js", get open() { return _isOpen; }, get name() { return filePath; }, prepare(sql: string): PreparedStatement { return makeStatement(sql); }, exec(sql: string): void { db.run(sql); scheduleSave(); }, pragma(pragmaStr: string, options?: { simple?: boolean }): unknown { const result = db.exec(`PRAGMA ${pragmaStr}`); if (!result.length) return null; const rows = result[0]; if (options?.simple) { return rows.values?.[0]?.[0] ?? null; } return (rows.values ?? []).map((row: unknown[]) => Object.fromEntries(rows.columns.map((col: string, i: number) => [col, row[i]])) ); }, transaction(fn: (...args: unknown[]) => T): (...args: unknown[]) => T { return (...args: unknown[]) => runSavepoint(fn, ...args); }, immediate(fn: () => void): void { runSavepoint(() => fn()); }, async backup(destination: string): Promise { if (dirty) persist(); if (filePath !== ":memory:") fs.copyFileSync(filePath, destination); }, checkpoint(_mode = "TRUNCATE"): void { if (dirty) try { persist(); } catch {} }, close(): void { gracefulClose(); }, get raw() { return db; }, }; }