Spaces:
Runtime error
Runtime error
| // 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<ReturnType<(typeof import("sql.js"))["default"]>> | 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<typeof _sqlJsLib> { | |
| 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<SqliteAdapter> { | |
| 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<typeof setTimeout> | 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<T>(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<T>(fn: (...args: unknown[]) => T): (...args: unknown[]) => T { | |
| return (...args: unknown[]) => runSavepoint(fn, ...args); | |
| }, | |
| immediate(fn: () => void): void { | |
| runSavepoint(() => fn()); | |
| }, | |
| async backup(destination: string): Promise<void> { | |
| 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; | |
| }, | |
| }; | |
| } | |