Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@remy/origin-sql

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@remy/origin-sql - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+97
-23
dist/origin-sql.bundle.js

@@ -1,2 +0,2 @@

/* origin-sql 0.1.0 — browser-side SQLite with optional libSQL sync */
/* origin-sql 0.2.0 — browser-side SQLite with optional libSQL sync */
/* Homepage: https://github.com/ */

@@ -73,7 +73,11 @@ /* License: MIT */

});
function call(op, payload) {
function call(op, payload, transfer) {
const id = nextId++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
port.postMessage({ id, op, payload });
if (transfer && transfer.length) {
port.postMessage({ id, op, payload }, transfer);
} else {
port.postMessage({ id, op, payload });
}
});

@@ -473,2 +477,3 @@ }

let intervalTimer = null;
let inFlightSync = null;
function deliverStatus() {

@@ -498,21 +503,29 @@ for (const cb of Array.from(statusListeners)) {

await setStatus({ state: "syncing", lastError: void 0 });
const promise = (async () => {
try {
const result = await syncer.sync();
if (result.pull?.pulled > 0) {
const { tables } = await rpc.call("sync:user-schema", {});
notifier.emit(tables.map((t) => t.name));
}
backoffFailures = 0;
pausedByAuth = false;
await setStatus({
state: "idle",
lastSyncedAt: Date.now(),
lastError: void 0
});
return result;
} catch (err) {
if (err instanceof AuthError) pausedByAuth = true;
else backoffFailures++;
await setStatus({ state: "error", lastError: err });
throw err;
}
})();
inFlightSync = promise;
try {
const result = await syncer.sync();
if (result.pull?.pulled > 0) {
const { tables } = await rpc.call("sync:user-schema", {});
notifier.emit(tables.map((t) => t.name));
}
backoffFailures = 0;
pausedByAuth = false;
await setStatus({
state: "idle",
lastSyncedAt: Date.now(),
lastError: void 0
});
return result;
} catch (err) {
if (err instanceof AuthError) pausedByAuth = true;
else backoffFailures++;
await setStatus({ state: "error", lastError: err });
throw err;
return await promise;
} finally {
if (inFlightSync === promise) inFlightSync = null;
}

@@ -568,2 +581,53 @@ }

}
async function exportDatabase() {
assertOpen();
if (inFlightSync) {
try {
await inFlightSync;
} catch {
}
}
const bytes = await rpc.call("export-file", {});
return new Blob([bytes], { type: "application/vnd.sqlite3" });
}
async function importDatabase(blob) {
assertOpen();
if (!(blob instanceof Blob) && !(blob instanceof ArrayBuffer) && !(blob instanceof Uint8Array)) {
throw new OriginSqlError("import requires a Blob, ArrayBuffer, or Uint8Array");
}
if (intervalTimer) {
clearTimeout(intervalTimer);
intervalTimer = null;
}
pausedByAuth = false;
backoffFailures = 0;
if (inFlightSync) {
try {
await inFlightSync;
} catch {
}
}
let buffer;
if (blob instanceof Blob) buffer = await blob.arrayBuffer();
else if (blob instanceof Uint8Array) buffer = blob.slice().buffer;
else buffer = blob.slice(0);
const bytes = new Uint8Array(buffer);
await rpc.call("import-replace", { bytes }, [buffer]);
if (schema) {
try {
await rpc.call("exec", { sql: schema, params: [] });
} catch (err) {
throw new SchemaError(err.message, err);
}
}
if (sync) {
await rpc.call("sync:setup", {});
}
const { tables } = await rpc.call("sync:user-schema", {}).catch(() => ({ tables: [] }));
if (tables?.length) notifier.emit(tables.map((t) => t.name));
if (sync) {
await refreshPending();
if (sync.interval) scheduleIntervalTick();
}
}
async function close() {

@@ -585,7 +649,17 @@ if (closed) return;

}
return { exec, query, transaction, subscribe, sync: syncNow, onSyncStatus, close };
return {
exec,
query,
transaction,
subscribe,
sync: syncNow,
onSyncStatus,
export: exportDatabase,
import: importDatabase,
close
};
}
// src/bundle-entry.js
var WORKER_SOURCE = '/* origin-sql 0.1.0 \u2014 browser-side SQLite with optional libSQL sync */\n/* Homepage: https://github.com/ */\n/* License: MIT */\n\n// src/worker.js\nimport sqlite3InitModule from "https://esm.sh/@sqlite.org/sqlite-wasm@3.46.1-build3";\n\n// src/errors.js\nvar OriginSqlError = class extends Error {\n constructor(message, cause) {\n super(message);\n this.name = "OriginSqlError";\n if (cause !== void 0) this.cause = cause;\n }\n};\nvar SchemaError = class extends OriginSqlError {\n constructor(message, cause) {\n super(message, cause);\n this.name = "SchemaError";\n }\n};\n\n// src/rpc.js\nfunction serveRpc(port, handlers) {\n port.addEventListener("message", async (event) => {\n const { id, op, payload } = event.data ?? {};\n if (id == null || !op) return;\n const handler = handlers[op];\n if (!handler) {\n port.postMessage({ id, error: { message: `unknown op: ${op}` } });\n return;\n }\n try {\n const result = await handler(payload);\n port.postMessage({ id, result });\n } catch (err) {\n port.postMessage({\n id,\n error: { message: err?.message ?? String(err), name: err?.name }\n });\n }\n });\n}\n\n// src/sql-parse.js\nvar IDENT = \'(?:"(?:[^"]|"")+"|`(?:[^`]|``)+`|\\\\[[^\\\\]]+\\\\]|[\\\\w]+)\';\nvar QUALIFIED = `(?:${IDENT}\\\\s*\\\\.\\\\s*)?${IDENT}`;\nvar WRITE_PATTERNS = [\n new RegExp(`\\\\bINSERT\\\\s+(?:OR\\\\s+\\\\w+\\\\s+)?INTO\\\\s+(${QUALIFIED})`, "gi"),\n new RegExp(`\\\\bREPLACE\\\\s+INTO\\\\s+(${QUALIFIED})`, "gi"),\n new RegExp(`\\\\bUPDATE\\\\s+(?:OR\\\\s+\\\\w+\\\\s+)?(${QUALIFIED})\\\\s+SET`, "gi"),\n new RegExp(`\\\\bDELETE\\\\s+FROM\\\\s+(${QUALIFIED})`, "gi")\n];\nvar DDL_PATTERNS = [\n new RegExp(\n `\\\\b(?:CREATE|DROP|ALTER)\\\\s+TABLE\\\\s+(?:IF\\\\s+(?:NOT\\\\s+)?EXISTS\\\\s+)?(${QUALIFIED})`,\n "gi"\n )\n];\nvar COMMENT_BLOCK = /\\/\\*[\\s\\S]*?\\*\\//g;\nvar COMMENT_LINE = /--[^\\n]*/g;\nvar STRING_LITERAL = /\'(?:[^\']|\'\')*\'/g;\nfunction stripNoise(sql) {\n return sql.replace(COMMENT_BLOCK, " ").replace(COMMENT_LINE, " ").replace(STRING_LITERAL, "\'\'");\n}\nfunction unquote(ident) {\n const t = ident.trim();\n if (t.length >= 2) {\n const first = t[0];\n const last = t[t.length - 1];\n if (first === \'"\' && last === \'"\' || first === "`" && last === "`") {\n return t.slice(1, -1);\n }\n if (first === "[" && last === "]") return t.slice(1, -1);\n }\n return t;\n}\nfunction normalize(ident) {\n const stripped = unquote(ident);\n const parts = stripped.split(".").map(unquote);\n return parts[parts.length - 1].toLowerCase();\n}\nfunction extractTouchedTables(sql) {\n if (typeof sql !== "string" || sql.length === 0) return /* @__PURE__ */ new Set();\n const cleaned = stripNoise(sql);\n const tables = /* @__PURE__ */ new Set();\n for (const pattern of WRITE_PATTERNS) {\n pattern.lastIndex = 0;\n for (const match of cleaned.matchAll(pattern)) {\n tables.add(normalize(match[1]));\n }\n }\n return tables;\n}\nfunction isDdl(sql) {\n if (typeof sql !== "string" || sql.length === 0) return false;\n const cleaned = stripNoise(sql);\n return DDL_PATTERNS.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(cleaned);\n });\n}\nfunction extractDdlTables(sql) {\n if (typeof sql !== "string" || sql.length === 0) return /* @__PURE__ */ new Set();\n const cleaned = stripNoise(sql);\n const tables = /* @__PURE__ */ new Set();\n for (const pattern of DDL_PATTERNS) {\n pattern.lastIndex = 0;\n for (const match of cleaned.matchAll(pattern)) {\n tables.add(normalize(match[1]));\n }\n }\n return tables;\n}\n\n// src/sync-triggers.js\nvar SYNC_META_SCHEMA = `\nCREATE TABLE IF NOT EXISTS _sync_meta (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n table_name TEXT NOT NULL,\n row_id TEXT NOT NULL,\n op TEXT NOT NULL CHECK (op IN (\'I\',\'U\',\'D\')),\n changed_at INTEGER NOT NULL,\n synced_at INTEGER\n);\nCREATE INDEX IF NOT EXISTS _sync_meta_pending\n ON _sync_meta(seq) WHERE synced_at IS NULL;\nCREATE TABLE IF NOT EXISTS _sync_cursor (\n remote_url TEXT PRIMARY KEY,\n last_seen INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS _sync_suspend (\n id INTEGER PRIMARY KEY CHECK (id = 1)\n);\n`.trim();\nvar IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;\nfunction assertSafeIdent(name) {\n if (typeof name !== "string" || !IDENT_RE.test(name)) {\n throw new SchemaError(\n `sync is only supported for tables whose names match ${IDENT_RE} (got ${JSON.stringify(name)})`\n );\n }\n return name;\n}\nvar CHANGED_AT = `CAST(unixepoch(\'subsec\') * 1000 AS INTEGER)`;\nfunction insertMeta(table, rowRef, op) {\n return `INSERT INTO _sync_meta(table_name, row_id, op, changed_at) VALUES (\'${table}\', CAST(${rowRef}.rowid AS TEXT), \'${op}\', ${CHANGED_AT})`;\n}\nfunction triggerSql(tableName) {\n const t = assertSafeIdent(tableName);\n const suspended = `EXISTS (SELECT 1 FROM _sync_suspend)`;\n return [\n `CREATE TRIGGER IF NOT EXISTS "_sync_trg_${t}_ins" AFTER INSERT ON "${t}" WHEN NOT ${suspended} BEGIN ${insertMeta(t, "NEW", "I")}; END`,\n `CREATE TRIGGER IF NOT EXISTS "_sync_trg_${t}_upd" AFTER UPDATE ON "${t}" WHEN NOT ${suspended} BEGIN ${insertMeta(t, "NEW", "U")}; END`,\n `CREATE TRIGGER IF NOT EXISTS "_sync_trg_${t}_del" AFTER DELETE ON "${t}" WHEN NOT ${suspended} BEGIN ${insertMeta(t, "OLD", "D")}; END`\n ];\n}\nvar LIST_USER_TABLES_SQL = `SELECT name FROM sqlite_master WHERE type = \'table\' AND name NOT LIKE \'\\\\_sync\\\\_%\' ESCAPE \'\\\\\' AND name NOT LIKE \'sqlite\\\\_%\' ESCAPE \'\\\\\'`;\n\n// src/worker-core.js\nfunction installWorker(sqlite32, scope = self) {\n let poolUtilPromise;\n let db;\n let txnDepth = 0;\n let syncEnabled = false;\n function listUserTables() {\n return db.selectValues(LIST_USER_TABLES_SQL);\n }\n function installTriggersForAllUserTables() {\n for (const name of listUserTables()) {\n for (const stmt of triggerSql(name)) db.exec(stmt);\n }\n }\n function runBound(sql, params) {\n const s = db.prepare(sql);\n try {\n if (params && params.length) s.bind(params);\n while (s.step()) {\n }\n } finally {\n s.finalize();\n }\n }\n function selectAll(sql, params) {\n const s = db.prepare(sql);\n try {\n if (params && params.length) s.bind(params);\n const cols = s.getColumnNames();\n const out = [];\n while (s.step()) {\n const vals = s.get([]);\n const obj = {};\n for (let i = 0; i < cols.length; i++) obj[cols[i]] = vals[i];\n out.push(obj);\n }\n return out;\n } finally {\n s.finalize();\n }\n }\n async function ensureInit(name) {\n if (db) return;\n if (!poolUtilPromise) {\n poolUtilPromise = sqlite32.installOpfsSAHPoolVfs({ name: "origin-sql-pool" });\n }\n const poolUtil = await poolUtilPromise;\n db = new poolUtil.OpfsSAHPoolDb(`/${name}.sqlite`);\n db.exec("PRAGMA foreign_keys = ON");\n }\n function runQuery(sql, params) {\n const stmt = db.prepare(sql);\n try {\n if (params && params.length) stmt.bind(params);\n const columns = stmt.getColumnNames();\n const rows = [];\n while (stmt.step()) rows.push(stmt.get([]));\n return { columns, rows };\n } finally {\n stmt.finalize();\n }\n }\n function runExec(sql, params) {\n const stmt = db.prepare(sql);\n try {\n if (params && params.length) stmt.bind(params);\n while (stmt.step()) {\n }\n } finally {\n stmt.finalize();\n }\n const tables = extractTouchedTables(sql);\n if (isDdl(sql)) for (const t of extractDdlTables(sql)) tables.add(t);\n return {\n changes: db.changes(),\n lastInsertRowid: Number(sqlite32.capi.sqlite3_last_insert_rowid(db.pointer)),\n tables: Array.from(tables)\n };\n }\n const handlers = {\n async open({ name }) {\n await ensureInit(name);\n return { ok: true };\n },\n query({ sql, params }) {\n return runQuery(sql, params);\n },\n exec({ sql, params }) {\n const result = runExec(sql, params);\n if (syncEnabled && isDdl(sql)) installTriggersForAllUserTables();\n return result;\n },\n "sync:setup"() {\n if (!db) throw new Error("database not opened");\n if (!syncEnabled) {\n db.exec(SYNC_META_SCHEMA);\n syncEnabled = true;\n }\n installTriggersForAllUserTables();\n return { ok: true };\n },\n "sync:user-schema"() {\n return {\n tables: selectAll(\n LIST_USER_TABLES_SQL.replace("SELECT name", "SELECT name, sql")\n ).map((r) => ({ name: r.name, createSql: r.sql }))\n };\n },\n "sync:prepare-push"() {\n db.exec("BEGIN");\n try {\n const collapsed = selectAll(`\n SELECT table_name, row_id, op, changed_at, seq\n FROM _sync_meta m\n WHERE synced_at IS NULL\n AND seq = (\n SELECT MAX(seq) FROM _sync_meta\n WHERE table_name = m.table_name\n AND row_id = m.row_id\n AND synced_at IS NULL\n )\n ORDER BY seq ASC\n `);\n let watermark = 0;\n const ops = [];\n for (const row of collapsed) {\n if (row.seq > watermark) watermark = row.seq;\n if (row.op === "D") {\n ops.push({ table: row.table_name, rowId: row.row_id, op: "D", changedAt: row.changed_at });\n continue;\n }\n const live = selectAll(\n `SELECT rowid AS __rowid, * FROM "${row.table_name}" WHERE rowid = ?`,\n [row.row_id]\n );\n if (live.length) {\n ops.push({\n table: row.table_name,\n rowId: row.row_id,\n op: "U",\n changedAt: row.changed_at,\n payload: live[0]\n });\n } else {\n ops.push({ table: row.table_name, rowId: row.row_id, op: "D", changedAt: row.changed_at });\n }\n }\n db.exec("COMMIT");\n return { ops, watermark };\n } catch (e) {\n try {\n db.exec("ROLLBACK");\n } catch {\n }\n throw e;\n }\n },\n "sync:mark-synced"({ watermark, syncedAt }) {\n runBound(\n "UPDATE _sync_meta SET synced_at = ? WHERE seq <= ? AND synced_at IS NULL",\n [syncedAt, watermark]\n );\n return { ok: true };\n },\n "sync:cursor:get"({ url }) {\n const rows = selectAll(\n "SELECT last_seen FROM _sync_cursor WHERE remote_url = ?",\n [url]\n );\n return { lastSeen: rows[0]?.last_seen ?? 0 };\n },\n "sync:cursor:set"({ url, lastSeen }) {\n runBound(\n "INSERT OR REPLACE INTO _sync_cursor(remote_url, last_seen) VALUES (?, ?)",\n [url, lastSeen]\n );\n return { ok: true };\n },\n "sync:pending-count"() {\n const rows = selectAll(\n "SELECT COUNT(*) AS n FROM _sync_meta WHERE synced_at IS NULL",\n []\n );\n return { count: rows[0]?.n ?? 0 };\n },\n "sync:apply"({ ops, cursorUrl, cursorLastSeen }) {\n db.exec("BEGIN");\n try {\n runBound("INSERT OR IGNORE INTO _sync_suspend(id) VALUES (1)", []);\n for (const op of ops) {\n if (op.op === "D") {\n runBound(`DELETE FROM "${op.table}" WHERE rowid = ?`, [op.rowId]);\n } else {\n const cols = Object.keys(op.payload).filter((k) => k !== "__rowid");\n const values = cols.map((k) => op.payload[k]);\n const colList = cols.map((c) => `"${c}"`).join(", ");\n const phList = ["?", ...cols.map(() => "?")].join(", ");\n runBound(\n `INSERT OR REPLACE INTO "${op.table}" (rowid, ${colList}) VALUES (${phList})`,\n [op.rowId, ...values]\n );\n }\n }\n db.exec("DELETE FROM _sync_suspend");\n if (cursorUrl && cursorLastSeen != null) {\n runBound(\n "INSERT OR REPLACE INTO _sync_cursor(remote_url, last_seen) VALUES (?, ?)",\n [cursorUrl, cursorLastSeen]\n );\n }\n db.exec("COMMIT");\n return { applied: ops.length };\n } catch (e) {\n try {\n db.exec("ROLLBACK");\n } catch {\n }\n throw e;\n }\n },\n "txn:begin"() {\n if (txnDepth > 0) throw new Error("nested transactions are not supported in v1");\n db.exec("BEGIN");\n txnDepth = 1;\n return { ok: true };\n },\n "txn:commit"() {\n if (txnDepth === 0) throw new Error("no active transaction");\n db.exec("COMMIT");\n txnDepth = 0;\n return { ok: true };\n },\n "txn:rollback"() {\n if (txnDepth === 0) return { ok: true };\n db.exec("ROLLBACK");\n txnDepth = 0;\n return { ok: true };\n },\n close() {\n if (db) {\n if (txnDepth > 0) {\n try {\n db.exec("ROLLBACK");\n } catch {\n }\n txnDepth = 0;\n }\n db.close();\n db = null;\n }\n return { ok: true };\n }\n };\n serveRpc(scope, handlers);\n scope.postMessage({ id: 0, event: "ready" });\n}\n\n// src/worker.js\nvar sqlite3 = await sqlite3InitModule({\n print: () => {\n },\n printErr: (...args) => console.error("[origin-sql]", ...args)\n});\ninstallWorker(sqlite3);\n';
var WORKER_SOURCE = '/* origin-sql 0.2.0 \u2014 browser-side SQLite with optional libSQL sync */\n/* Homepage: https://github.com/ */\n/* License: MIT */\n\n// src/worker.js\nimport sqlite3InitModule from "https://esm.sh/@sqlite.org/sqlite-wasm@3.46.1-build3";\n\n// src/errors.js\nvar OriginSqlError = class extends Error {\n constructor(message, cause) {\n super(message);\n this.name = "OriginSqlError";\n if (cause !== void 0) this.cause = cause;\n }\n};\nvar SchemaError = class extends OriginSqlError {\n constructor(message, cause) {\n super(message, cause);\n this.name = "SchemaError";\n }\n};\n\n// src/rpc.js\nvar TRANSFER = /* @__PURE__ */ Symbol.for("origin-sql.rpc.transfer");\nfunction withTransfer(value, transfer) {\n return { [TRANSFER]: transfer, value };\n}\nfunction serveRpc(port, handlers) {\n port.addEventListener("message", async (event) => {\n const { id, op, payload } = event.data ?? {};\n if (id == null || !op) return;\n const handler = handlers[op];\n if (!handler) {\n port.postMessage({ id, error: { message: `unknown op: ${op}` } });\n return;\n }\n try {\n const result = await handler(payload);\n if (result && typeof result === "object" && TRANSFER in result) {\n port.postMessage({ id, result: result.value }, result[TRANSFER]);\n } else {\n port.postMessage({ id, result });\n }\n } catch (err) {\n port.postMessage({\n id,\n error: { message: err?.message ?? String(err), name: err?.name }\n });\n }\n });\n}\n\n// src/sql-parse.js\nvar IDENT = \'(?:"(?:[^"]|"")+"|`(?:[^`]|``)+`|\\\\[[^\\\\]]+\\\\]|[\\\\w]+)\';\nvar QUALIFIED = `(?:${IDENT}\\\\s*\\\\.\\\\s*)?${IDENT}`;\nvar WRITE_PATTERNS = [\n new RegExp(`\\\\bINSERT\\\\s+(?:OR\\\\s+\\\\w+\\\\s+)?INTO\\\\s+(${QUALIFIED})`, "gi"),\n new RegExp(`\\\\bREPLACE\\\\s+INTO\\\\s+(${QUALIFIED})`, "gi"),\n new RegExp(`\\\\bUPDATE\\\\s+(?:OR\\\\s+\\\\w+\\\\s+)?(${QUALIFIED})\\\\s+SET`, "gi"),\n new RegExp(`\\\\bDELETE\\\\s+FROM\\\\s+(${QUALIFIED})`, "gi")\n];\nvar DDL_PATTERNS = [\n new RegExp(\n `\\\\b(?:CREATE|DROP|ALTER)\\\\s+TABLE\\\\s+(?:IF\\\\s+(?:NOT\\\\s+)?EXISTS\\\\s+)?(${QUALIFIED})`,\n "gi"\n )\n];\nvar COMMENT_BLOCK = /\\/\\*[\\s\\S]*?\\*\\//g;\nvar COMMENT_LINE = /--[^\\n]*/g;\nvar STRING_LITERAL = /\'(?:[^\']|\'\')*\'/g;\nfunction stripNoise(sql) {\n return sql.replace(COMMENT_BLOCK, " ").replace(COMMENT_LINE, " ").replace(STRING_LITERAL, "\'\'");\n}\nfunction unquote(ident) {\n const t = ident.trim();\n if (t.length >= 2) {\n const first = t[0];\n const last = t[t.length - 1];\n if (first === \'"\' && last === \'"\' || first === "`" && last === "`") {\n return t.slice(1, -1);\n }\n if (first === "[" && last === "]") return t.slice(1, -1);\n }\n return t;\n}\nfunction normalize(ident) {\n const stripped = unquote(ident);\n const parts = stripped.split(".").map(unquote);\n return parts[parts.length - 1].toLowerCase();\n}\nfunction extractTouchedTables(sql) {\n if (typeof sql !== "string" || sql.length === 0) return /* @__PURE__ */ new Set();\n const cleaned = stripNoise(sql);\n const tables = /* @__PURE__ */ new Set();\n for (const pattern of WRITE_PATTERNS) {\n pattern.lastIndex = 0;\n for (const match of cleaned.matchAll(pattern)) {\n tables.add(normalize(match[1]));\n }\n }\n return tables;\n}\nfunction isDdl(sql) {\n if (typeof sql !== "string" || sql.length === 0) return false;\n const cleaned = stripNoise(sql);\n return DDL_PATTERNS.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(cleaned);\n });\n}\nfunction extractDdlTables(sql) {\n if (typeof sql !== "string" || sql.length === 0) return /* @__PURE__ */ new Set();\n const cleaned = stripNoise(sql);\n const tables = /* @__PURE__ */ new Set();\n for (const pattern of DDL_PATTERNS) {\n pattern.lastIndex = 0;\n for (const match of cleaned.matchAll(pattern)) {\n tables.add(normalize(match[1]));\n }\n }\n return tables;\n}\n\n// src/sync-triggers.js\nvar SYNC_META_SCHEMA = `\nCREATE TABLE IF NOT EXISTS _sync_meta (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n table_name TEXT NOT NULL,\n row_id TEXT NOT NULL,\n op TEXT NOT NULL CHECK (op IN (\'I\',\'U\',\'D\')),\n changed_at INTEGER NOT NULL,\n synced_at INTEGER\n);\nCREATE INDEX IF NOT EXISTS _sync_meta_pending\n ON _sync_meta(seq) WHERE synced_at IS NULL;\nCREATE TABLE IF NOT EXISTS _sync_cursor (\n remote_url TEXT PRIMARY KEY,\n last_seen INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS _sync_suspend (\n id INTEGER PRIMARY KEY CHECK (id = 1)\n);\n`.trim();\nvar IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;\nfunction assertSafeIdent(name) {\n if (typeof name !== "string" || !IDENT_RE.test(name)) {\n throw new SchemaError(\n `sync is only supported for tables whose names match ${IDENT_RE} (got ${JSON.stringify(name)})`\n );\n }\n return name;\n}\nvar CHANGED_AT = `CAST(unixepoch(\'subsec\') * 1000 AS INTEGER)`;\nfunction insertMeta(table, rowRef, op) {\n return `INSERT INTO _sync_meta(table_name, row_id, op, changed_at) VALUES (\'${table}\', CAST(${rowRef}.rowid AS TEXT), \'${op}\', ${CHANGED_AT})`;\n}\nfunction triggerSql(tableName) {\n const t = assertSafeIdent(tableName);\n const suspended = `EXISTS (SELECT 1 FROM _sync_suspend)`;\n return [\n `CREATE TRIGGER IF NOT EXISTS "_sync_trg_${t}_ins" AFTER INSERT ON "${t}" WHEN NOT ${suspended} BEGIN ${insertMeta(t, "NEW", "I")}; END`,\n `CREATE TRIGGER IF NOT EXISTS "_sync_trg_${t}_upd" AFTER UPDATE ON "${t}" WHEN NOT ${suspended} BEGIN ${insertMeta(t, "NEW", "U")}; END`,\n `CREATE TRIGGER IF NOT EXISTS "_sync_trg_${t}_del" AFTER DELETE ON "${t}" WHEN NOT ${suspended} BEGIN ${insertMeta(t, "OLD", "D")}; END`\n ];\n}\nvar LIST_USER_TABLES_SQL = `SELECT name FROM sqlite_master WHERE type = \'table\' AND name NOT LIKE \'\\\\_sync\\\\_%\' ESCAPE \'\\\\\' AND name NOT LIKE \'sqlite\\\\_%\' ESCAPE \'\\\\\'`;\n\n// src/worker-core.js\nfunction installWorker(sqlite32, scope = self) {\n let poolUtilPromise;\n let poolUtil;\n let db;\n let dbPath;\n let txnDepth = 0;\n let syncEnabled = false;\n function listUserTables() {\n return db.selectValues(LIST_USER_TABLES_SQL);\n }\n function installTriggersForAllUserTables() {\n for (const name of listUserTables()) {\n for (const stmt of triggerSql(name)) db.exec(stmt);\n }\n }\n function runBound(sql, params) {\n const s = db.prepare(sql);\n try {\n if (params && params.length) s.bind(params);\n while (s.step()) {\n }\n } finally {\n s.finalize();\n }\n }\n function selectAll(sql, params) {\n const s = db.prepare(sql);\n try {\n if (params && params.length) s.bind(params);\n const cols = s.getColumnNames();\n const out = [];\n while (s.step()) {\n const vals = s.get([]);\n const obj = {};\n for (let i = 0; i < cols.length; i++) obj[cols[i]] = vals[i];\n out.push(obj);\n }\n return out;\n } finally {\n s.finalize();\n }\n }\n async function ensureInit(name) {\n if (db) return;\n if (!poolUtilPromise) {\n poolUtilPromise = sqlite32.installOpfsSAHPoolVfs({ name: "origin-sql-pool" });\n }\n poolUtil = await poolUtilPromise;\n dbPath = `/${name}.sqlite`;\n db = new poolUtil.OpfsSAHPoolDb(dbPath);\n db.exec("PRAGMA foreign_keys = ON");\n }\n function closeDbHandle() {\n if (!db) return;\n if (txnDepth > 0) {\n try {\n db.exec("ROLLBACK");\n } catch {\n }\n txnDepth = 0;\n }\n db.close();\n db = null;\n }\n function runQuery(sql, params) {\n const stmt = db.prepare(sql);\n try {\n if (params && params.length) stmt.bind(params);\n const columns = stmt.getColumnNames();\n const rows = [];\n while (stmt.step()) rows.push(stmt.get([]));\n return { columns, rows };\n } finally {\n stmt.finalize();\n }\n }\n function runExec(sql, params) {\n const stmt = db.prepare(sql);\n try {\n if (params && params.length) stmt.bind(params);\n while (stmt.step()) {\n }\n } finally {\n stmt.finalize();\n }\n const tables = extractTouchedTables(sql);\n if (isDdl(sql)) for (const t of extractDdlTables(sql)) tables.add(t);\n return {\n changes: db.changes(),\n lastInsertRowid: Number(sqlite32.capi.sqlite3_last_insert_rowid(db.pointer)),\n tables: Array.from(tables)\n };\n }\n const handlers = {\n async open({ name }) {\n await ensureInit(name);\n return { ok: true };\n },\n query({ sql, params }) {\n return runQuery(sql, params);\n },\n exec({ sql, params }) {\n const result = runExec(sql, params);\n if (syncEnabled && isDdl(sql)) installTriggersForAllUserTables();\n return result;\n },\n "sync:setup"() {\n if (!db) throw new Error("database not opened");\n if (!syncEnabled) {\n db.exec(SYNC_META_SCHEMA);\n syncEnabled = true;\n }\n installTriggersForAllUserTables();\n return { ok: true };\n },\n "sync:user-schema"() {\n return {\n tables: selectAll(\n LIST_USER_TABLES_SQL.replace("SELECT name", "SELECT name, sql")\n ).map((r) => ({ name: r.name, createSql: r.sql }))\n };\n },\n "sync:prepare-push"() {\n db.exec("BEGIN");\n try {\n const collapsed = selectAll(`\n SELECT table_name, row_id, op, changed_at, seq\n FROM _sync_meta m\n WHERE synced_at IS NULL\n AND seq = (\n SELECT MAX(seq) FROM _sync_meta\n WHERE table_name = m.table_name\n AND row_id = m.row_id\n AND synced_at IS NULL\n )\n ORDER BY seq ASC\n `);\n let watermark = 0;\n const ops = [];\n for (const row of collapsed) {\n if (row.seq > watermark) watermark = row.seq;\n if (row.op === "D") {\n ops.push({ table: row.table_name, rowId: row.row_id, op: "D", changedAt: row.changed_at });\n continue;\n }\n const live = selectAll(\n `SELECT rowid AS __rowid, * FROM "${row.table_name}" WHERE rowid = ?`,\n [row.row_id]\n );\n if (live.length) {\n ops.push({\n table: row.table_name,\n rowId: row.row_id,\n op: "U",\n changedAt: row.changed_at,\n payload: live[0]\n });\n } else {\n ops.push({ table: row.table_name, rowId: row.row_id, op: "D", changedAt: row.changed_at });\n }\n }\n db.exec("COMMIT");\n return { ops, watermark };\n } catch (e) {\n try {\n db.exec("ROLLBACK");\n } catch {\n }\n throw e;\n }\n },\n "sync:mark-synced"({ watermark, syncedAt }) {\n runBound(\n "UPDATE _sync_meta SET synced_at = ? WHERE seq <= ? AND synced_at IS NULL",\n [syncedAt, watermark]\n );\n return { ok: true };\n },\n "sync:cursor:get"({ url }) {\n const rows = selectAll(\n "SELECT last_seen FROM _sync_cursor WHERE remote_url = ?",\n [url]\n );\n return { lastSeen: rows[0]?.last_seen ?? 0 };\n },\n "sync:cursor:set"({ url, lastSeen }) {\n runBound(\n "INSERT OR REPLACE INTO _sync_cursor(remote_url, last_seen) VALUES (?, ?)",\n [url, lastSeen]\n );\n return { ok: true };\n },\n "sync:pending-count"() {\n const rows = selectAll(\n "SELECT COUNT(*) AS n FROM _sync_meta WHERE synced_at IS NULL",\n []\n );\n return { count: rows[0]?.n ?? 0 };\n },\n "sync:apply"({ ops, cursorUrl, cursorLastSeen }) {\n db.exec("BEGIN");\n try {\n runBound("INSERT OR IGNORE INTO _sync_suspend(id) VALUES (1)", []);\n for (const op of ops) {\n if (op.op === "D") {\n runBound(`DELETE FROM "${op.table}" WHERE rowid = ?`, [op.rowId]);\n } else {\n const cols = Object.keys(op.payload).filter((k) => k !== "__rowid");\n const values = cols.map((k) => op.payload[k]);\n const colList = cols.map((c) => `"${c}"`).join(", ");\n const phList = ["?", ...cols.map(() => "?")].join(", ");\n runBound(\n `INSERT OR REPLACE INTO "${op.table}" (rowid, ${colList}) VALUES (${phList})`,\n [op.rowId, ...values]\n );\n }\n }\n db.exec("DELETE FROM _sync_suspend");\n if (cursorUrl && cursorLastSeen != null) {\n runBound(\n "INSERT OR REPLACE INTO _sync_cursor(remote_url, last_seen) VALUES (?, ?)",\n [cursorUrl, cursorLastSeen]\n );\n }\n db.exec("COMMIT");\n return { applied: ops.length };\n } catch (e) {\n try {\n db.exec("ROLLBACK");\n } catch {\n }\n throw e;\n }\n },\n async "export-file"() {\n if (!db || !poolUtil || !dbPath) throw new Error("database not opened");\n if (txnDepth > 0) throw new Error("cannot export during a transaction");\n const bytes = await poolUtil.exportFile(dbPath);\n return withTransfer(bytes, [bytes.buffer]);\n },\n async "import-replace"({ bytes }) {\n if (!poolUtil || !dbPath) throw new Error("database not opened");\n if (txnDepth > 0) throw new Error("cannot import during a transaction");\n closeDbHandle();\n try {\n await poolUtil.importDb(dbPath, bytes);\n } catch (err) {\n db = new poolUtil.OpfsSAHPoolDb(dbPath);\n db.exec("PRAGMA foreign_keys = ON");\n throw err;\n }\n db = new poolUtil.OpfsSAHPoolDb(dbPath);\n db.exec("PRAGMA foreign_keys = ON");\n syncEnabled = false;\n return { ok: true };\n },\n "txn:begin"() {\n if (txnDepth > 0) throw new Error("nested transactions are not supported in v1");\n db.exec("BEGIN");\n txnDepth = 1;\n return { ok: true };\n },\n "txn:commit"() {\n if (txnDepth === 0) throw new Error("no active transaction");\n db.exec("COMMIT");\n txnDepth = 0;\n return { ok: true };\n },\n "txn:rollback"() {\n if (txnDepth === 0) return { ok: true };\n db.exec("ROLLBACK");\n txnDepth = 0;\n return { ok: true };\n },\n close() {\n closeDbHandle();\n return { ok: true };\n }\n };\n serveRpc(scope, handlers);\n scope.postMessage({ id: 0, event: "ready" });\n}\n\n// src/worker.js\nvar sqlite3 = await sqlite3InitModule({\n print: () => {\n },\n printErr: (...args) => console.error("[origin-sql]", ...args)\n});\ninstallWorker(sqlite3);\n';
var workerBlobUrl = null;

