ai_api / src /lib /db /adapters /sqljsAdapter.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
5.96 kB
// 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;
},
};
}