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.2.1
to
0.3.0
+46
-4
dist/origin-sql.bundle.js

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

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

@@ -384,2 +384,5 @@ /* License: MIT */

}
if (sync.syncOnMutation !== void 0 && typeof sync.syncOnMutation !== "boolean") {
throw new OriginSqlError("sync.syncOnMutation must be a boolean");
}
}

@@ -417,3 +420,6 @@ const worker = new Worker(workerUrl ?? WORKER_URL, { type: "module" });

const result = await rpc.call("exec", { sql, params });
if (result.tables?.length) notifier.emit(result.tables);
if (result.tables?.length) {
notifier.emit(result.tables);
triggerAutoSync();
}
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };

@@ -450,3 +456,6 @@ }

await rpc.call("txn:commit", {});
if (touched.size) notifier.emit(touched);
if (touched.size) {
notifier.emit(touched);
triggerAutoSync();
}
return value;

@@ -545,2 +554,35 @@ } catch (err) {

}
let autoSyncRunning = false;
let trailingAutoSyncScheduled = false;
async function autoSyncLoop() {
autoSyncRunning = true;
try {
do {
trailingAutoSyncScheduled = false;
while (inFlightSync) {
try {
await inFlightSync;
} catch {
}
}
if (closed || pausedByAuth) break;
try {
await runSyncOnce();
} catch {
}
} while (trailingAutoSyncScheduled && !closed && !pausedByAuth);
} finally {
autoSyncRunning = false;
}
}
function triggerAutoSync() {
if (!syncer || !sync?.syncOnMutation) return;
if (closed || pausedByAuth) return;
if (autoSyncRunning) {
trailingAutoSyncScheduled = true;
return;
}
autoSyncLoop().catch(() => {
});
}
async function syncNow() {

@@ -663,3 +705,3 @@ assertOpen();

// src/bundle-entry.js
var WORKER_SOURCE = '/* origin-sql 0.2.1 \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 WORKER_SOURCE = '/* origin-sql 0.3.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;

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

+1
-1

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

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

@@ -3,0 +3,0 @@ /* License: MIT */

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

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

@@ -114,2 +114,3 @@ # origin-sql

interval: 15_000, // optional; tick every N ms with exponential backoff on error
syncOnMutation: true, // optional; kick off a background sync after every successful exec/committed transaction
}

@@ -120,2 +121,4 @@ ```

With `syncOnMutation: true`, every successful `exec` and committed `transaction` kicks off a fire-and-forget sync. Rapid mutations while a sync is already running coalesce into a single trailing sync — you won't get one network round-trip per insert. The mutation promise does **not** wait for the sync; check `onSyncStatus` if you need to know when remote convergence has happened. Auto-sync is suppressed while `AuthError` has paused the scheduler — resume it by calling `db.sync()` with a fresh token. Combine with `interval` if you also want periodic pulls of remote changes (the auto-sync covers your local writes, the interval covers "someone else wrote on another device").
### Backup and restore

@@ -122,0 +125,0 @@

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

}
if (sync.syncOnMutation !== undefined && typeof sync.syncOnMutation !== 'boolean') {
throw new OriginSqlError('sync.syncOnMutation must be a boolean');
}
}

@@ -82,3 +85,6 @@

const result = await rpc.call('exec', { sql, params });
if (result.tables?.length) notifier.emit(result.tables);
if (result.tables?.length) {
notifier.emit(result.tables);
triggerAutoSync();
}
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };

@@ -117,3 +123,6 @@ }

await rpc.call('txn:commit', {});
if (touched.size) notifier.emit(touched);
if (touched.size) {
notifier.emit(touched);
triggerAutoSync();
}
return value;

@@ -218,2 +227,30 @@ } catch (err) {

let autoSyncRunning = false;
let trailingAutoSyncScheduled = false;
async function autoSyncLoop() {
autoSyncRunning = true;
try {
do {
trailingAutoSyncScheduled = false;
// Wait for any concurrent sync (interval tick or manual db.sync()) to settle.
while (inFlightSync) {
try { await inFlightSync; } catch { /* status already captured */ }
}
if (closed || pausedByAuth) break;
try { await runSyncOnce(); } catch { /* status already captured */ }
} while (trailingAutoSyncScheduled && !closed && !pausedByAuth);
} finally {
autoSyncRunning = false;
}
}
function triggerAutoSync() {
if (!syncer || !sync?.syncOnMutation) return;
if (closed || pausedByAuth) return;
if (autoSyncRunning) {
trailingAutoSyncScheduled = true;
return;
}
autoSyncLoop().catch(() => {});
}
async function syncNow() {

@@ -220,0 +257,0 @@ assertOpen();