@@ -592,0 +666,0 @@ function getWorkerUrl() {

+50
-15

@@ -1,2 +0,2 @@

/* origin-sql 0.1.0 — browser-side SQLite with optional libSQL sync */
/* origin-sql 0.2.0 — browser-side SQLite with optional libSQL sync */
/* Homepage: https://github.com/ */

@@ -24,2 +24,6 @@ /* License: MIT */

// src/rpc.js
var TRANSFER = /* @__PURE__ */ Symbol.for("origin-sql.rpc.transfer");
function withTransfer(value, transfer) {
return { [TRANSFER]: transfer, value };
}
function serveRpc(port, handlers) {

@@ -36,3 +40,7 @@ port.addEventListener("message", async (event) => {

const result = await handler(payload);
port.postMessage({ id, result });
if (result && typeof result === "object" && TRANSFER in result) {
port.postMessage({ id, result: result.value }, result[TRANSFER]);
} else {
port.postMessage({ id, result });
}
} catch (err) {

@@ -165,3 +173,5 @@ port.postMessage({

let poolUtilPromise;
let poolUtil;
let db;
let dbPath;
let txnDepth = 0;

@@ -209,6 +219,19 @@ let syncEnabled = false;

}
const poolUtil = await poolUtilPromise;
db = new poolUtil.OpfsSAHPoolDb(`/${name}.sqlite`);
poolUtil = await poolUtilPromise;
dbPath = `/${name}.sqlite`;
db = new poolUtil.OpfsSAHPoolDb(dbPath);
db.exec("PRAGMA foreign_keys = ON");
}
function closeDbHandle() {
if (!db) return;
if (txnDepth > 0) {
try {
db.exec("ROLLBACK");
} catch {
}
txnDepth = 0;
}
db.close();
db = null;
}
function runQuery(sql, params) {

@@ -384,2 +407,24 @@ const stmt = db.prepare(sql);

},
async "export-file"() {
if (!db || !poolUtil || !dbPath) throw new Error("database not opened");
if (txnDepth > 0) throw new Error("cannot export during a transaction");
const bytes = await poolUtil.exportFile(dbPath);
return withTransfer(bytes, [bytes.buffer]);
},
async "import-replace"({ bytes }) {
if (!poolUtil || !dbPath) throw new Error("database not opened");
if (txnDepth > 0) throw new Error("cannot import during a transaction");
closeDbHandle();
try {
await poolUtil.importDb(dbPath, bytes);
} catch (err) {
db = new poolUtil.OpfsSAHPoolDb(dbPath);
db.exec("PRAGMA foreign_keys = ON");
throw err;
}
db = new poolUtil.OpfsSAHPoolDb(dbPath);
db.exec("PRAGMA foreign_keys = ON");
syncEnabled = false;
return { ok: true };
},
"txn:begin"() {

@@ -404,13 +449,3 @@ if (txnDepth > 0) throw new Error("nested transactions are not supported in v1");

close() {
if (db) {
if (txnDepth > 0) {
try {
db.exec("ROLLBACK");
} catch {
}
txnDepth = 0;
}
db.close();
db = null;
}
closeDbHandle();
return { ok: true };

@@ -417,0 +452,0 @@ }

{
"name": "@remy/origin-sql",
"version": "0.1.0",
"version": "0.2.0",
"description": "Browser-side SQLite with OPFS persistence and optional libSQL sync",

@@ -5,0 +5,0 @@ "type": "module",

@@ -103,2 +103,4 @@ # origin-sql

- `db.onSyncStatus(cb): () => void` — subscribe to sync state transitions. `cb` is called synchronously once at registration and on every transition with `{ state: 'idle'|'syncing'|'error', pendingPush, lastSyncedAt, lastError }`. Returns an unsubscribe.
- `db.export(): Promise<Blob>` — dump the entire SQLite file as a `Blob` tagged `application/vnd.sqlite3`. Useful for user-initiated backups or "download my data" flows. See [Backup and restore](#backup-and-restore).
- `db.import(source): Promise<void>` — replace the entire database with the contents of `source` (a `Blob`, `ArrayBuffer`, or `Uint8Array`). The handle stays usable: the schema is re-applied and sync is re-initialised if configured. Throws if a transaction is in flight.
- `db.close(): Promise<void>` — flushes, terminates the worker, rejects pending calls, cancels any sync interval.

@@ -118,2 +120,32 @@

### Backup and restore
`db.export()` hands you the whole database as a standard SQLite file, suitable for saving to disk, uploading to S3, or round-tripping through `sqlite3` on the command line:
```js
const blob = await db.export();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'my-app.sqlite';
a.click();
URL.revokeObjectURL(url);
```
`db.import(blob)` replaces the current database with an uploaded file. The in-memory `db` handle stays valid — schema and sync setup are re-applied automatically — so the usual pattern is a single file input:
```js
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
await db.import(file); // file is a Blob
// subscribers fire for every user table; re-render your UI
});
```
Notes:
- Import rejects if a transaction is in flight. Await any `db.transaction(...)` first.
- When `sync` is configured, importing effectively "forks" the local clone. The imported file's `_sync_meta` state governs what gets pushed next; rows from a non-sync export won't be flagged for push. Expect the next `db.sync()` to behave as if the imported data had always been local.
- The exported file contains every table, including the `_sync_*` internal tables when sync is configured.
### Known limitations

@@ -212,2 +244,57 @@

## Recipes
Things that are trivial in user-land and therefore deliberately not part of the core API.
### Export as JSON
`db.export()` returns a binary SQLite file — the universal, lossless format. When you need a JSON dump instead (product requirement to "download as JSON", uploading to a JSON-native endpoint, diffable snapshots in tests), iterate `sqlite_master` yourself:
```js
async function exportJson(db) {
const tables = await db.query(
`SELECT name FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'
AND name NOT LIKE '\\_sync\\_%' ESCAPE '\\'`,
);
const dump = { version: 1, exportedAt: new Date().toISOString(), tables: {} };
for (const { name } of tables) {
dump.tables[name] = await db.query(`SELECT * FROM "${name}"`);
}
return new Blob([JSON.stringify(dump, null, 2)], { type: 'application/json' });
}
```
Caveats you own when you do this:
- **BLOB columns** need base64 (`btoa(String.fromCharCode(...row.col))`) or they'll JSON-stringify to `{}`.
- **Integers past `Number.MAX_SAFE_INTEGER`** lose precision — cast to TEXT in the query (`CAST(big_id AS TEXT) AS big_id`) and parse client-side.
- **Schema is not preserved.** Types, constraints, indexes, triggers, and views are gone. Re-importing requires you to recreate the schema separately.
- **Not round-trip-symmetric with `db.import()`.** Use `db.export()` for true backup/restore; use JSON for "send a copy of my data" and similar one-way flows.
### Restore from JSON
The mirror of above — rebuild a database from a JSON dump:
```js
async function importJson(db, dump) {
await db.transaction(async (tx) => {
for (const [table, rows] of Object.entries(dump.tables)) {
if (!rows.length) continue;
const cols = Object.keys(rows[0]);
const placeholders = cols.map(() => '?').join(', ');
const colList = cols.map((c) => `"${c}"`).join(', ');
for (const row of rows) {
await tx.exec(
`INSERT OR REPLACE INTO "${table}" (${colList}) VALUES (${placeholders})`,
cols.map((c) => row[c]),
);
}
}
});
}
```
Assumes the tables already exist (run your `schema` first, or pass it when opening the DB).
## Running the notes demo

@@ -214,0 +301,0 @@

@@ -145,2 +145,3 @@ import { createRpc } from './rpc.js';

let intervalTimer = null;
let inFlightSync = null;

@@ -171,21 +172,29 @@ function deliverStatus() {

await setStatus({ state: 'syncing', lastError: undefined });
const promise = (async () => {
try {
const result = await syncer.sync();
if (result.pull?.pulled > 0) {
const { tables } = await rpc.call('sync:user-schema', {});
notifier.emit(tables.map((t) => t.name));
}
backoffFailures = 0;
pausedByAuth = false;
await setStatus({
state: 'idle',
lastSyncedAt: Date.now(),
lastError: undefined,
});
return result;
} catch (err) {
if (err instanceof AuthError) pausedByAuth = true;
else backoffFailures++;
await setStatus({ state: 'error', lastError: err });
throw err;
}
})();
inFlightSync = promise;
try {
const result = await syncer.sync();
if (result.pull?.pulled > 0) {
const { tables } = await rpc.call('sync:user-schema', {});
notifier.emit(tables.map((t) => t.name));
}
backoffFailures = 0;
pausedByAuth = false;
await setStatus({
state: 'idle',
lastSyncedAt: Date.now(),
lastError: undefined,
});
return result;
} catch (err) {
if (err instanceof AuthError) pausedByAuth = true;
else backoffFailures++;
await setStatus({ state: 'error', lastError: err });
throw err;
return await promise;
} finally {
if (inFlightSync === promise) inFlightSync = null;
}

@@ -242,2 +251,48 @@ }

async function exportDatabase() {
assertOpen();
if (inFlightSync) { try { await inFlightSync; } catch {} }
const bytes = await rpc.call('export-file', {});
return new Blob([bytes], { type: 'application/vnd.sqlite3' });
}
async function importDatabase(blob) {
assertOpen();
if (!(blob instanceof Blob) && !(blob instanceof ArrayBuffer) && !(blob instanceof Uint8Array)) {
throw new OriginSqlError('import requires a Blob, ArrayBuffer, or Uint8Array');
}
if (intervalTimer) { clearTimeout(intervalTimer); intervalTimer = null; }
pausedByAuth = false;
backoffFailures = 0;
if (inFlightSync) { try { await inFlightSync; } catch {} }
let buffer;
if (blob instanceof Blob) buffer = await blob.arrayBuffer();
else if (blob instanceof Uint8Array) buffer = blob.slice().buffer;
else buffer = blob.slice(0);
const bytes = new Uint8Array(buffer);
await rpc.call('import-replace', { bytes }, [buffer]);
if (schema) {
try {
await rpc.call('exec', { sql: schema, params: [] });
} catch (err) {
throw new SchemaError(err.message, err);
}
}
if (sync) {
await rpc.call('sync:setup', {});
}
// Imported data can touch any table — fan out to all subscribers.
const { tables } = await rpc.call('sync:user-schema', {}).catch(() => ({ tables: [] }));
if (tables?.length) notifier.emit(tables.map((t) => t.name));
if (sync) {
await refreshPending();
if (sync.interval) scheduleIntervalTick();
}
}
async function close() {

@@ -254,3 +309,13 @@ if (closed) return;

return { exec, query, transaction, subscribe, sync: syncNow, onSyncStatus, close };
return {
exec,
query,
transaction,
subscribe,
sync: syncNow,
onSyncStatus,
export: exportDatabase,
import: importDatabase,
close,
};
}
import { RpcError } from './errors.js';
const TRANSFER = Symbol.for('origin-sql.rpc.transfer');
export function withTransfer(value, transfer) {
return { [TRANSFER]: transfer, value };
}
export function createRpc(port) {

@@ -20,7 +26,11 @@ const pending = new Map();

function call(op, payload) {
function call(op, payload, transfer) {
const id = nextId++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
port.postMessage({ id, op, payload });
if (transfer && transfer.length) {
port.postMessage({ id, op, payload }, transfer);
} else {
port.postMessage({ id, op, payload });
}
});

@@ -49,3 +59,7 @@ }

const result = await handler(payload);
port.postMessage({ id, result });
if (result && typeof result === 'object' && TRANSFER in result) {
port.postMessage({ id, result: result.value }, result[TRANSFER]);
} else {
port.postMessage({ id, result });
}
} catch (err) {

@@ -52,0 +66,0 @@ port.postMessage({

@@ -1,2 +0,2 @@

import { serveRpc } from './rpc.js';
import { serveRpc, withTransfer } from './rpc.js';
import { extractTouchedTables, isDdl, extractDdlTables } from './sql-parse.js';

@@ -11,3 +11,5 @@ import {

let poolUtilPromise;
let poolUtil;
let db;
let dbPath;
let txnDepth = 0;

@@ -59,7 +61,18 @@ let syncEnabled = false;

}
const poolUtil = await poolUtilPromise;
db = new poolUtil.OpfsSAHPoolDb(`/${name}.sqlite`);
poolUtil = await poolUtilPromise;
dbPath = `/${name}.sqlite`;
db = new poolUtil.OpfsSAHPoolDb(dbPath);
db.exec('PRAGMA foreign_keys = ON');
}
function closeDbHandle() {
if (!db) return;
if (txnDepth > 0) {
try { db.exec('ROLLBACK'); } catch {}
txnDepth = 0;
}
db.close();
db = null;
}
function runQuery(sql, params) {

@@ -230,2 +243,26 @@ const stmt = db.prepare(sql);

},
async 'export-file'() {
if (!db || !poolUtil || !dbPath) throw new Error('database not opened');
if (txnDepth > 0) throw new Error('cannot export during a transaction');
const bytes = await poolUtil.exportFile(dbPath);
return withTransfer(bytes, [bytes.buffer]);
},
async 'import-replace'({ bytes }) {
if (!poolUtil || !dbPath) throw new Error('database not opened');
if (txnDepth > 0) throw new Error('cannot import during a transaction');
closeDbHandle();
try {
await poolUtil.importDb(dbPath, bytes);
} catch (err) {
// Pool may have released the slot; reopen a fresh handle on whatever
// is on disk (possibly the pre-import file) so the db stays usable.
db = new poolUtil.OpfsSAHPoolDb(dbPath);
db.exec('PRAGMA foreign_keys = ON');
throw err;
}
db = new poolUtil.OpfsSAHPoolDb(dbPath);
db.exec('PRAGMA foreign_keys = ON');
syncEnabled = false;
return { ok: true };
},
'txn:begin'() {

@@ -250,10 +287,3 @@ if (txnDepth > 0) throw new Error('nested transactions are not supported in v1');

close() {
if (db) {
if (txnDepth > 0) {
try { db.exec('ROLLBACK'); } catch {}
txnDepth = 0;
}
db.close();
db = null;
}
closeDbHandle();
return { ok: true };

@@ -260,0 +290,0 @@ },