@sqlite-sync/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hlc.ts","../src/worker-db/db-worker-client.ts","../src/memory-db/sqlite-reactive-db.ts","../src/memory-db/memory-db.ts","../src/sqlite-crdt/make-crdt-table.ts","../src/sync-db.ts"],"sourcesContent":["export interface HLC {\n timestamp: number;\n counter: number;\n nodeId: string;\n}\n\nexport class HLCCounter {\n private timestamp: number;\n private counter: number;\n private nodeId: string;\n\n private readonly getTimestamp: () => number;\n\n constructor(nodeId: string, getTimestamp: () => number) {\n this.timestamp = getTimestamp();\n this.counter = 0;\n this.nodeId = nodeId;\n this.getTimestamp = getTimestamp;\n }\n\n getCurrentHLC(): HLC {\n return {\n timestamp: this.timestamp,\n counter: this.counter,\n nodeId: this.nodeId,\n };\n }\n\n getNextHLC(): HLC {\n const now = this.getTimestamp();\n\n if (now > this.timestamp) {\n this.timestamp = now;\n this.counter = 0;\n return this.getCurrentHLC();\n }\n\n this.counter++;\n return this.getCurrentHLC();\n }\n\n mergeHLC(hlc: HLC) {\n if (this.timestamp === hlc.timestamp) {\n this.counter = Math.max(this.counter, hlc.counter) + 1;\n } else if (this.timestamp > hlc.timestamp) {\n this.counter++;\n } else {\n this.timestamp = hlc.timestamp;\n this.counter = hlc.counter + 1;\n }\n }\n}\n\nexport function serializeHLC(hlc: HLC) {\n return (\n hlc.timestamp.toString().padStart(15, \"0\") +\n \":\" +\n hlc.counter.toString(36).padStart(5, \"0\") +\n \":\" +\n hlc.nodeId\n );\n}\n\nexport function deserializeHLC(serialized: string) {\n const [ts, count, ...node] = serialized.split(\":\");\n return {\n timestamp: parseInt(ts),\n counter: parseInt(count, 36),\n nodeId: node.join(\":\"),\n };\n}\n\nexport function compareHLC(one: HLC, two: HLC) {\n if (one.timestamp == two.timestamp) {\n if (one.counter === two.counter) {\n if (one.nodeId === two.nodeId) {\n return 0;\n }\n return one.nodeId < two.nodeId ? -1 : 1;\n }\n return one.counter - two.counter;\n }\n return one.timestamp - two.timestamp;\n}\n\n","import {\n createTypedEventTarget,\n createDeferredPromise,\n type DeferredPromise,\n} from \"../utils\";\nimport {\n isWorkerInitResponse,\n isWorkerNotificationMessage,\n isWorkerResponseMessage,\n} from \"./worker-common\";\nimport type {\n AsyncRpc,\n WorkerBroadcastChannels,\n WorkerConfig,\n WorkerInitMessage,\n WorkerNotificationMessage,\n WorkerRequestMessage,\n WorkerRequestMethod,\n WorkerResponseMessage,\n WorkerRpc,\n} from \"./worker-common\";\n\nexport const createWorkerDbClient = ({\n broadcastChannels,\n}: {\n broadcastChannels: WorkerBroadcastChannels;\n}) => {\n const eventTarget = createTypedEventTarget<{\n \"new-notification\": WorkerNotificationMessage;\n }>();\n const workerRequestsMap = new Map<string, DeferredPromise<unknown>>();\n\n const queryWorker = <TMethod extends WorkerRequestMethod>(\n method: TMethod,\n args: Parameters<WorkerRpc[TMethod]>\n ): Promise<ReturnType<WorkerRpc[TMethod]>> => {\n // TODO Add timeout\n const requestId = crypto.randomUUID();\n const promise = createDeferredPromise<unknown>();\n workerRequestsMap.set(requestId, promise);\n\n const request: WorkerRequestMessage<TMethod> = {\n type: \"request\",\n requestId,\n method,\n args,\n };\n\n broadcastChannels.requests.postMessage(request);\n\n return promise.promise as Promise<ReturnType<WorkerRpc[TMethod]>>;\n };\n\n const handleWorkerResponse = (message: WorkerResponseMessage) => {\n const promise = workerRequestsMap.get(message.requestId);\n if (!promise) {\n return;\n }\n\n promise.resolve(message.data);\n workerRequestsMap.delete(message.requestId);\n };\n\n broadcastChannels.responses.onmessage = (event) => {\n const message = event.data;\n\n if (isWorkerResponseMessage(message)) {\n handleWorkerResponse(message);\n } else if (isWorkerNotificationMessage(message)) {\n eventTarget.dispatchEvent(\"new-notification\", message);\n }\n };\n\n const rpc: AsyncRpc<WorkerRpc> = {\n execute: (query) => queryWorker(\"execute\", [query]),\n getSnapshot: () => queryWorker(\"getSnapshot\", []),\n pushTabEvents: (request) => queryWorker(\"pushTabEvents\", [request]),\n pullEvents: (params) => queryWorker(\"pullEvents\", [params]),\n postInitReady: () => queryWorker(\"postInitReady\", []),\n };\n\n return {\n ...rpc,\n addEventListener: eventTarget.addEventListener,\n removeEventListener: eventTarget.removeEventListener,\n };\n};\n\nexport function initializeWorkerDb({\n worker,\n broadcastChannels,\n config,\n}: {\n worker: Worker;\n broadcastChannels: WorkerBroadcastChannels;\n config: WorkerConfig;\n}) {\n const promise = createDeferredPromise<void>();\n broadcastChannels.responses.onmessage = (event) => {\n const message = event.data;\n if (!isWorkerInitResponse(message)) {\n return;\n }\n promise.resolve();\n worker.onmessage = null;\n };\n\n const configMessage: WorkerInitMessage = {\n type: \"init\",\n config,\n };\n worker.postMessage(configMessage);\n\n broadcastChannels.requests.postMessage({\n type: \"request\",\n requestId: crypto.randomUUID(),\n method: \"postInitReady\",\n args: [],\n });\n\n return promise.promise;\n}\n\n","import sqlite3InitModule, { type Sqlite3Static } from \"@sqlite.org/sqlite-wasm\";\nimport { startPerformanceLogger, type Logger } from \"../logger\";\nimport { SQLiteDbWrapper, type PreparedStatement } from \"../sqlite-db-wrapper\";\nimport { createTypedEventTarget, TypedEvent } from \"../utils\";\n\nlet sqliteModule: Sqlite3Static | null = null;\n\ntype TableName<Database> = keyof Database extends string\n ? keyof Database\n : never;\n\ntype SQLiteReactiveDbOptions = {\n snapshot?: Uint8Array<ArrayBufferLike>;\n logger?: Logger;\n};\n\ntype EventsMap = {\n \"transaction-committed\": void;\n \"transaction-rolled-back\": void;\n \"any-table-changed\": void;\n} & Record<`table:${string}`, void>;\n\nexport function createSQLiteReactiveDb<Database>(\n opts: SQLiteReactiveDbOptions\n) {\n return SQLiteReactiveDb.create<Database>(opts);\n}\n\nconst defaultLogger: Logger = (type, message, level = \"info\") => {\n const logMessage = `[${type}] ${message}`;\n switch (level) {\n case \"info\":\n console.log(logMessage);\n break;\n case \"warning\":\n console.warn(logMessage);\n break;\n case \"error\":\n console.error(logMessage);\n break;\n case \"trace\":\n console.trace(logMessage);\n break;\n }\n};\n\nexport class SQLiteReactiveDb<Database> {\n readonly db: SQLiteDbWrapper<Database>;\n private sqlite3: Sqlite3Static;\n\n private readonly logger: Logger;\n\n private tablesUsedStatement: PreparedStatement<\n [string],\n { name: string; isWrite: boolean }\n > | null = null;\n\n private eventTarget = createTypedEventTarget<EventsMap>();\n\n private constructor(sqlite3: Sqlite3Static, logger: Logger) {\n this.sqlite3 = sqlite3;\n this.logger = logger;\n\n this.db = new SQLiteDbWrapper({\n db: new sqlite3.oo1.DB({ filename: \":memory:\" }),\n logger: this.logger,\n loggerPrefix: \"memory\",\n sqlite3,\n });\n }\n\n static async create<Database>(opts: SQLiteReactiveDbOptions) {\n const logger = opts.logger ?? defaultLogger;\n const perf = startPerformanceLogger(logger);\n if (!sqliteModule) {\n sqliteModule = await sqlite3InitModule();\n }\n\n const db = new SQLiteReactiveDb<Database>(sqliteModule, logger);\n\n if (opts.snapshot) {\n db.useSnapshot(opts.snapshot);\n }\n db.registerDbHooks();\n\n perf.logEnd(\"createSQLiteMemoryDb\", \"success\", \"info\");\n\n return db;\n }\n\n createLiveQuery<TResult>(query: {\n sql: string;\n parameters: readonly unknown[];\n }) {\n const fetchRows = () =>\n this.db.execute<TResult>({\n sql: query.sql,\n parameters: query.parameters ?? [],\n }).rows;\n\n let rows: TResult[] | null = null;\n\n const getRows = () => {\n if (!rows) {\n rows = fetchRows();\n }\n return rows;\n };\n\n let subscriber: (() => void) | null = null;\n\n const refresh = () => {\n rows = fetchRows();\n subscriber?.();\n };\n\n const subscribe = (onchange: () => void) => {\n if (subscriber) {\n throw new Error(\"Subscriber already exists\");\n }\n\n subscriber = onchange;\n const subscription = this.subscribeToQueryChanges({\n sql: query.sql,\n onDataChange: refresh,\n });\n\n return () => {\n subscription.unsubscribe();\n subscriber = null;\n };\n };\n\n return { getRows, refresh, subscribe };\n }\n\n subscribeToQueryChanges(params: { sql: string; onDataChange: () => void }) {\n const { sql, onDataChange } = params;\n\n const tables = this.getTablesUsed(sql);\n const readTables = new Set<string>();\n for (const table of tables) {\n if (!readTables.has(table.name)) {\n readTables.add(table.name);\n } else if (table.isWrite) {\n throw new Error(\n \"This query writes and reads from the same table. This may cause infinite loops.\"\n );\n }\n }\n\n const notifyDataChange = createDebouncedCallback(() => {\n onDataChange();\n }, 30);\n\n for (const table of readTables) {\n this.eventTarget.addEventListener(`table:${table}`, notifyDataChange);\n }\n this.eventTarget.addEventListener(\"any-table-changed\", notifyDataChange);\n\n return {\n unsubscribe: () => {\n for (const table of readTables) {\n this.eventTarget.removeEventListener(\n `table:${table}`,\n notifyDataChange\n );\n this.eventTarget.removeEventListener(\n \"any-table-changed\",\n notifyDataChange\n );\n }\n },\n };\n }\n\n subscribeToTableChanges(table: string, onChanges: () => void) {\n this.eventTarget.addEventListener(`table:${table}`, onChanges);\n this.eventTarget.addEventListener(\"any-table-changed\", onChanges);\n return {\n unsubscribe: () => {\n this.eventTarget.removeEventListener(`table:${table}`, onChanges);\n this.eventTarget.removeEventListener(\"any-table-changed\", onChanges);\n },\n };\n }\n\n getTablesUsed(query: string) {\n if (!this.tablesUsedStatement) {\n this.tablesUsedStatement = this.db.prepare<\n [string],\n { name: string; isWrite: boolean }\n >(\n \"select t.tbl_name as name, u.wr as isWrite from tables_used(?) as u inner join sqlite_master as t on t.name = u.name where u.schema = 'main'\"\n );\n }\n\n const tables = this.tablesUsedStatement.execute([query]);\n\n if (tables.length == 0 && query.toLowerCase().includes(\"delete\")) {\n // tables_used function does not work with delete queries that clear entire tables\n tables.push(...this.getClearedTables(query));\n }\n\n return tables;\n }\n\n private getClearedTables(query: string) {\n const operations = this.db.execute<{\n opcode: string;\n p1: number;\n p2: number;\n }>(`EXPLAIN ${query.split(\";\")[0]}`).rows;\n\n const clearedTablesRootPages = new Set<number>();\n for (const operation of operations) {\n if (operation.opcode === \"Clear\" && operation.p2 === 0) {\n clearedTablesRootPages.add(operation.p1);\n }\n }\n\n if (clearedTablesRootPages.size === 0) {\n return [];\n }\n\n const tableNames = this.db.execute<{ name: string; isWrite: boolean }>(\n `select t.tbl_name as name, true as isWrite from sqlite_master as t where t.rootpage in (${Array.from(\n clearedTablesRootPages\n ).join(\",\")})`\n ).rows;\n\n return tableNames;\n }\n\n addEventListener<K extends keyof EventsMap>(\n type: K,\n listener: (event: TypedEvent<EventsMap[K]>) => void\n ) {\n this.eventTarget.addEventListener(type, listener);\n }\n\n removeEventListener<K extends keyof EventsMap>(\n type: K,\n listener: (event: TypedEvent<EventsMap[K]>) => void\n ) {\n this.eventTarget.removeEventListener(type, listener);\n }\n\n notifyTableSubscribers(tables: (TableName<Database> | (string & {}))[] = []) {\n if (tables.length === 0) {\n this.eventTarget.dispatchEvent(\"any-table-changed\", undefined);\n return;\n }\n\n for (const table of tables) {\n this.eventTarget.dispatchEvent(`table:${table}`, undefined);\n }\n }\n\n private registerDbHooks() {\n const updateQueue = new Set<string>();\n\n this.sqlite3.capi.sqlite3_update_hook(\n this.db.ensureDb,\n (_ctx, _opId, _db, table) => {\n updateQueue.add(table);\n },\n 0\n );\n\n this.sqlite3.capi.sqlite3_rollback_hook(\n this.db.ensureDb,\n () => {\n if (updateQueue.size === 0) {\n return 0;\n }\n\n updateQueue.clear();\n this.eventTarget.dispatchEvent(\"transaction-rolled-back\", undefined);\n\n return 0;\n },\n 0\n );\n\n this.sqlite3.capi.sqlite3_commit_hook(\n this.db.ensureDb,\n () => {\n if (updateQueue.size === 0) {\n return 0;\n }\n\n const tables = Array.from(updateQueue);\n updateQueue.clear();\n this.eventTarget.dispatchEvent(\"transaction-committed\", undefined);\n\n queueMicrotask(() => {\n this.notifyTableSubscribers(tables);\n });\n return 0;\n },\n 0\n );\n }\n\n createSnapshot() {\n const perf = startPerformanceLogger(this.logger);\n const snapshot = this.sqlite3.capi.sqlite3_js_db_export(this.db.ensureDb);\n perf.logEnd(\n \"createSnapshot\",\n `snapshot size: ${snapshot.byteLength}`,\n \"info\"\n );\n\n return snapshot;\n }\n\n useSnapshot(snapshot: Uint8Array<ArrayBufferLike>) {\n this.db.useSnapshot(snapshot);\n this.notifyTableSubscribers();\n }\n}\n\nfunction createDebouncedCallback<TArgs extends unknown[]>(\n callback: (...args: TArgs) => void,\n delay: number\n) {\n let timeout: unknown | null = null;\n let shouldCallWithoutDelay = true;\n\n return (...args: TArgs) => {\n if (shouldCallWithoutDelay) {\n callback(...args);\n shouldCallWithoutDelay = false;\n return;\n }\n\n const effect = () => {\n timeout = null;\n shouldCallWithoutDelay = true;\n return callback(...args);\n };\n\n if (timeout) {\n clearTimeout(timeout as any);\n }\n\n timeout = setTimeout(effect, delay);\n };\n}\n","import { sql, type Kysely } from \"kysely\";\nimport { HLCCounter, serializeHLC } from \"../hlc\";\nimport {\n applyMemoryDbSchema,\n type MemoryDbSchema,\n} from \"../migrations/system-schema\";\nimport {\n registerCrdtFunctions,\n type CrdtEventStatus,\n type PersistedCrdtEvent,\n} from \"../sqlite-crdt/crdt-table-schema\";\nimport { makeCrdtTable } from \"../sqlite-crdt/make-crdt-table\";\nimport { createSyncIdCounter } from \"../sqlite-crdt/sync-id-counter\";\nimport type { SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\nimport { createCrdtStorage } from \"../sqlite-crdt/crdt-storage\";\nimport { applyCrdtEventMutations } from \"../sqlite-crdt/apply-crdt-event\";\nimport type { SQLiteReactiveDb } from \"./sqlite-reactive-db\";\nimport { generateId } from \"../utils\";\n\nexport type MemoryDbCrdtTableConfig = {\n baseTableName: string;\n crdtTableName: string;\n};\n\ntype MemoryDbOptions<Database> = {\n reactiveDb: SQLiteReactiveDb<Database>;\n hlcCounter: HLCCounter;\n tabId: string;\n crdtTables: MemoryDbCrdtTableConfig[];\n};\n\nexport async function createMemoryDb<Database>({\n reactiveDb: _reactiveDb,\n hlcCounter,\n tabId,\n crdtTables,\n}: MemoryDbOptions<Database>) {\n const reactiveDb = _reactiveDb as unknown as SQLiteReactiveDb<MemoryDbSchema>;\n const db = reactiveDb.db;\n\n applyMemoryDbSchema(db);\n for (const table of crdtTables) {\n makeCrdtTable({\n db,\n baseTableName: table.baseTableName,\n crdtTableName: table.crdtTableName,\n });\n }\n\n const localSyncId = createSyncIdCounter({\n initialSyncId: 0,\n });\n\n const pendingLocalEvents: PersistedCrdtEvent[] = [];\n registerCrdtFunctions({\n db,\n getTableSchema: (dataset: string) => db.dbSchema[dataset],\n getNextTimestamp: () => serializeHLC(hlcCounter.getNextHLC()),\n updateLogTableName: \"crdt_update_log\",\n onEventApplied: (event) => {\n const persistedEvent: PersistedCrdtEvent = {\n ...event,\n origin: tabId,\n sync_id: ++localSyncId.current,\n status: \"applied\" as const,\n };\n enqueueCrdtEvent(db, persistedEvent);\n pendingLocalEvents.push(persistedEvent);\n },\n });\n db.createScalarFunction({\n name: \"gen_id\",\n callback: () => generateId(),\n deterministic: false,\n directOnly: false,\n innocuous: true,\n });\n\n reactiveDb.addEventListener(\"transaction-rolled-back\", () => {\n pendingLocalEvents.length = 0;\n });\n reactiveDb.addEventListener(\"transaction-committed\", () => {\n const appliedEvents = pendingLocalEvents.splice(0);\n queueMicrotask(() => {\n for (const event of appliedEvents) {\n crdtStorage.dispatchEvent(\"event-applied\", event);\n }\n crdtStorage.dispatchEvent(\"event-processing-done\", undefined);\n });\n });\n\n const crdtStorage = createCrdtStorage({\n syncId: localSyncId,\n persistEvents: (events) => persistEvents(db, events),\n popPendingEventsBatch: () => popPendingEventsBatch(db, 50),\n applyCrdtEventMutations: (event) =>\n applyCrdtEventMutations({\n db,\n event,\n updateLogTableName: \"crdt_update_log\",\n }),\n updateEventStatus: (syncId, status) =>\n updateEventStatus(db, syncId, status),\n });\n\n return {\n crdtStorage,\n };\n}\n\nfunction enqueueCrdtEvent(\n db: SQLiteDbWrapper<MemoryDbSchema>,\n event: PersistedCrdtEvent\n) {\n db.executePrepared(\"enqueue-crdt-events\", event, (db, params) =>\n (db as unknown as Kysely<MemoryDbSchema>)\n .insertInto(\"persisted_crdt_events\")\n .values({\n status: params(\"status\"),\n sync_id: params(\"sync_id\"),\n type: params(\"type\"),\n timestamp: params(\"timestamp\"),\n dataset: params(\"dataset\"),\n item_id: params(\"item_id\"),\n payload: params(\"payload\"),\n origin: params(\"origin\"),\n })\n );\n}\n\nfunction persistEvents(\n db: SQLiteDbWrapper<MemoryDbSchema>,\n events: PersistedCrdtEvent[]\n) {\n db.executeTransaction((db) => {\n const chunkSize = 100;\n for (let i = 0; i < events.length; i += chunkSize) {\n const chunk = events.slice(i, i + chunkSize);\n db.executeKysely((db) =>\n db.insertInto(\"persisted_crdt_events\").values(chunk)\n );\n }\n });\n}\n\nfunction popPendingEventsBatch(\n db: SQLiteDbWrapper<MemoryDbSchema>,\n limit: number\n) {\n const events = db.executePrepared(\n \"pop-enqueued-crdt-events\",\n {\n limit: limit,\n },\n (db, param) =>\n db\n .selectFrom(\"persisted_crdt_events\")\n .where(\"status\", \"=\", sql.lit(\"pending\"))\n .limit(param(\"limit\"))\n .orderBy(\"sync_id\", \"asc\")\n .selectAll()\n );\n return {\n events,\n hasMore: events.length === limit,\n };\n}\n\nfunction updateEventStatus(\n db: SQLiteDbWrapper<MemoryDbSchema>,\n syncId: number,\n status: CrdtEventStatus\n) {\n db.executePrepared(\n \"update-crdt-event-status\",\n { syncId, status },\n (db, params) =>\n db\n .updateTable(\"persisted_crdt_events\")\n .set({ status: params(\"status\") })\n .where(\"sync_id\", \"=\", params(\"syncId\"))\n );\n}\n\n","import type { SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\n\nexport function makeCrdtTable({\n db,\n baseTableName,\n crdtTableName,\n}: {\n db: SQLiteDbWrapper<any>;\n baseTableName: string;\n crdtTableName: string;\n}) {\n const tableSchema = db.dbSchema[baseTableName];\n\n if (!tableSchema) {\n throw new Error(`Table ${baseTableName} not found`);\n }\n\n db.execute(`\ncreate view ${crdtTableName} as\nselect * from ${baseTableName}\nwhere tombstone = 0;`);\n\n const allColumnNames = tableSchema.columns.map((column) => column.name);\n\n const jsonPayload = (from: \"new\" | \"old\") =>\n \"'{'||\" +\n allColumnNames\n .map((col) => `'\"${col}\":'||json_quote(${from}.${col})`)\n .join(\"||','||\") +\n \"||'}'\";\n\n db.execute(`\ncreate trigger ${crdtTableName}_created\ninstead of insert on ${crdtTableName}\nfor each row\nbegin\nselect handle_item_created('${baseTableName}', ${jsonPayload(\"new\")});\nend;\n`);\n\n db.execute(`\ncreate trigger ${crdtTableName}_updated\ninstead of update on ${crdtTableName}\nfor each row\nbegin\nselect handle_item_updated(\n '${baseTableName}',\n ${jsonPayload(\"old\")},\n ${jsonPayload(\"new\")}\n);\nend;\n`);\n\n db.execute(`\ncreate trigger ${crdtTableName}_deleted\ninstead of delete on ${crdtTableName}\nfor each row\nwhen old.tombstone = 0\nbegin\nselect handle_item_deleted('${baseTableName}', old.id);\nend;\n`);\n}\n\n","import { deserializeHLC, HLCCounter } from \"./hlc\";\nimport { generateId } from \"./utils\";\nimport { createBroadcastChannels } from \"./worker-db/worker-common\";\nimport {\n initializeWorkerDb,\n createWorkerDbClient,\n} from \"./worker-db/db-worker-client\";\nimport {\n createSQLiteReactiveDb,\n SQLiteReactiveDb,\n} from \"./memory-db/sqlite-reactive-db\";\nimport {\n createMemoryDb,\n type MemoryDbCrdtTableConfig,\n} from \"./memory-db/memory-db\";\nimport { createSyncIdCounter } from \"./sqlite-crdt/sync-id-counter\";\nimport { createCrdtSyncRemoteSource } from \"./sqlite-crdt/crdt-sync-remote-source\";\n\ntype SyncedDbOptions = {\n dbPath: string;\n clearOnInit?: boolean;\n // tabId?: string;\n // clientId: string;\n // logger?: Logger;\n crdtTables: MemoryDbCrdtTableConfig[];\n\n worker: Worker;\n};\n\nexport async function createSyncedDb<Database>(options: SyncedDbOptions) {\n if (!options.dbPath.startsWith(\"/\")) {\n throw new Error(\"dbPath must be an absolute path\");\n }\n\n const tabId = generateId();\n\n const broadcastChannels = createBroadcastChannels();\n\n await initializeWorkerDb({\n worker: options.worker,\n broadcastChannels,\n config: {\n clientId: generateId(),\n dbPath: options.dbPath,\n clearOnInit: options.clearOnInit,\n syncServer: {\n host: \"\",\n room: \"\",\n },\n },\n });\n\n const workerClient = createWorkerDbClient({\n broadcastChannels,\n });\n\n const hlcCounter = new HLCCounter(tabId, () => Date.now());\n\n const workerClientSnapshot = await workerClient.getSnapshot();\n const reactiveDb = await createSQLiteReactiveDb<Database>({\n snapshot: workerClientSnapshot.file,\n });\n const { crdtStorage } = await createMemoryDb({\n reactiveDb: reactiveDb,\n hlcCounter,\n tabId,\n crdtTables: options.crdtTables,\n });\n\n const remoteSyncId = createSyncIdCounter({\n initialSyncId: workerClientSnapshot.syncId,\n });\n const remoteSyncSource = createCrdtSyncRemoteSource({\n bufferSize: 100,\n syncId: remoteSyncId,\n storage: crdtStorage,\n nodeId: tabId,\n pullEvents: (request) => workerClient.pullEvents(request),\n pushEvents: (request) => workerClient.pushTabEvents(request),\n });\n\n workerClient.addEventListener(\"new-notification\", (event) => {\n const notification = event.payload;\n if (\n notification.notificationType === \"new-event-chunk-applied\" &&\n notification.newSyncId > remoteSyncId.current\n ) {\n remoteSyncSource.pullEvents();\n }\n });\n\n crdtStorage.addEventListener(\"event-applied\", (event) => {\n if (event.payload.origin === \"remote\") {\n hlcCounter.mergeHLC(deserializeHLC(event.payload.timestamp));\n }\n });\n\n return {\n db: reactiveDb.db,\n reactiveDb: reactiveDb as Omit<SQLiteReactiveDb<Database>, \"db\">,\n workerDb: workerClient,\n };\n}\n\nexport type SyncedDb<Database> = Awaited<\n ReturnType<typeof createSyncedDb<Database>>\n>;\n\n"],"mappings":"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","names":["sql","db"]}
@@ -0,0 +1,45 @@
1
+ import { f as EventsPullResponse, b as EventsPushResponse } from './crdt-sync-remote-source-rrqinqLn.js';
2
+ export { q as CrdtEventStatus, m as CrdtStorage, G as DeferredPromise, P as PersistedCrdtEvent, v as SyncIdCounter, p as crdtSchema, l as createCrdtStorage, x as createDeferredPromise, u as createSyncIdCounter, F as jsonSafeParse } from './crdt-sync-remote-source-rrqinqLn.js';
3
+ import { z } from 'zod';
4
+ export { c as createCrdtSyncProducer, d as dummyKysely } from './crdt-sync-producer-0toEpGf0.js';
5
+ import '@sqlite.org/sqlite-wasm';
6
+ import 'kysely';
7
+
8
+ declare const syncServerRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
9
+ type: z.ZodLiteral<"pull-events">;
10
+ requestId: z.ZodString;
11
+ afterSyncId: z.ZodNumber;
12
+ excludeNodeId: z.ZodOptional<z.ZodString>;
13
+ }, z.core.$strip>, z.ZodObject<{
14
+ type: z.ZodLiteral<"push-events">;
15
+ requestId: z.ZodString;
16
+ nodeId: z.ZodString;
17
+ events: z.ZodArray<z.ZodObject<{
18
+ timestamp: z.ZodString;
19
+ type: z.ZodEnum<{
20
+ "item-created": "item-created";
21
+ "item-updated": "item-updated";
22
+ }>;
23
+ dataset: z.ZodString;
24
+ item_id: z.ZodString;
25
+ payload: z.ZodString;
26
+ }, z.core.$strip>>;
27
+ }, z.core.$strip>], "type">;
28
+ type SyncServerMessage = {
29
+ type: "events-pull-response";
30
+ requestId: string;
31
+ data: EventsPullResponse;
32
+ } | {
33
+ type: "events-push-response";
34
+ requestId: string;
35
+ data: EventsPushResponse;
36
+ } | {
37
+ type: "events-applied";
38
+ newSyncId: number;
39
+ };
40
+ type SyncServerRequest = z.infer<typeof syncServerRequestSchema>;
41
+ type ExtractSyncServerRequest<T extends SyncServerRequest["type"]> = Extract<SyncServerRequest, {
42
+ type: T;
43
+ }>;
44
+
45
+ export { type ExtractSyncServerRequest, type SyncServerMessage, type SyncServerRequest, syncServerRequestSchema };
package/dist/server.js ADDED
@@ -0,0 +1,45 @@
1
+ import {
2
+ crdtSchema,
3
+ createCrdtStorage,
4
+ createCrdtSyncProducer,
5
+ createDeferredPromise,
6
+ createSyncIdCounter,
7
+ dummyKysely,
8
+ jsonSafeParse
9
+ } from "./chunk-YLXMST5Z.js";
10
+
11
+ // src/server/server-common.ts
12
+ import { z } from "zod";
13
+ var syncServerRequestSchema = z.discriminatedUnion("type", [
14
+ z.object({
15
+ type: z.literal("pull-events"),
16
+ requestId: z.string(),
17
+ afterSyncId: z.number(),
18
+ excludeNodeId: z.string().optional()
19
+ }),
20
+ z.object({
21
+ type: z.literal("push-events"),
22
+ requestId: z.string(),
23
+ nodeId: z.string(),
24
+ events: z.array(
25
+ z.object({
26
+ timestamp: z.string(),
27
+ type: z.enum(["item-created", "item-updated"]),
28
+ dataset: z.string(),
29
+ item_id: z.string(),
30
+ payload: z.string()
31
+ })
32
+ )
33
+ })
34
+ ]);
35
+ export {
36
+ crdtSchema,
37
+ createCrdtStorage,
38
+ createCrdtSyncProducer,
39
+ createDeferredPromise,
40
+ createSyncIdCounter,
41
+ dummyKysely,
42
+ jsonSafeParse,
43
+ syncServerRequestSchema
44
+ };
45
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/server-common.ts"],"sourcesContent":["import type {\n EventsPullResponse,\n EventsPushResponse,\n} from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport { z } from \"zod\";\n\nexport const syncServerRequestSchema = z.discriminatedUnion(\"type\", [\n z.object({\n type: z.literal(\"pull-events\"),\n requestId: z.string(),\n afterSyncId: z.number(),\n excludeNodeId: z.string().optional(),\n }),\n z.object({\n type: z.literal(\"push-events\"),\n requestId: z.string(),\n nodeId: z.string(),\n events: z.array(\n z.object({\n timestamp: z.string(),\n type: z.enum([\"item-created\", \"item-updated\"]),\n dataset: z.string(),\n item_id: z.string(),\n payload: z.string(),\n })\n ),\n }),\n]);\n\nexport type SyncServerMessage =\n | {\n type: \"events-pull-response\";\n requestId: string;\n data: EventsPullResponse;\n }\n | {\n type: \"events-push-response\";\n requestId: string;\n data: EventsPushResponse;\n }\n | {\n type: \"events-applied\";\n newSyncId: number;\n };\n\nexport type SyncServerRequest = z.infer<typeof syncServerRequestSchema>;\n\nexport type ExtractSyncServerRequest<T extends SyncServerRequest[\"type\"]> =\n Extract<SyncServerRequest, { type: T }>;\n\n"],"mappings":";;;;;;;;;;;AAIA,SAAS,SAAS;AAEX,IAAM,0BAA0B,EAAE,mBAAmB,QAAQ;AAAA,EAClE,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,aAAa;AAAA,IAC7B,WAAW,EAAE,OAAO;AAAA,IACpB,aAAa,EAAE,OAAO;AAAA,IACtB,eAAe,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,aAAa;AAAA,IAC7B,WAAW,EAAE,OAAO;AAAA,IACpB,QAAQ,EAAE,OAAO;AAAA,IACjB,QAAQ,EAAE;AAAA,MACR,EAAE,OAAO;AAAA,QACP,WAAW,EAAE,OAAO;AAAA,QACpB,MAAM,EAAE,KAAK,CAAC,gBAAgB,cAAc,CAAC;AAAA,QAC7C,SAAS,EAAE,OAAO;AAAA,QAClB,SAAS,EAAE,OAAO;AAAA,QAClB,SAAS,EAAE,OAAO;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH,CAAC;","names":[]}
@@ -0,0 +1,19 @@
1
+ import { Migration } from 'kysely';
2
+ export { Migration } from 'kysely';
3
+ import { L as Logger, e as EventsPullRequest, f as EventsPullResponse, E as EventsPushRequest, b as EventsPushResponse } from './crdt-sync-remote-source-rrqinqLn.js';
4
+ import '@sqlite.org/sqlite-wasm';
5
+
6
+ type WorkerOptions = {
7
+ migrations: Record<number, Migration>;
8
+ logger?: Logger;
9
+ createRemoteSource?: CreateRemoteSourceFactory;
10
+ };
11
+ type CreateRemoteSourceFactory = (opts: {
12
+ onEventsAvailable: (newSyncId: number) => void;
13
+ }) => {
14
+ pullEvents: (request: EventsPullRequest) => Promise<EventsPullResponse>;
15
+ pushEvents: (request: EventsPushRequest) => Promise<EventsPushResponse>;
16
+ };
17
+ declare function startDbWorker(opts: WorkerOptions): Promise<void>;
18
+
19
+ export { EventsPullRequest, EventsPullResponse, EventsPushRequest, EventsPushResponse, startDbWorker };
package/dist/worker.js ADDED
@@ -0,0 +1,261 @@
1
+ import {
2
+ SQLiteDbWrapper,
3
+ applyWorkerDbSchema,
4
+ createBroadcastChannels,
5
+ createCrdtSyncRemoteSource,
6
+ createSQLiteKysely,
7
+ createSyncDbMigrator,
8
+ isWorkerInitMessage,
9
+ isWorkerRequestMessage,
10
+ syncDbWorkerLockName
11
+ } from "./chunk-LK5FJCUD.js";
12
+ import {
13
+ applyCrdtEventMutations,
14
+ createCrdtStorage,
15
+ createCrdtSyncProducer,
16
+ createDeferredPromise,
17
+ createSyncIdCounter
18
+ } from "./chunk-YLXMST5Z.js";
19
+
20
+ // src/worker-db/db-worker.ts
21
+ import { sql } from "kysely";
22
+ import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
23
+ var defaultLogger = (type, message, level = "info") => {
24
+ const logMessage = `[${type}] ${message}`;
25
+ switch (level) {
26
+ case "info":
27
+ console.log(logMessage);
28
+ break;
29
+ case "warning":
30
+ console.warn(logMessage);
31
+ break;
32
+ case "error":
33
+ console.error(logMessage);
34
+ break;
35
+ case "trace":
36
+ console.trace(logMessage);
37
+ break;
38
+ }
39
+ };
40
+ async function createDbWorker(config, opts) {
41
+ const broadcastChannels = createBroadcastChannels();
42
+ const logger = opts.logger ?? defaultLogger;
43
+ const sqlite3 = await sqlite3InitModule();
44
+ const pool = await sqlite3.installOpfsSAHPoolVfs({
45
+ name: "sync-db-storage",
46
+ clearOnInit: config.clearOnInit
47
+ });
48
+ const db = new SQLiteDbWrapper({
49
+ db: new pool.OpfsSAHPoolDb(config.dbPath),
50
+ logger,
51
+ loggerPrefix: "worker",
52
+ sqlite3
53
+ });
54
+ db.execute("PRAGMA locking_mode=exclusive");
55
+ db.execute("PRAGMA journal_mode=WAL");
56
+ db.execute(`ATTACH DATABASE '${config.dbPath}-worker' as worker`);
57
+ applyWorkerDbSchema(db);
58
+ const kysely = createSQLiteKysely(db);
59
+ const migrator = createSyncDbMigrator({
60
+ db: kysely,
61
+ migrations: opts.migrations
62
+ });
63
+ await migrator.migrateToLatest();
64
+ db.invalidateDbSchema();
65
+ const localSyncId = createSyncIdCounter({
66
+ initialSyncId: getLatestSyncId(db)
67
+ });
68
+ const crdtStorage = createCrdtStorage({
69
+ syncId: localSyncId,
70
+ applyCrdtEventMutations: (event) => applyCrdtEventMutations({
71
+ db,
72
+ event,
73
+ updateLogTableName: "crdt_update_log"
74
+ }),
75
+ persistEvents: (events) => persistEvents(db, events),
76
+ popPendingEventsBatch: () => popPendingEventsBatch(db, 50),
77
+ updateEventStatus: (syncId, status) => updateEventStatus(db, syncId, status)
78
+ });
79
+ createCrdtSyncProducer({
80
+ bufferSize: 100,
81
+ storage: crdtStorage,
82
+ broadcastEvents: (chunk) => {
83
+ broadcastChannels.responses.postMessage({
84
+ notificationType: "new-event-chunk-applied",
85
+ newSyncId: chunk.newSyncId
86
+ });
87
+ }
88
+ });
89
+ if (opts.createRemoteSource) {
90
+ let crdtSyncRemoteSource = null;
91
+ const remoteSource = opts.createRemoteSource?.({
92
+ onEventsAvailable: () => {
93
+ crdtSyncRemoteSource?.pullEvents();
94
+ }
95
+ });
96
+ const storedRemoteSyncId = Number.parseInt(
97
+ getMetaValue(db, "remote-sync-id")
98
+ );
99
+ const remoteSyncId = createSyncIdCounter({
100
+ initialSyncId: Number.isNaN(storedRemoteSyncId) ? -1 : storedRemoteSyncId,
101
+ saveToStorage: (syncId) => setMetaValue(db, "remote-sync-id", syncId.toString())
102
+ });
103
+ crdtSyncRemoteSource = createCrdtSyncRemoteSource({
104
+ bufferSize: 50,
105
+ syncId: remoteSyncId,
106
+ nodeId: config.clientId,
107
+ storage: crdtStorage,
108
+ pullEvents: remoteSource.pullEvents,
109
+ pushEvents: remoteSource.pushEvents
110
+ });
111
+ await crdtSyncRemoteSource.pullEvents({ includeSelf: true });
112
+ }
113
+ const rpcTarget = {
114
+ execute: (query) => db.execute(query),
115
+ getSnapshot: () => {
116
+ db.execute("PRAGMA journal_mode=off");
117
+ const file = db.createSnapshot();
118
+ db.execute("PRAGMA journal_mode=WAL");
119
+ return {
120
+ file,
121
+ syncId: localSyncId.current
122
+ };
123
+ },
124
+ postInitReady: () => {
125
+ broadcastChannels.responses.postMessage({
126
+ type: "init-ready"
127
+ });
128
+ },
129
+ pushTabEvents: (request) => {
130
+ crdtStorage.enqueueEvents(
131
+ request.events.map((event) => ({
132
+ ...event,
133
+ origin: request.nodeId
134
+ }))
135
+ );
136
+ return {
137
+ ok: true
138
+ };
139
+ },
140
+ pullEvents: (request) => {
141
+ const events = db.executeKysely((db2) => {
142
+ const query = db2.selectFrom("worker.crdt_events").where("sync_id", ">", request.afterSyncId).where("status", "=", "applied").orderBy("sync_id", "asc").selectAll();
143
+ if (request.excludeNodeId) {
144
+ query.where("origin", "!=", request.excludeNodeId);
145
+ }
146
+ return query;
147
+ }).rows;
148
+ return {
149
+ events,
150
+ hasMore: false,
151
+ newSyncId: events[events.length - 1]?.sync_id ?? request.afterSyncId
152
+ };
153
+ }
154
+ };
155
+ broadcastChannels.requests.onmessage = (event) => {
156
+ const message = event.data;
157
+ if (!isWorkerRequestMessage(message)) {
158
+ return;
159
+ }
160
+ const method = rpcTarget[message.method];
161
+ const data = method.apply(null, message.args);
162
+ const response = {
163
+ type: "response",
164
+ requestId: message.requestId,
165
+ data
166
+ };
167
+ broadcastChannels.responses.postMessage(response);
168
+ };
169
+ rpcTarget.postInitReady();
170
+ }
171
+ async function getConfig() {
172
+ let configSet = false;
173
+ const responsePromise = createDeferredPromise();
174
+ self.onmessage = (event) => {
175
+ if (configSet) {
176
+ console.error("Worker config already set");
177
+ return;
178
+ }
179
+ const message = event.data;
180
+ if (!isWorkerInitMessage(message)) {
181
+ return;
182
+ }
183
+ responsePromise.resolve(message.config);
184
+ configSet = true;
185
+ };
186
+ return responsePromise.promise;
187
+ }
188
+ async function startDbWorker(opts) {
189
+ const config = await getConfig();
190
+ await navigator.locks.request(
191
+ syncDbWorkerLockName,
192
+ { mode: "exclusive" },
193
+ async (lock) => {
194
+ if (!lock) {
195
+ return;
196
+ }
197
+ await createDbWorker(config, opts);
198
+ await new Promise(() => {
199
+ });
200
+ }
201
+ );
202
+ console.error("Failed to acquire lock");
203
+ }
204
+ function getLatestSyncId(db) {
205
+ const result = db.executePrepared(
206
+ "get-latest-sync-id",
207
+ {},
208
+ (db2) => db2.selectFrom("worker.crdt_events").select((eb) => eb.fn.max("sync_id").as("sync_id"))
209
+ );
210
+ return result[0]?.sync_id ?? 0;
211
+ }
212
+ function persistEvents(db, events) {
213
+ db.executeTransaction((db2) => {
214
+ const chunkSize = 100;
215
+ for (let i = 0; i < events.length; i += chunkSize) {
216
+ const chunk = events.slice(i, i + chunkSize);
217
+ db2.executeKysely(
218
+ (db3) => db3.insertInto("worker.crdt_events").values(chunk)
219
+ );
220
+ }
221
+ });
222
+ }
223
+ function popPendingEventsBatch(db, limit) {
224
+ const events = db.executePrepared(
225
+ "pop-enqueued-crdt-events",
226
+ {
227
+ limit
228
+ },
229
+ (db2, param) => db2.selectFrom("worker.crdt_events").where("status", "=", sql.lit("pending")).limit(param("limit")).orderBy("sync_id", "asc").selectAll()
230
+ );
231
+ return {
232
+ events,
233
+ hasMore: events.length === limit
234
+ };
235
+ }
236
+ function updateEventStatus(db, syncId, status) {
237
+ db.executePrepared(
238
+ "update-crdt-event-status",
239
+ { syncId, status },
240
+ (db2, params) => db2.updateTable("worker.crdt_events").set({ status: params("status") }).where("sync_id", "=", params("syncId"))
241
+ );
242
+ }
243
+ function getMetaValue(db, key) {
244
+ const [result] = db.executePrepared(
245
+ "get-meta-value",
246
+ { key },
247
+ (db2, params) => db2.selectFrom("worker.meta").where("key", "=", params("key")).select("value")
248
+ );
249
+ return result?.value ?? null;
250
+ }
251
+ function setMetaValue(db, key, value) {
252
+ db.executePrepared(
253
+ "set-meta-value",
254
+ { key, value },
255
+ (db2, params) => db2.insertInto("worker.meta").values({ key: params("key"), value: params("value") }).onConflict((oc) => oc.doUpdateSet({ value: params("value") }))
256
+ );
257
+ }
258
+ export {
259
+ startDbWorker
260
+ };
261
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/worker-db/db-worker.ts"],"sourcesContent":["/* eslint-disable prefer-spread */\nimport { sql, type Kysely, type Migration } from \"kysely\";\nimport { createDeferredPromise } from \"../utils\";\nimport {\n createBroadcastChannels,\n isWorkerInitMessage,\n isWorkerRequestMessage,\n syncDbWorkerLockName,\n type WorkerConfig,\n type WorkerResponseMessage,\n type WorkerRpc,\n} from \"./worker-common\";\nimport sqlite3InitModule from \"@sqlite.org/sqlite-wasm\";\nimport { SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\nimport {\n applyWorkerDbSchema,\n type WorkerDbSchema,\n} from \"../migrations/system-schema\";\nimport { createSQLiteKysely } from \"../sqlite-kysely\";\nimport { createSyncDbMigrator } from \"../migrations/migrator\";\nimport { createSyncIdCounter } from \"../sqlite-crdt/sync-id-counter\";\nimport { applyCrdtEventMutations } from \"../sqlite-crdt/apply-crdt-event\";\nimport { createCrdtStorage } from \"../sqlite-crdt/crdt-storage\";\nimport type {\n CrdtEventStatus,\n PersistedCrdtEvent,\n} from \"../sqlite-crdt/crdt-table-schema\";\nimport { createCrdtSyncProducer } from \"../sqlite-crdt/crdt-sync-producer\";\nimport {\n createCrdtSyncRemoteSource,\n type CrdtSyncRemoteSource,\n type EventsPullRequest,\n type EventsPullResponse,\n type EventsPushRequest,\n type EventsPushResponse,\n} from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { Logger } from \"../logger\";\n\nconst defaultLogger: Logger = (type, message, level = \"info\") => {\n const logMessage = `[${type}] ${message}`;\n switch (level) {\n case \"info\":\n console.log(logMessage);\n break;\n case \"warning\":\n console.warn(logMessage);\n break;\n case \"error\":\n console.error(logMessage);\n break;\n case \"trace\":\n console.trace(logMessage);\n break;\n }\n};\n\nasync function createDbWorker(config: WorkerConfig, opts: WorkerOptions) {\n const broadcastChannels = createBroadcastChannels();\n const logger = opts.logger ?? defaultLogger;\n\n const sqlite3 = await sqlite3InitModule();\n\n const pool = await sqlite3.installOpfsSAHPoolVfs({\n name: \"sync-db-storage\",\n clearOnInit: config.clearOnInit,\n });\n\n const db = new SQLiteDbWrapper<WorkerDbSchema>({\n db: new pool.OpfsSAHPoolDb(config.dbPath),\n logger: logger,\n loggerPrefix: \"worker\",\n sqlite3,\n });\n\n db.execute(\"PRAGMA locking_mode=exclusive\");\n db.execute(\"PRAGMA journal_mode=WAL\");\n db.execute(`ATTACH DATABASE '${config.dbPath}-worker' as worker`);\n applyWorkerDbSchema(db);\n const kysely = createSQLiteKysely<WorkerDbSchema>(db);\n const migrator = createSyncDbMigrator({\n db: kysely as Kysely<unknown>,\n migrations: opts.migrations,\n });\n await migrator.migrateToLatest();\n db.invalidateDbSchema();\n\n const localSyncId = createSyncIdCounter({\n initialSyncId: getLatestSyncId(db),\n });\n\n const crdtStorage = createCrdtStorage({\n syncId: localSyncId,\n applyCrdtEventMutations: (event) =>\n applyCrdtEventMutations({\n db,\n event,\n updateLogTableName: \"crdt_update_log\",\n }),\n persistEvents: (events) => persistEvents(db, events),\n popPendingEventsBatch: () => popPendingEventsBatch(db, 50),\n updateEventStatus: (syncId, status) =>\n updateEventStatus(db, syncId, status),\n });\n\n createCrdtSyncProducer({\n bufferSize: 100,\n storage: crdtStorage,\n broadcastEvents: (chunk) => {\n broadcastChannels.responses.postMessage({\n notificationType: \"new-event-chunk-applied\",\n newSyncId: chunk.newSyncId,\n });\n },\n });\n\n if (opts.createRemoteSource) {\n let crdtSyncRemoteSource: CrdtSyncRemoteSource | null = null;\n const remoteSource = opts.createRemoteSource?.({\n onEventsAvailable: () => {\n crdtSyncRemoteSource?.pullEvents();\n },\n });\n\n const storedRemoteSyncId = Number.parseInt(\n getMetaValue(db, \"remote-sync-id\")\n );\n const remoteSyncId = createSyncIdCounter({\n initialSyncId: Number.isNaN(storedRemoteSyncId) ? -1 : storedRemoteSyncId,\n saveToStorage: (syncId) =>\n setMetaValue(db, \"remote-sync-id\", syncId.toString()),\n });\n crdtSyncRemoteSource = createCrdtSyncRemoteSource({\n bufferSize: 50,\n syncId: remoteSyncId,\n nodeId: config.clientId,\n storage: crdtStorage,\n pullEvents: remoteSource.pullEvents,\n pushEvents: remoteSource.pushEvents,\n });\n await crdtSyncRemoteSource.pullEvents({ includeSelf: true });\n }\n\n const rpcTarget: WorkerRpc = {\n execute: (query) => db.execute(query),\n getSnapshot: () => {\n db.execute(\"PRAGMA journal_mode=off\");\n const file = db.createSnapshot();\n db.execute(\"PRAGMA journal_mode=WAL\");\n return {\n file,\n syncId: localSyncId.current,\n };\n },\n postInitReady: () => {\n broadcastChannels.responses.postMessage({\n type: \"init-ready\",\n });\n },\n pushTabEvents: (request) => {\n crdtStorage.enqueueEvents(\n request.events.map((event) => ({\n ...event,\n origin: request.nodeId,\n }))\n );\n return {\n ok: true,\n };\n },\n pullEvents: (request) => {\n const events = db.executeKysely((db) => {\n const query = db\n .selectFrom(\"worker.crdt_events\")\n .where(\"sync_id\", \">\", request.afterSyncId)\n .where(\"status\", \"=\", \"applied\")\n .orderBy(\"sync_id\", \"asc\")\n .selectAll();\n if (request.excludeNodeId) {\n query.where(\"origin\", \"!=\", request.excludeNodeId);\n }\n return query;\n }).rows;\n\n return {\n events,\n hasMore: false,\n newSyncId: events[events.length - 1]?.sync_id ?? request.afterSyncId,\n };\n },\n };\n\n broadcastChannels.requests.onmessage = (event) => {\n const message = event.data;\n\n if (!isWorkerRequestMessage(message)) {\n return;\n }\n\n const method = rpcTarget[message.method] as () => ReturnType<\n WorkerRpc[keyof WorkerRpc]\n >;\n const data = method.apply(null, message.args as []);\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data,\n };\n broadcastChannels.responses.postMessage(response);\n };\n\n rpcTarget.postInitReady();\n}\n\nasync function getConfig(): Promise<WorkerConfig> {\n let configSet = false;\n const responsePromise = createDeferredPromise<WorkerConfig>();\n\n self.onmessage = (event: MessageEvent<unknown>) => {\n if (configSet) {\n console.error(\"Worker config already set\");\n return;\n }\n\n const message = event.data;\n if (!isWorkerInitMessage(message)) {\n return;\n }\n\n responsePromise.resolve(message.config);\n configSet = true;\n };\n\n return responsePromise.promise;\n}\n\ntype WorkerOptions = {\n migrations: Record<number, Migration>;\n logger?: Logger;\n createRemoteSource?: CreateRemoteSourceFactory;\n};\n\ntype CreateRemoteSourceFactory = (opts: {\n onEventsAvailable: (newSyncId: number) => void;\n}) => {\n pullEvents: (request: EventsPullRequest) => Promise<EventsPullResponse>;\n pushEvents: (request: EventsPushRequest) => Promise<EventsPushResponse>;\n};\n\nexport async function startDbWorker(opts: WorkerOptions) {\n const config = await getConfig();\n\n await navigator.locks.request(\n syncDbWorkerLockName,\n { mode: \"exclusive\" },\n async (lock) => {\n if (!lock) {\n return;\n }\n\n await createDbWorker(config, opts);\n\n await new Promise<void>(() => {});\n }\n );\n\n console.error(\"Failed to acquire lock\");\n}\n\nfunction getLatestSyncId(db: SQLiteDbWrapper<WorkerDbSchema>) {\n const result = db.executePrepared(\"get-latest-sync-id\", {}, (db) =>\n db\n .selectFrom(\"worker.crdt_events\")\n .select((eb) => eb.fn.max(\"sync_id\").as(\"sync_id\"))\n );\n return result[0]?.sync_id ?? 0;\n}\n\nfunction persistEvents(\n db: SQLiteDbWrapper<WorkerDbSchema>,\n events: PersistedCrdtEvent[]\n) {\n db.executeTransaction((db) => {\n const chunkSize = 100;\n for (let i = 0; i < events.length; i += chunkSize) {\n const chunk = events.slice(i, i + chunkSize);\n db.executeKysely((db) =>\n db.insertInto(\"worker.crdt_events\").values(chunk)\n );\n }\n });\n}\n\nfunction popPendingEventsBatch(\n db: SQLiteDbWrapper<WorkerDbSchema>,\n limit: number\n) {\n const events = db.executePrepared(\n \"pop-enqueued-crdt-events\",\n {\n limit: limit,\n },\n (db, param) =>\n db\n .selectFrom(\"worker.crdt_events\")\n .where(\"status\", \"=\", sql.lit(\"pending\"))\n .limit(param(\"limit\"))\n .orderBy(\"sync_id\", \"asc\")\n .selectAll()\n );\n return {\n events,\n hasMore: events.length === limit,\n };\n}\n\nfunction updateEventStatus(\n db: SQLiteDbWrapper<WorkerDbSchema>,\n syncId: number,\n status: CrdtEventStatus\n) {\n db.executePrepared(\n \"update-crdt-event-status\",\n { syncId, status },\n (db, params) =>\n db\n .updateTable(\"worker.crdt_events\")\n .set({ status: params(\"status\") })\n .where(\"sync_id\", \"=\", params(\"syncId\"))\n );\n}\n\nfunction getMetaValue(db: SQLiteDbWrapper<WorkerDbSchema>, key: string) {\n const [result] = db.executePrepared(\"get-meta-value\", { key }, (db, params) =>\n db\n .selectFrom(\"worker.meta\")\n .where(\"key\", \"=\", params(\"key\"))\n .select(\"value\")\n );\n return result?.value ?? null;\n}\n\nfunction setMetaValue(\n db: SQLiteDbWrapper<WorkerDbSchema>,\n key: string,\n value: string\n) {\n db.executePrepared(\"set-meta-value\", { key, value }, (db, params) =>\n db\n .insertInto(\"worker.meta\")\n .values({ key: params(\"key\"), value: params(\"value\") })\n .onConflict((oc) => oc.doUpdateSet({ value: params(\"value\") }))\n );\n}\n\n"],"mappings":"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","names":["db"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@sqlite-sync/core",
3
+ "version": "0.0.1",
4
+ "description": "SQLite synchronization library with CRDT support",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/krolebord/sqlite-sync.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "keywords": [
13
+ "sqlite",
14
+ "sync",
15
+ "crdt",
16
+ "offline-first",
17
+ "local-first"
18
+ ],
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./worker": {
28
+ "types": "./dist/worker.d.ts",
29
+ "import": "./dist/worker.js"
30
+ },
31
+ "./server": {
32
+ "types": "./dist/server.d.ts",
33
+ "import": "./dist/server.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "dependencies": {
40
+ "@sqlite.org/sqlite-wasm": "3.51.1-build2",
41
+ "broadcast-channel": "^7.2.0",
42
+ "kysely": "^0.28.8",
43
+ "zod": "^4.1.13"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.10.2",
47
+ "tsup": "^8.3.5",
48
+ "typescript": "~5.9.3"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "dev": "tsup --watch",
53
+ "typecheck": "tsc --noEmit"
54
+ }
55
+ }