@sqlite-sync/core 0.9.4 → 0.10.0
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.
- package/dist/admit-client-events-BDrJw4SK.js +35 -0
- package/dist/admit-client-events-BDrJw4SK.js.map +1 -0
- package/dist/crdt-sync-remote-source-Dtvyrk0R.js +504 -0
- package/dist/crdt-sync-remote-source-Dtvyrk0R.js.map +1 -0
- package/dist/{utils-Dr5JwcTP.js → crdt-table-schema-2w_AIpsV.js} +9 -2
- package/dist/crdt-table-schema-2w_AIpsV.js.map +1 -0
- package/dist/{index-Dbbd4flq.d.ts → index-B4s8GFWF.d.ts} +33 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -48
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +3 -3
- package/dist/{make-crdt-table-CvpTHmRY.js → make-crdt-table-D8VQ-ptj.js} +23 -14
- package/dist/make-crdt-table-D8VQ-ptj.js.map +1 -0
- package/dist/{reset-state-4Hn95CKG.js → reset-state-BVWEcT_z.js} +39 -429
- package/dist/reset-state-BVWEcT_z.js.map +1 -0
- package/dist/server.d.ts +30 -2
- package/dist/server.js +103 -3
- package/dist/server.js.map +1 -1
- package/dist/{system-schema-CkglcfTS.js → system-schema-C2JZDv6L.js} +3 -10
- package/dist/system-schema-C2JZDv6L.js.map +1 -0
- package/dist/worker.d.ts +3 -2
- package/dist/worker.js +52 -8
- package/dist/worker.js.map +1 -1
- package/package.json +2 -1
- package/dist/make-crdt-table-CvpTHmRY.js.map +0 -1
- package/dist/reset-state-4Hn95CKG.js.map +0 -1
- package/dist/system-schema-CkglcfTS.js.map +0 -1
- package/dist/utils-Dr5JwcTP.js.map +0 -1
package/dist/worker.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { E as createImportData, _ as formatSchemaVerificationIssues, a as createBroadcastChannels, b as createMigrator, d as syncDbClientLockName, f as syncDbWorkerLockName, i as createResetStateStore, l as isWorkerRequestMessage, m as createCrdtStorage, n as createIdbResetStore, p as createCrdtSyncProducer, r as createReloadRequestHandler, s as isWorkerInitMessage, v as verifySyncSchema, w as xxhash, x as SQLiteDbWrapper } from "./reset-state-BVWEcT_z.js";
|
|
2
|
+
import { a as createDeferredPromise, l as jsonSafeParse } from "./crdt-table-schema-2w_AIpsV.js";
|
|
3
|
+
import { a as HLCCounter, t as createCrdtSyncRemoteSource } from "./crdt-sync-remote-source-Dtvyrk0R.js";
|
|
4
|
+
import { n as applyWorkerDbSchema, s as workerDbConfig } from "./system-schema-C2JZDv6L.js";
|
|
4
5
|
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
5
6
|
//#region src/web-socket/ws-remote-source.ts
|
|
6
7
|
const createWsRemoteSource = ({ createWebSocket }) => {
|
|
7
|
-
return async ({ onEventsAvailable }) => {
|
|
8
|
+
return async ({ onEventsAvailable, onReconnected, signal }) => {
|
|
8
9
|
const socket = createWebSocket();
|
|
9
10
|
const openPromise = createDeferredPromise({
|
|
10
11
|
timeout: 5e3,
|
|
@@ -15,11 +16,47 @@ const createWsRemoteSource = ({ createWebSocket }) => {
|
|
|
15
16
|
socket.addEventListener("open", () => {
|
|
16
17
|
openPromise.resolve(void 0);
|
|
17
18
|
});
|
|
18
|
-
|
|
19
|
+
const abortOpen = () => {
|
|
20
|
+
socket.close();
|
|
21
|
+
openPromise.reject(signal.reason);
|
|
22
|
+
};
|
|
23
|
+
signal.addEventListener("abort", abortOpen, { once: true });
|
|
24
|
+
try {
|
|
25
|
+
await openPromise.promise;
|
|
26
|
+
} finally {
|
|
27
|
+
signal.removeEventListener("abort", abortOpen);
|
|
28
|
+
}
|
|
29
|
+
socket.addEventListener("open", () => {
|
|
30
|
+
onReconnected();
|
|
31
|
+
});
|
|
32
|
+
const waitForOpen = () => new Promise((resolve, reject) => {
|
|
33
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
34
|
+
resolve();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (signal.aborted) {
|
|
38
|
+
reject(signal.reason);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const onOpen = () => {
|
|
42
|
+
signal.removeEventListener("abort", onAbort);
|
|
43
|
+
resolve();
|
|
44
|
+
};
|
|
45
|
+
const onAbort = () => {
|
|
46
|
+
socket.removeEventListener("open", onOpen);
|
|
47
|
+
reject(signal.reason);
|
|
48
|
+
};
|
|
49
|
+
socket.addEventListener("open", onOpen, { once: true });
|
|
50
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
51
|
+
});
|
|
19
52
|
const requestsMap = /* @__PURE__ */ new Map();
|
|
20
53
|
const pushEvents = async (request) => {
|
|
54
|
+
await waitForOpen();
|
|
21
55
|
const requestId = crypto.randomUUID();
|
|
22
|
-
const promise = createDeferredPromise({
|
|
56
|
+
const promise = createDeferredPromise({
|
|
57
|
+
timeout: 5e3,
|
|
58
|
+
onTimeout: () => requestsMap.delete(requestId)
|
|
59
|
+
});
|
|
23
60
|
requestsMap.set(requestId, promise);
|
|
24
61
|
const wsRequest = {
|
|
25
62
|
type: "push-events",
|
|
@@ -31,8 +68,12 @@ const createWsRemoteSource = ({ createWebSocket }) => {
|
|
|
31
68
|
return promise.promise;
|
|
32
69
|
};
|
|
33
70
|
const pullEvents = async (request) => {
|
|
71
|
+
await waitForOpen();
|
|
34
72
|
const requestId = crypto.randomUUID();
|
|
35
|
-
const promise = createDeferredPromise({
|
|
73
|
+
const promise = createDeferredPromise({
|
|
74
|
+
timeout: 2e3,
|
|
75
|
+
onTimeout: () => requestsMap.delete(requestId)
|
|
76
|
+
});
|
|
36
77
|
requestsMap.set(requestId, promise);
|
|
37
78
|
const wsRequest = {
|
|
38
79
|
type: "pull-events",
|
|
@@ -327,6 +368,9 @@ async function createDbWorker(config, opts) {
|
|
|
327
368
|
},
|
|
328
369
|
goOnline: () => remoteSource.goOnline(),
|
|
329
370
|
goOffline: () => remoteSource.goOffline("DISCONNECTED"),
|
|
371
|
+
sync: () => {
|
|
372
|
+
remoteSource.syncWithRemote();
|
|
373
|
+
},
|
|
330
374
|
requestReload: createReloadRequestHandler({
|
|
331
375
|
resetState,
|
|
332
376
|
broadcast: broadcastNotification
|
|
@@ -378,7 +422,7 @@ async function createDbWorker(config, opts) {
|
|
|
378
422
|
}
|
|
379
423
|
function createRemoteSource({ clientId, crdtStorage, migrator, pullSyncId, pushSyncId, remoteFactory }) {
|
|
380
424
|
return createCrdtSyncRemoteSource({
|
|
381
|
-
bufferSize:
|
|
425
|
+
bufferSize: 75,
|
|
382
426
|
pullSyncId,
|
|
383
427
|
pushSyncId,
|
|
384
428
|
nodeId: clientId,
|
package/dist/worker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.js","names":[],"sources":["../src/web-socket/ws-remote-source.ts","../src/worker-db/event-log-gc.ts","../src/worker-db/storage-version.ts","../src/worker-db/db-worker.ts"],"sourcesContent":["import type { SyncServerMessage, SyncServerRequest } from \"../server\";\nimport type { CreateRemoteSourceFactory } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport { createDeferredPromise, type DeferredPromise, jsonSafeParse } from \"../utils\";\nimport type { EventsPullRequest, EventsPushRequest, EventsPushResponse, GetEventsBatch } from \"../worker\";\n\ntype WsRemoteSourceConfig = {\n createWebSocket: () => Pick<WebSocket, \"onmessage\" | \"close\" | \"addEventListener\"> & {\n send: (data: string) => void;\n };\n};\n\nexport const createWsRemoteSource = ({ createWebSocket }: WsRemoteSourceConfig): CreateRemoteSourceFactory => {\n return async ({ onEventsAvailable }) => {\n const socket = createWebSocket();\n\n const openPromise = createDeferredPromise<void>({\n timeout: 5000,\n onTimeout: () => {\n socket.close();\n },\n });\n socket.addEventListener(\"open\", () => {\n openPromise.resolve(undefined);\n });\n await openPromise.promise;\n\n const requestsMap = new Map<string, DeferredPromise<unknown>>();\n\n const pushEvents = async (request: EventsPushRequest): Promise<EventsPushResponse> => {\n const requestId = crypto.randomUUID();\n const promise = createDeferredPromise<EventsPushResponse>({ timeout: 5000 });\n requestsMap.set(requestId, promise as DeferredPromise<unknown>);\n\n const wsRequest: SyncServerRequest = {\n type: \"push-events\",\n requestId,\n nodeId: request.nodeId,\n events: request.events,\n };\n socket.send(JSON.stringify(wsRequest));\n\n return promise.promise;\n };\n\n const pullEvents = async (request: EventsPullRequest): Promise<GetEventsBatch> => {\n const requestId = crypto.randomUUID();\n const promise = createDeferredPromise<GetEventsBatch>({ timeout: 2000 });\n requestsMap.set(requestId, promise as DeferredPromise<unknown>);\n\n const wsRequest: SyncServerRequest = {\n type: \"pull-events\",\n requestId,\n afterSyncId: request.afterSyncId,\n excludeNodeId: request.excludeNodeId,\n };\n socket.send(JSON.stringify(wsRequest));\n\n return promise.promise;\n };\n\n socket.onmessage = (event) => {\n const result = jsonSafeParse<SyncServerMessage>(event.data);\n\n if (!result.success || !(\"type\" in result.data) || !result.data.type) {\n return;\n }\n\n const message = result.data;\n\n switch (message.type) {\n case \"events-pull-response\": {\n const promise = requestsMap.get(message.requestId);\n if (!promise) {\n return;\n }\n promise.resolve(message.data);\n requestsMap.delete(message.requestId);\n break;\n }\n case \"events-push-response\": {\n const promise = requestsMap.get(message.requestId);\n if (!promise) {\n return;\n }\n promise.resolve(message.data);\n requestsMap.delete(message.requestId);\n break;\n }\n case \"events-applied\":\n onEventsAvailable({ newSyncId: message.newSyncId, remoteEventHlcSum: message.eventHlcSum });\n break;\n default:\n message satisfies never;\n return;\n }\n };\n\n return {\n pushEvents,\n pullEvents,\n disconnect: () => {\n socket.close();\n },\n };\n };\n};\n","import type { Logger } from \"../logger\";\nimport type { SystemDbConfig } from \"../migrations/system-schema\";\nimport type { StoredValue } from \"../sqlite-crdt/stored-value\";\nimport type { InternalSQLiteWrapper } from \"../sqlite-db-wrapper\";\n\nexport const WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS = 100;\n\ntype EventLogGcOptions = {\n db: InternalSQLiteWrapper<any>;\n dbConfig: SystemDbConfig;\n pushSyncId: StoredValue<number>;\n eventHlcAccumulator: StoredValue<string>;\n logger?: Logger;\n};\n\nexport function runWorkerEventLogGc({ db, dbConfig, pushSyncId, eventHlcAccumulator, logger }: EventLogGcOptions) {\n const eventsTable = dbConfig.eventsTable.fullIdentifier;\n\n if (eventHlcAccumulator.current === \"\") {\n const [{ count }] = db.executePreparedRaw<[], { count: number }>({\n key: \"event-log-gc-count-applied-events\",\n sql: `SELECT count(*) AS count FROM ${eventsTable} WHERE \"status\" = 'applied'`,\n meta: { loggerLevel: \"system\" },\n });\n if (count > 0) {\n logger?.(\n \"worker\",\n \"Skipping event log GC until the event HLC checksum has been computed from full history\",\n \"info\",\n );\n }\n return { skipped: true as const };\n }\n\n const [{ deleteBeforeSyncId }] = db.executePreparedRaw<[number], { deleteBeforeSyncId: number | null }>({\n key: \"event-log-gc-delete-before-sync-id\",\n sql: `\n SELECT min(\"sync_id\") AS deleteBeforeSyncId\n FROM (\n SELECT \"sync_id\"\n FROM ${eventsTable}\n WHERE \"status\" IN ('applied', 'deduped')\n ORDER BY \"sync_id\" DESC\n LIMIT ?\n )\n `,\n params: [WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS],\n meta: { loggerLevel: \"system\" },\n });\n\n if (deleteBeforeSyncId === null) {\n return { skipped: false as const };\n }\n\n db.executePreparedRaw<[number, number], never>({\n key: \"event-log-gc-delete-events\",\n sql: `\n DELETE FROM ${eventsTable}\n WHERE \"sync_id\" < ?\n AND \"status\" IN ('applied', 'deduped')\n AND (\"origin\" = 'remote' OR \"sync_id\" <= ?)\n `,\n params: [deleteBeforeSyncId, pushSyncId.current],\n meta: { loggerLevel: \"system\" },\n });\n\n logger?.(\"worker\", \"Event log GC completed\", \"info\");\n return { skipped: false as const };\n}\n","import type { ResetStore } from \"./reset-state\";\n\n/**\n * Internal persisted-storage format version. Bump when the library changes\n * the worker DB layout in a way old persisted databases cannot survive —\n * every client resets its local DB on the next worker start.\n */\nexport const LIB_STORAGE_VERSION = 2;\n\nconst storageVersionKey = (dbId: string) => `sqlite-sync-storage-version-${dbId}`;\n\nexport function formatStorageVersion(appStorageVersion: string | undefined): string {\n return appStorageVersion === undefined\n ? `lib-v${LIB_STORAGE_VERSION}`\n : `lib-v${LIB_STORAGE_VERSION}:app-${appStorageVersion}`;\n}\n\ntype StorageVersionStoreOptions = {\n store: ResetStore;\n dbId: string;\n /** Dev-provided app storage version, combined with the internal lib version. */\n appStorageVersion?: string;\n};\n\nexport type StorageVersionStore = ReturnType<typeof createStorageVersionStore>;\n\n/**\n * Worker-owned durable storage version. The current version combines the\n * internal lib version with the dev-provided app version; when the stored\n * version does not match (including a missing record), the elected worker\n * initializes with `clearOnInit: true`. Wiping on a missing record is\n * harmless for fresh installs and correctly resets databases persisted\n * before versioning existed.\n */\nexport function createStorageVersionStore({ store, dbId, appStorageVersion }: StorageVersionStoreOptions) {\n const key = storageVersionKey(dbId);\n const currentVersion = formatStorageVersion(appStorageVersion);\n\n return {\n currentVersion,\n async isVersionMismatch(): Promise<boolean> {\n const storedVersion = await store.get<string>(key);\n return storedVersion !== currentVersion;\n },\n /**\n * Record the current version. Must be called only after the worker has\n * successfully initialized with the wiped DB, so a failed init can be\n * retried by a later elected worker.\n */\n async markCurrentVersionApplied(): Promise<void> {\n await store.set(key, currentVersion);\n },\n };\n}\n","import sqlite3InitModule from \"@sqlite.org/sqlite-wasm\";\nimport { createImportData } from \"../export-import\";\nimport { xxhash } from \"../hash\";\nimport { HLCCounter } from \"../hlc\";\nimport type { Logger } from \"../logger\";\nimport { createMigrator, type SyncDbMigrator } from \"../migrations/migrator\";\nimport { applyWorkerDbSchema, type WorkerDbSchema, workerDbConfig } from \"../migrations/system-schema\";\nimport { formatSchemaVerificationIssues, verifySyncSchema } from \"../schema/verify-sync-schema\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { type CrdtStorage, createCrdtStorage } from \"../sqlite-crdt/crdt-storage\";\nimport { createCrdtSyncProducer } from \"../sqlite-crdt/crdt-sync-producer\";\nimport { type CreateRemoteSourceFactory, createCrdtSyncRemoteSource } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { CrdtEventStatus } from \"../sqlite-crdt/crdt-table-schema\";\nimport { SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\nimport type { KvStore } from \"../sqlite-kv-store\";\nimport { createDeferredPromise } from \"../utils\";\nimport { runWorkerEventLogGc } from \"./event-log-gc\";\nimport { createIdbResetStore, createReloadRequestHandler, createResetStateStore, type ResetStore } from \"./reset-state\";\nimport { createStorageVersionStore } from \"./storage-version\";\nimport {\n createBroadcastChannels,\n isWorkerInitMessage,\n isWorkerRequestMessage,\n syncDbClientLockName,\n syncDbWorkerLockName,\n type WorkerConfig,\n type WorkerErrorResponseMessage,\n type WorkerNotificationMessage,\n type WorkerResponseMessage,\n type WorkerRpc,\n} from \"./worker-common\";\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(config.dbId);\n const logger = opts.logger ?? defaultLogger;\n\n if (opts.verifySchema) {\n const issues = await verifySyncSchema(opts.syncDbSchema);\n if (issues.length > 0) {\n throw new Error(formatSchemaVerificationIssues(issues));\n }\n logger(\"worker\", \"Schema verification passed: migrations match the declared schema\", \"info\");\n }\n\n const [sqlite3] = await Promise.all([sqlite3InitModule(), xxhash.ensureLoaded()]);\n\n const resetStore = opts.resetStore ?? createIdbResetStore();\n const resetState = createResetStateStore({\n store: resetStore,\n dbId: config.dbId,\n });\n const storageVersion = createStorageVersionStore({\n store: resetStore,\n dbId: config.dbId,\n appStorageVersion: opts.storageVersion,\n });\n const [pendingReset, isVersionMismatch] = await Promise.all([\n resetState.resolvePendingReset(),\n storageVersion.isVersionMismatch(),\n ]);\n\n if (isVersionMismatch) {\n logger(\"worker\", `Storage version mismatch — resetting local DB to ${storageVersion.currentVersion}`, \"warning\");\n }\n\n const pool = await sqlite3.installOpfsSAHPoolVfs({\n name: config.dbId,\n directory: `.${config.dbId}`,\n clearOnInit: !!pendingReset || isVersionMismatch,\n initialCapacity: 8,\n });\n\n const db = new SQLiteDbWrapper<WorkerDbSchema>({\n db: () => new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`),\n logger: logger,\n loggerPrefix: \"worker\",\n sqlite3,\n });\n\n db.execute(\"PRAGMA locking_mode=exclusive\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA synchronous=NORMAL\", { loggerLevel: \"system\" });\n\n db.execute(`ATTACH DATABASE '/${config.dbId}-worker.db' as worker`, { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.locking_mode=exclusive\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.journal_mode=WAL\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.synchronous=NORMAL\", { loggerLevel: \"system\" });\n\n const { kvStore } = applyWorkerDbSchema(db);\n\n const migrator = createMigrator({\n migrations: opts.syncDbSchema.migrations,\n schemaVersion: kvStore.createNumberStoredValue(\"schema-version\", -1),\n updateLogTableName: workerDbConfig.updateLogTable.fullIdentifier,\n });\n migrator.migrateDbToLatest({\n startTransaction: (callback) => {\n db.executeTransaction((tx) =>\n callback({ execute: (sql, parameters, meta) => tx.execute({ sql, parameters }, meta) }),\n );\n },\n });\n db.invalidateDbSchema();\n\n const pullSyncId = kvStore.createNumberStoredValue(\"pull-sync-id\", -1);\n const pushSyncId = kvStore.createNumberStoredValue(\"push-sync-id\", -1);\n const eventHlcAccumulator = kvStore.createStringStoredValue(\"crdt.consistency.event_hlc_sum.v2\", \"\");\n\n // Record the applied reset epoch / storage version only after the wiped DB\n // initialized successfully, so a failed init can be retried by a later\n // elected worker, while a later election does not wipe again.\n if (pendingReset) {\n await resetState.markResetApplied(pendingReset.epoch);\n }\n if (isVersionMismatch) {\n await storageVersion.markCurrentVersionApplied();\n }\n\n if (opts.eventLogGc === true) {\n runWorkerEventLogGc({\n db,\n dbConfig: workerDbConfig,\n pushSyncId,\n eventHlcAccumulator,\n logger,\n });\n }\n\n const crdtStorage = createCrdtStorage({\n nodeId: config.clientId,\n initialLocalSyncId: getMaxSyncId(db, \"none\"),\n migrator,\n hlc: new HLCCounter(config.clientId, () => Date.now()),\n db,\n dbConfig: workerDbConfig,\n eventHlcAccumulator,\n schema: opts.syncDbSchema,\n });\n\n createCrdtSyncProducer({\n storage: crdtStorage,\n broadcastEvents: (chunk) => {\n broadcastChannels.responses.postMessage({\n notificationType: \"new-event-chunk-applied\",\n newSyncId: chunk.newSyncId,\n eventHlcSum: chunk.eventHlcSum,\n });\n },\n });\n\n const remoteSource = createRemoteSource({\n crdtStorage,\n migrator,\n clientId: config.clientId,\n pullSyncId,\n pushSyncId,\n remoteFactory: opts.createRemoteSource,\n });\n remoteSource.goOnline();\n\n const broadcastNotification = (notification: WorkerNotificationMessage) => {\n broadcastChannels.responses.postMessage(notification);\n };\n\n const postState = () => {\n broadcastNotification({\n notificationType: \"state-changed\",\n state: remoteSource.getState(),\n });\n };\n const stateChangedSubscription = remoteSource.addEventListener(\"state-changed\", () => {\n postState();\n });\n const deSyncDetectedSubscription = remoteSource.addEventListener(\"de-sync-detected\", (event) => {\n broadcastNotification({\n notificationType: \"de-sync-detected\",\n reason: event.payload.reason,\n });\n });\n const remoteSchemaVersionMismatchSubscription = remoteSource.addEventListener(\n \"remote-schema-version-mismatch\",\n (event) => {\n broadcastNotification({\n notificationType: \"remote-schema-version-mismatch\",\n remoteSchemaVersion: event.payload.remoteSchemaVersion,\n localSchemaVersion: event.payload.localSchemaVersion,\n });\n },\n );\n\n const rpcTarget: WorkerRpc = {\n execute: (query) => db.execute(query),\n getSnapshot: () => {\n const appliedSyncId = getMaxSyncId(db, \"pending\");\n db.execute(\"PRAGMA journal_mode=off\", { loggerLevel: \"system\" });\n const file = db.createSnapshot();\n db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n return {\n file,\n syncId: appliedSyncId,\n schemaVersion: migrator.currentSchemaVersion,\n };\n },\n importData: createImportData({\n migrator,\n tablesConfig: opts.syncDbSchema.tablesConfig,\n applyEvents: async (events) => {\n crdtStorage.applyOwnEvents(events);\n await crdtStorage.internal.processEnqueuedEvents();\n },\n }),\n postState,\n pushTabEvents: (request) => {\n const { beforeSyncId, afterSyncId } = crdtStorage.enqueueLocalEvents(request.events, request.nodeId);\n return {\n ok: true,\n beforeSyncId,\n afterSyncId,\n };\n },\n pullEvents: (request) => {\n return crdtStorage.getEventsBatch({\n afterSyncId: request.afterSyncId,\n status: \"applied\",\n excludeNodeId: request.excludeNodeId ?? \"\",\n limit: 100,\n });\n },\n goOnline: () => remoteSource.goOnline(),\n goOffline: () => remoteSource.goOffline(\"DISCONNECTED\"),\n requestReload: createReloadRequestHandler({\n resetState,\n broadcast: broadcastNotification,\n }),\n };\n\n broadcastChannels.requests.onmessage = (event) => {\n const message = event.data;\n\n if (!isWorkerRequestMessage(message)) {\n return;\n }\n\n const sendError = (error: unknown) => {\n const response: WorkerErrorResponseMessage = {\n type: \"error-response\",\n requestId: message.requestId,\n error: error instanceof Error ? error.message : String(error),\n };\n broadcastChannels.responses.postMessage(response);\n };\n\n try {\n const method = rpcTarget[message.method] as () => ReturnType<WorkerRpc[keyof WorkerRpc]>;\n const data = method.apply(null, message.args as []);\n\n if (data instanceof Promise) {\n data\n .then((result) => {\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data: result,\n };\n broadcastChannels.responses.postMessage(response);\n })\n .catch(sendError);\n } else {\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data,\n };\n broadcastChannels.responses.postMessage(response);\n }\n } catch (error) {\n sendError(error);\n }\n };\n\n rpcTarget.postState();\n\n return async () => {\n stateChangedSubscription.unsubscribe();\n deSyncDetectedSubscription.unsubscribe();\n remoteSchemaVersionMismatchSubscription.unsubscribe();\n await remoteSource.dispose();\n broadcastChannels.requests.close();\n broadcastChannels.responses.close();\n db.close();\n };\n}\n\ntype InitRemoteOptions = {\n clientId: string;\n crdtStorage: CrdtStorage;\n migrator: SyncDbMigrator;\n pullSyncId: ReturnType<KvStore[\"createNumberStoredValue\"]>;\n pushSyncId: ReturnType<KvStore[\"createNumberStoredValue\"]>;\n remoteFactory?: CreateRemoteSourceFactory;\n};\n\nfunction createRemoteSource({\n clientId,\n crdtStorage,\n migrator,\n pullSyncId,\n pushSyncId,\n remoteFactory,\n}: InitRemoteOptions) {\n return createCrdtSyncRemoteSource({\n bufferSize: 50,\n pullSyncId,\n pushSyncId,\n nodeId: clientId,\n storage: crdtStorage,\n migrator,\n remoteFactory,\n });\n}\n\nexport async function getWorkerConfig<Props = never>(): Promise<WorkerConfig<Props>> {\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 syncDbSchema: SyncDbSchema;\n logger?: Logger;\n createRemoteSource?: CreateRemoteSourceFactory;\n workerConfig?: WorkerConfig;\n /** Durable storage for reset state. Defaults to an IndexedDB-backed store. */\n resetStore?: ResetStore;\n /**\n * App-provided storage version, combined with the library's internal storage\n * version. Bump it when deploying a code change that old persisted local DBs\n * cannot survive — on mismatch the elected worker wipes the local DB on startup.\n */\n storageVersion?: string;\n /**\n * Dev-time drift check: replays the full migration history on a throwaway\n * in-memory database and verifies the result matches the schema declared with\n * `defineSyncSchema`. On mismatch the worker throws and refuses to start.\n * Enable in development only, e.g. `verifySchema: import.meta.env.DEV`.\n */\n verifySchema?: boolean;\n /**\n * Runs local worker event-log garbage collection on startup. Disabled by default;\n * set `eventLogGc: true` to enable. GC keeps the latest event rows and never removes\n * pending events or local rows that have not been pushed to the remote.\n */\n eventLogGc?: boolean;\n};\n\nexport async function startDbWorker(opts: WorkerOptions) {\n const config = opts.workerConfig ?? (await getWorkerConfig());\n\n await navigator.locks.request(`${syncDbWorkerLockName}-${config.dbId}`, { mode: \"exclusive\" }, async (lock) => {\n if (!lock) {\n return;\n }\n\n const cleanup = await createDbWorker(config, opts);\n\n const clientLockName = `${syncDbClientLockName}-${config.dbId}`;\n await new Promise<void>((resolve) => {\n const interval = setInterval(async () => {\n const { held } = await navigator.locks.query();\n const hasClients = held?.some((l) => l.name === clientLockName && l.mode === \"shared\");\n if (!hasClients) {\n clearInterval(interval);\n resolve();\n }\n }, 5_000);\n });\n\n await cleanup();\n });\n\n self.close();\n}\n\nfunction getMaxSyncId(db: SQLiteDbWrapper<WorkerDbSchema>, excludingStatus: \"none\" | \"pending\") {\n const [result] = db.executePrepared(\n \"get-max-sync-id\",\n { excludingStatus: excludingStatus as CrdtEventStatus },\n (db, params) =>\n db\n .selectFrom(\"worker.crdt_events\")\n .where(\"status\", \"!=\", params(\"excludingStatus\"))\n .select((eb) => eb.fn.max(\"sync_id\").as(\"sync_id\")),\n { loggerLevel: \"system\" },\n );\n\n return result?.sync_id ?? 0;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"worker.js","names":[],"sources":["../src/web-socket/ws-remote-source.ts","../src/worker-db/event-log-gc.ts","../src/worker-db/storage-version.ts","../src/worker-db/db-worker.ts"],"sourcesContent":["import type { SyncServerMessage, SyncServerRequest } from \"../server\";\nimport type { CreateRemoteSourceFactory } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport { createDeferredPromise, type DeferredPromise, jsonSafeParse } from \"../utils\";\nimport type { EventsPullRequest, EventsPushRequest, EventsPushResponse, GetEventsBatch } from \"../worker\";\n\ntype WsRemoteSourceConfig = {\n createWebSocket: () => Pick<WebSocket, \"onmessage\" | \"close\" | \"addEventListener\" | \"removeEventListener\"> & {\n readyState: number;\n send: (data: string) => void;\n };\n};\n\nexport const createWsRemoteSource = ({ createWebSocket }: WsRemoteSourceConfig): CreateRemoteSourceFactory => {\n return async ({ onEventsAvailable, onReconnected, signal }) => {\n const socket = createWebSocket();\n\n const openPromise = createDeferredPromise<void>({\n timeout: 5000,\n onTimeout: () => {\n socket.close();\n },\n });\n socket.addEventListener(\"open\", () => {\n openPromise.resolve(undefined);\n });\n const abortOpen = () => {\n socket.close();\n openPromise.reject(signal.reason);\n };\n signal.addEventListener(\"abort\", abortOpen, { once: true });\n try {\n await openPromise.promise;\n } finally {\n signal.removeEventListener(\"abort\", abortOpen);\n }\n\n socket.addEventListener(\"open\", () => {\n onReconnected();\n });\n\n const waitForOpen = () =>\n new Promise<void>((resolve, reject) => {\n if (socket.readyState === WebSocket.OPEN) {\n resolve();\n return;\n }\n if (signal.aborted) {\n reject(signal.reason);\n return;\n }\n const onOpen = () => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n };\n const onAbort = () => {\n socket.removeEventListener(\"open\", onOpen);\n reject(signal.reason);\n };\n socket.addEventListener(\"open\", onOpen, { once: true });\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n\n const requestsMap = new Map<string, DeferredPromise<unknown>>();\n\n const pushEvents = async (request: EventsPushRequest): Promise<EventsPushResponse> => {\n await waitForOpen();\n const requestId = crypto.randomUUID();\n const promise = createDeferredPromise<EventsPushResponse>({\n timeout: 5000,\n onTimeout: () => requestsMap.delete(requestId),\n });\n requestsMap.set(requestId, promise as DeferredPromise<unknown>);\n\n const wsRequest: SyncServerRequest = {\n type: \"push-events\",\n requestId,\n nodeId: request.nodeId,\n events: request.events,\n };\n socket.send(JSON.stringify(wsRequest));\n\n return promise.promise;\n };\n\n const pullEvents = async (request: EventsPullRequest): Promise<GetEventsBatch> => {\n await waitForOpen();\n const requestId = crypto.randomUUID();\n const promise = createDeferredPromise<GetEventsBatch>({\n timeout: 2000,\n onTimeout: () => requestsMap.delete(requestId),\n });\n requestsMap.set(requestId, promise as DeferredPromise<unknown>);\n\n const wsRequest: SyncServerRequest = {\n type: \"pull-events\",\n requestId,\n afterSyncId: request.afterSyncId,\n excludeNodeId: request.excludeNodeId,\n };\n socket.send(JSON.stringify(wsRequest));\n\n return promise.promise;\n };\n\n socket.onmessage = (event) => {\n const result = jsonSafeParse<SyncServerMessage>(event.data);\n\n if (!result.success || !(\"type\" in result.data) || !result.data.type) {\n return;\n }\n\n const message = result.data;\n\n switch (message.type) {\n case \"events-pull-response\": {\n const promise = requestsMap.get(message.requestId);\n if (!promise) {\n return;\n }\n promise.resolve(message.data);\n requestsMap.delete(message.requestId);\n break;\n }\n case \"events-push-response\": {\n const promise = requestsMap.get(message.requestId);\n if (!promise) {\n return;\n }\n promise.resolve(message.data);\n requestsMap.delete(message.requestId);\n break;\n }\n case \"events-applied\":\n onEventsAvailable({ newSyncId: message.newSyncId, remoteEventHlcSum: message.eventHlcSum });\n break;\n default:\n message satisfies never;\n return;\n }\n };\n\n return {\n pushEvents,\n pullEvents,\n disconnect: () => {\n socket.close();\n },\n };\n };\n};\n","import type { Logger } from \"../logger\";\nimport type { SystemDbConfig } from \"../migrations/system-schema\";\nimport type { StoredValue } from \"../sqlite-crdt/stored-value\";\nimport type { InternalSQLiteWrapper } from \"../sqlite-db-wrapper\";\n\nexport const WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS = 100;\n\ntype EventLogGcOptions = {\n db: InternalSQLiteWrapper<any>;\n dbConfig: SystemDbConfig;\n pushSyncId: StoredValue<number>;\n eventHlcAccumulator: StoredValue<string>;\n logger?: Logger;\n};\n\nexport function runWorkerEventLogGc({ db, dbConfig, pushSyncId, eventHlcAccumulator, logger }: EventLogGcOptions) {\n const eventsTable = dbConfig.eventsTable.fullIdentifier;\n\n if (eventHlcAccumulator.current === \"\") {\n const [{ count }] = db.executePreparedRaw<[], { count: number }>({\n key: \"event-log-gc-count-applied-events\",\n sql: `SELECT count(*) AS count FROM ${eventsTable} WHERE \"status\" = 'applied'`,\n meta: { loggerLevel: \"system\" },\n });\n if (count > 0) {\n logger?.(\n \"worker\",\n \"Skipping event log GC until the event HLC checksum has been computed from full history\",\n \"info\",\n );\n }\n return { skipped: true as const };\n }\n\n const [{ deleteBeforeSyncId }] = db.executePreparedRaw<[number], { deleteBeforeSyncId: number | null }>({\n key: \"event-log-gc-delete-before-sync-id\",\n sql: `\n SELECT min(\"sync_id\") AS deleteBeforeSyncId\n FROM (\n SELECT \"sync_id\"\n FROM ${eventsTable}\n WHERE \"status\" IN ('applied', 'deduped')\n ORDER BY \"sync_id\" DESC\n LIMIT ?\n )\n `,\n params: [WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS],\n meta: { loggerLevel: \"system\" },\n });\n\n if (deleteBeforeSyncId === null) {\n return { skipped: false as const };\n }\n\n db.executePreparedRaw<[number, number], never>({\n key: \"event-log-gc-delete-events\",\n sql: `\n DELETE FROM ${eventsTable}\n WHERE \"sync_id\" < ?\n AND \"status\" IN ('applied', 'deduped')\n AND (\"origin\" = 'remote' OR \"sync_id\" <= ?)\n `,\n params: [deleteBeforeSyncId, pushSyncId.current],\n meta: { loggerLevel: \"system\" },\n });\n\n logger?.(\"worker\", \"Event log GC completed\", \"info\");\n return { skipped: false as const };\n}\n","import type { ResetStore } from \"./reset-state\";\n\n/**\n * Internal persisted-storage format version. Bump when the library changes\n * the worker DB layout in a way old persisted databases cannot survive —\n * every client resets its local DB on the next worker start.\n */\nexport const LIB_STORAGE_VERSION = 2;\n\nconst storageVersionKey = (dbId: string) => `sqlite-sync-storage-version-${dbId}`;\n\nexport function formatStorageVersion(appStorageVersion: string | undefined): string {\n return appStorageVersion === undefined\n ? `lib-v${LIB_STORAGE_VERSION}`\n : `lib-v${LIB_STORAGE_VERSION}:app-${appStorageVersion}`;\n}\n\ntype StorageVersionStoreOptions = {\n store: ResetStore;\n dbId: string;\n /** Dev-provided app storage version, combined with the internal lib version. */\n appStorageVersion?: string;\n};\n\nexport type StorageVersionStore = ReturnType<typeof createStorageVersionStore>;\n\n/**\n * Worker-owned durable storage version. The current version combines the\n * internal lib version with the dev-provided app version; when the stored\n * version does not match (including a missing record), the elected worker\n * initializes with `clearOnInit: true`. Wiping on a missing record is\n * harmless for fresh installs and correctly resets databases persisted\n * before versioning existed.\n */\nexport function createStorageVersionStore({ store, dbId, appStorageVersion }: StorageVersionStoreOptions) {\n const key = storageVersionKey(dbId);\n const currentVersion = formatStorageVersion(appStorageVersion);\n\n return {\n currentVersion,\n async isVersionMismatch(): Promise<boolean> {\n const storedVersion = await store.get<string>(key);\n return storedVersion !== currentVersion;\n },\n /**\n * Record the current version. Must be called only after the worker has\n * successfully initialized with the wiped DB, so a failed init can be\n * retried by a later elected worker.\n */\n async markCurrentVersionApplied(): Promise<void> {\n await store.set(key, currentVersion);\n },\n };\n}\n","import sqlite3InitModule from \"@sqlite.org/sqlite-wasm\";\nimport { createImportData } from \"../export-import\";\nimport { xxhash } from \"../hash\";\nimport { HLCCounter } from \"../hlc\";\nimport type { Logger } from \"../logger\";\nimport { createMigrator, type SyncDbMigrator } from \"../migrations/migrator\";\nimport { applyWorkerDbSchema, type WorkerDbSchema, workerDbConfig } from \"../migrations/system-schema\";\nimport { formatSchemaVerificationIssues, verifySyncSchema } from \"../schema/verify-sync-schema\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { type CrdtStorage, createCrdtStorage } from \"../sqlite-crdt/crdt-storage\";\nimport { createCrdtSyncProducer } from \"../sqlite-crdt/crdt-sync-producer\";\nimport {\n type CreateRemoteSourceFactory,\n createCrdtSyncRemoteSource,\n PUSH_BATCH_SIZE,\n} from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { CrdtEventStatus } from \"../sqlite-crdt/crdt-table-schema\";\nimport { SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\nimport type { KvStore } from \"../sqlite-kv-store\";\nimport { createDeferredPromise } from \"../utils\";\nimport { runWorkerEventLogGc } from \"./event-log-gc\";\nimport { createIdbResetStore, createReloadRequestHandler, createResetStateStore, type ResetStore } from \"./reset-state\";\nimport { createStorageVersionStore } from \"./storage-version\";\nimport {\n createBroadcastChannels,\n isWorkerInitMessage,\n isWorkerRequestMessage,\n syncDbClientLockName,\n syncDbWorkerLockName,\n type WorkerConfig,\n type WorkerErrorResponseMessage,\n type WorkerNotificationMessage,\n type WorkerResponseMessage,\n type WorkerRpc,\n} from \"./worker-common\";\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(config.dbId);\n const logger = opts.logger ?? defaultLogger;\n\n if (opts.verifySchema) {\n const issues = await verifySyncSchema(opts.syncDbSchema);\n if (issues.length > 0) {\n throw new Error(formatSchemaVerificationIssues(issues));\n }\n logger(\"worker\", \"Schema verification passed: migrations match the declared schema\", \"info\");\n }\n\n const [sqlite3] = await Promise.all([sqlite3InitModule(), xxhash.ensureLoaded()]);\n\n const resetStore = opts.resetStore ?? createIdbResetStore();\n const resetState = createResetStateStore({\n store: resetStore,\n dbId: config.dbId,\n });\n const storageVersion = createStorageVersionStore({\n store: resetStore,\n dbId: config.dbId,\n appStorageVersion: opts.storageVersion,\n });\n const [pendingReset, isVersionMismatch] = await Promise.all([\n resetState.resolvePendingReset(),\n storageVersion.isVersionMismatch(),\n ]);\n\n if (isVersionMismatch) {\n logger(\"worker\", `Storage version mismatch — resetting local DB to ${storageVersion.currentVersion}`, \"warning\");\n }\n\n const pool = await sqlite3.installOpfsSAHPoolVfs({\n name: config.dbId,\n directory: `.${config.dbId}`,\n clearOnInit: !!pendingReset || isVersionMismatch,\n initialCapacity: 8,\n });\n\n const db = new SQLiteDbWrapper<WorkerDbSchema>({\n db: () => new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`),\n logger: logger,\n loggerPrefix: \"worker\",\n sqlite3,\n });\n\n db.execute(\"PRAGMA locking_mode=exclusive\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA synchronous=NORMAL\", { loggerLevel: \"system\" });\n\n db.execute(`ATTACH DATABASE '/${config.dbId}-worker.db' as worker`, { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.locking_mode=exclusive\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.journal_mode=WAL\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.synchronous=NORMAL\", { loggerLevel: \"system\" });\n\n const { kvStore } = applyWorkerDbSchema(db);\n\n const migrator = createMigrator({\n migrations: opts.syncDbSchema.migrations,\n schemaVersion: kvStore.createNumberStoredValue(\"schema-version\", -1),\n updateLogTableName: workerDbConfig.updateLogTable.fullIdentifier,\n });\n migrator.migrateDbToLatest({\n startTransaction: (callback) => {\n db.executeTransaction((tx) =>\n callback({ execute: (sql, parameters, meta) => tx.execute({ sql, parameters }, meta) }),\n );\n },\n });\n db.invalidateDbSchema();\n\n const pullSyncId = kvStore.createNumberStoredValue(\"pull-sync-id\", -1);\n const pushSyncId = kvStore.createNumberStoredValue(\"push-sync-id\", -1);\n const eventHlcAccumulator = kvStore.createStringStoredValue(\"crdt.consistency.event_hlc_sum.v2\", \"\");\n\n // Record the applied reset epoch / storage version only after the wiped DB\n // initialized successfully, so a failed init can be retried by a later\n // elected worker, while a later election does not wipe again.\n if (pendingReset) {\n await resetState.markResetApplied(pendingReset.epoch);\n }\n if (isVersionMismatch) {\n await storageVersion.markCurrentVersionApplied();\n }\n\n if (opts.eventLogGc === true) {\n runWorkerEventLogGc({\n db,\n dbConfig: workerDbConfig,\n pushSyncId,\n eventHlcAccumulator,\n logger,\n });\n }\n\n const crdtStorage = createCrdtStorage({\n nodeId: config.clientId,\n initialLocalSyncId: getMaxSyncId(db, \"none\"),\n migrator,\n hlc: new HLCCounter(config.clientId, () => Date.now()),\n db,\n dbConfig: workerDbConfig,\n eventHlcAccumulator,\n schema: opts.syncDbSchema,\n });\n\n createCrdtSyncProducer({\n storage: crdtStorage,\n broadcastEvents: (chunk) => {\n broadcastChannels.responses.postMessage({\n notificationType: \"new-event-chunk-applied\",\n newSyncId: chunk.newSyncId,\n eventHlcSum: chunk.eventHlcSum,\n });\n },\n });\n\n const remoteSource = createRemoteSource({\n crdtStorage,\n migrator,\n clientId: config.clientId,\n pullSyncId,\n pushSyncId,\n remoteFactory: opts.createRemoteSource,\n });\n remoteSource.goOnline();\n\n const broadcastNotification = (notification: WorkerNotificationMessage) => {\n broadcastChannels.responses.postMessage(notification);\n };\n\n const postState = () => {\n broadcastNotification({\n notificationType: \"state-changed\",\n state: remoteSource.getState(),\n });\n };\n const stateChangedSubscription = remoteSource.addEventListener(\"state-changed\", () => {\n postState();\n });\n const deSyncDetectedSubscription = remoteSource.addEventListener(\"de-sync-detected\", (event) => {\n broadcastNotification({\n notificationType: \"de-sync-detected\",\n reason: event.payload.reason,\n });\n });\n const remoteSchemaVersionMismatchSubscription = remoteSource.addEventListener(\n \"remote-schema-version-mismatch\",\n (event) => {\n broadcastNotification({\n notificationType: \"remote-schema-version-mismatch\",\n remoteSchemaVersion: event.payload.remoteSchemaVersion,\n localSchemaVersion: event.payload.localSchemaVersion,\n });\n },\n );\n\n const rpcTarget: WorkerRpc = {\n execute: (query) => db.execute(query),\n getSnapshot: () => {\n const appliedSyncId = getMaxSyncId(db, \"pending\");\n db.execute(\"PRAGMA journal_mode=off\", { loggerLevel: \"system\" });\n const file = db.createSnapshot();\n db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n return {\n file,\n syncId: appliedSyncId,\n schemaVersion: migrator.currentSchemaVersion,\n };\n },\n importData: createImportData({\n migrator,\n tablesConfig: opts.syncDbSchema.tablesConfig,\n applyEvents: async (events) => {\n crdtStorage.applyOwnEvents(events);\n await crdtStorage.internal.processEnqueuedEvents();\n },\n }),\n postState,\n pushTabEvents: (request) => {\n const { beforeSyncId, afterSyncId } = crdtStorage.enqueueLocalEvents(request.events, request.nodeId);\n return {\n ok: true,\n beforeSyncId,\n afterSyncId,\n };\n },\n pullEvents: (request) => {\n return crdtStorage.getEventsBatch({\n afterSyncId: request.afterSyncId,\n status: \"applied\",\n excludeNodeId: request.excludeNodeId ?? \"\",\n limit: 100,\n });\n },\n goOnline: () => remoteSource.goOnline(),\n goOffline: () => remoteSource.goOffline(\"DISCONNECTED\"),\n sync: () => {\n void remoteSource.syncWithRemote();\n },\n requestReload: createReloadRequestHandler({\n resetState,\n broadcast: broadcastNotification,\n }),\n };\n\n broadcastChannels.requests.onmessage = (event) => {\n const message = event.data;\n\n if (!isWorkerRequestMessage(message)) {\n return;\n }\n\n const sendError = (error: unknown) => {\n const response: WorkerErrorResponseMessage = {\n type: \"error-response\",\n requestId: message.requestId,\n error: error instanceof Error ? error.message : String(error),\n };\n broadcastChannels.responses.postMessage(response);\n };\n\n try {\n const method = rpcTarget[message.method] as () => ReturnType<WorkerRpc[keyof WorkerRpc]>;\n const data = method.apply(null, message.args as []);\n\n if (data instanceof Promise) {\n data\n .then((result) => {\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data: result,\n };\n broadcastChannels.responses.postMessage(response);\n })\n .catch(sendError);\n } else {\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data,\n };\n broadcastChannels.responses.postMessage(response);\n }\n } catch (error) {\n sendError(error);\n }\n };\n\n rpcTarget.postState();\n\n return async () => {\n stateChangedSubscription.unsubscribe();\n deSyncDetectedSubscription.unsubscribe();\n remoteSchemaVersionMismatchSubscription.unsubscribe();\n await remoteSource.dispose();\n broadcastChannels.requests.close();\n broadcastChannels.responses.close();\n db.close();\n };\n}\n\ntype InitRemoteOptions = {\n clientId: string;\n crdtStorage: CrdtStorage;\n migrator: SyncDbMigrator;\n pullSyncId: ReturnType<KvStore[\"createNumberStoredValue\"]>;\n pushSyncId: ReturnType<KvStore[\"createNumberStoredValue\"]>;\n remoteFactory?: CreateRemoteSourceFactory;\n};\n\nfunction createRemoteSource({\n clientId,\n crdtStorage,\n migrator,\n pullSyncId,\n pushSyncId,\n remoteFactory,\n}: InitRemoteOptions) {\n return createCrdtSyncRemoteSource({\n bufferSize: PUSH_BATCH_SIZE,\n pullSyncId,\n pushSyncId,\n nodeId: clientId,\n storage: crdtStorage,\n migrator,\n remoteFactory,\n });\n}\n\nexport async function getWorkerConfig<Props = never>(): Promise<WorkerConfig<Props>> {\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 syncDbSchema: SyncDbSchema;\n logger?: Logger;\n createRemoteSource?: CreateRemoteSourceFactory;\n workerConfig?: WorkerConfig;\n /** Durable storage for reset state. Defaults to an IndexedDB-backed store. */\n resetStore?: ResetStore;\n /**\n * App-provided storage version, combined with the library's internal storage\n * version. Bump it when deploying a code change that old persisted local DBs\n * cannot survive — on mismatch the elected worker wipes the local DB on startup.\n */\n storageVersion?: string;\n /**\n * Dev-time drift check: replays the full migration history on a throwaway\n * in-memory database and verifies the result matches the schema declared with\n * `defineSyncSchema`. On mismatch the worker throws and refuses to start.\n * Enable in development only, e.g. `verifySchema: import.meta.env.DEV`.\n */\n verifySchema?: boolean;\n /**\n * Runs local worker event-log garbage collection on startup. Disabled by default;\n * set `eventLogGc: true` to enable. GC keeps the latest event rows and never removes\n * pending events or local rows that have not been pushed to the remote.\n */\n eventLogGc?: boolean;\n};\n\nexport async function startDbWorker(opts: WorkerOptions) {\n const config = opts.workerConfig ?? (await getWorkerConfig());\n\n await navigator.locks.request(`${syncDbWorkerLockName}-${config.dbId}`, { mode: \"exclusive\" }, async (lock) => {\n if (!lock) {\n return;\n }\n\n const cleanup = await createDbWorker(config, opts);\n\n const clientLockName = `${syncDbClientLockName}-${config.dbId}`;\n await new Promise<void>((resolve) => {\n const interval = setInterval(async () => {\n const { held } = await navigator.locks.query();\n const hasClients = held?.some((l) => l.name === clientLockName && l.mode === \"shared\");\n if (!hasClients) {\n clearInterval(interval);\n resolve();\n }\n }, 5_000);\n });\n\n await cleanup();\n });\n\n self.close();\n}\n\nfunction getMaxSyncId(db: SQLiteDbWrapper<WorkerDbSchema>, excludingStatus: \"none\" | \"pending\") {\n const [result] = db.executePrepared(\n \"get-max-sync-id\",\n { excludingStatus: excludingStatus as CrdtEventStatus },\n (db, params) =>\n db\n .selectFrom(\"worker.crdt_events\")\n .where(\"status\", \"!=\", params(\"excludingStatus\"))\n .select((eb) => eb.fn.max(\"sync_id\").as(\"sync_id\")),\n { loggerLevel: \"system\" },\n );\n\n return result?.sync_id ?? 0;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqlite-sync/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "SQLite synchronization library with CRDT support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^22.10.2",
|
|
54
|
+
"fast-check": "^4.10.2",
|
|
54
55
|
"kysely": "^0.28.10",
|
|
55
56
|
"tsdown": "0.22.14",
|
|
56
57
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"make-crdt-table-CvpTHmRY.js","names":[],"sources":["../src/sqlite-crdt/make-crdt-table.ts"],"sourcesContent":["import type { SQLiteReactiveDb } from \"../memory-db/sqlite-reactive-db\";\nimport type { InternalSQLiteTransactionWrapper, SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\nimport { quoteId } from \"../utils\";\nimport type { CrdtChangeIntent, InternalCrdtStorage } from \"./crdt-storage\";\n\nexport const CRDT_CHANGE_INTENTS_TABLE = \"crdt_change_intents\";\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 const columns = new Map(tableSchema.columns.map((column) => [column.name, column]));\n\n const idColumn = columns.get(\"id\");\n if (!idColumn) {\n throw new Error(\n `Table \"${baseTableName}\" is missing a required \"id\" column. CRDT tables must have an \"id\" column to identify items.`,\n );\n }\n if (idColumn.dataType.toUpperCase() !== \"TEXT\") {\n throw new Error(\n `Table \"${baseTableName}\": \"id\" column must be of type TEXT, got \"${idColumn.dataType}\". CRDT item IDs are stored as strings.`,\n );\n }\n\n const tombstoneColumn = columns.get(\"tombstone\");\n if (!tombstoneColumn) {\n throw new Error(\n `Table \"${baseTableName}\" is missing a required \"tombstone\" column. CRDT tables must have a \"tombstone\" INTEGER column for soft deletes.`,\n );\n }\n const tombstoneType = tombstoneColumn.dataType.toUpperCase();\n if (tombstoneType !== \"INTEGER\" && tombstoneType !== \"BOOLEAN\") {\n throw new Error(\n `Table \"${baseTableName}\": \"tombstone\" column must be of type INTEGER or BOOLEAN, got \"${tombstoneColumn.dataType}\". It is compared as 0/1 for soft deletes.`,\n );\n }\n\n for (const sql of createCrdtViewStatements({\n baseTableName,\n crdtTableName,\n columnNames: tableSchema.columns.map((column) => column.name),\n })) {\n db.execute(sql, { loggerLevel: \"system\" });\n }\n}\n\n// Workerd caps expr depth at 100; a left-deep `||` chain fails around 34 columns.\nfunction concatSql(parts: readonly string[]): string {\n if (parts.length === 0) {\n return \"''\";\n }\n if (parts.length === 1) {\n return parts[0];\n }\n const mid = parts.length >> 1;\n return `(${concatSql(parts.slice(0, mid))})||(${concatSql(parts.slice(mid))})`;\n}\n\nfunction sqlStringLiteral(value: string) {\n return `'${value.replaceAll(\"'\", \"''\")}'`;\n}\n\nfunction fullRowPayloadSql(columnNames: string[]) {\n return concatSql([\n \"'{'\",\n ...columnNames.flatMap((columnName, index) => [\n ...(index > 0 ? [\"','\"] : []),\n sqlStringLiteral(`${JSON.stringify(columnName)}:`),\n `json_quote(new.${quoteId(columnName)})`,\n ]),\n \"'}'\",\n ]);\n}\n\nfunction sparseUpdatePayloadSql(columnNames: string[]) {\n const changedFields = columnNames\n .filter((columnName) => columnName !== \"id\")\n .map((columnName) => {\n const quoted = quoteId(columnName);\n const key = sqlStringLiteral(`${JSON.stringify(columnName)}:`);\n return `case when old.${quoted} collate binary is not new.${quoted} then ${key}||json_quote(new.${quoted})||',' else '' end`;\n });\n return concatSql([\"'{'\", `rtrim(${concatSql(changedFields)}, ',')`, \"'}'\"]);\n}\n\nexport function createCrdtViewStatements({\n baseTableName,\n crdtTableName,\n columnNames,\n}: {\n baseTableName: string;\n crdtTableName: string;\n columnNames: string[];\n}) {\n const fullPayload = fullRowPayloadSql(columnNames);\n const updatePayload = sparseUpdatePayloadSql(columnNames);\n\n return [\n `create table if not exists ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (\n \"seq\" integer primary key,\n \"dataset\" text not null,\n \"type\" text not null,\n \"item_id\" text not null,\n \"new_item_id\" text,\n \"payload_json\" text not null\n)`,\n `create view ${quoteId(crdtTableName)} as\nselect * from ${quoteId(baseTableName)}\nwhere tombstone = 0`,\n `create trigger ${quoteId(`${crdtTableName}_created`)}\ninstead of insert on ${quoteId(crdtTableName)}\nfor each row\nbegin\n insert into ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (\n \"dataset\", \"type\", \"item_id\", \"new_item_id\", \"payload_json\"\n ) values (\n ${sqlStringLiteral(baseTableName)}, 'item-created', new.\"id\", null, ${fullPayload}\n );\nend`,\n `create trigger ${quoteId(`${crdtTableName}_updated`)}\ninstead of update on ${quoteId(crdtTableName)}\nfor each row\nbegin\n insert into ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (\n \"dataset\", \"type\", \"item_id\", \"new_item_id\", \"payload_json\"\n ) values (\n ${sqlStringLiteral(baseTableName)}, 'item-updated', old.\"id\", new.\"id\", ${updatePayload}\n );\nend`,\n `create trigger ${quoteId(`${crdtTableName}_deleted`)}\ninstead of delete on ${quoteId(crdtTableName)}\nfor each row\nwhen old.\"tombstone\" = 0\nbegin\n insert into ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (\n \"dataset\", \"type\", \"item_id\", \"new_item_id\", \"payload_json\"\n ) values (\n ${sqlStringLiteral(baseTableName)}, 'item-deleted', old.\"id\", null, '{}'\n );\nend`,\n ];\n}\n\nexport function registerCrdtIntentDrainer({\n reactiveDb,\n storage,\n}: {\n reactiveDb: SQLiteReactiveDb<any>;\n storage: InternalCrdtStorage;\n}) {\n let eventApplied = false;\n let processedIntentMutationVersion = reactiveDb.getTableMutationVersion(CRDT_CHANGE_INTENTS_TABLE);\n\n reactiveDb.db.setAfterMutatingStatement((tx) => {\n if (reactiveDb.getTableMutationVersion(CRDT_CHANGE_INTENTS_TABLE) === processedIntentMutationVersion) {\n return;\n }\n\n try {\n if (drainCrdtChangeIntents({ tx, storage })) {\n eventApplied = true;\n }\n } finally {\n processedIntentMutationVersion = reactiveDb.getTableMutationVersion(CRDT_CHANGE_INTENTS_TABLE);\n }\n });\n\n reactiveDb.addEventListener(\"transaction-committed\", () => {\n if (eventApplied) {\n eventApplied = false;\n void storage.internal.processEnqueuedEvents();\n }\n });\n\n reactiveDb.addEventListener(\"transaction-rolled-back\", () => {\n eventApplied = false;\n });\n}\n\nexport function drainCrdtChangeIntents({\n tx,\n storage,\n}: {\n tx: InternalSQLiteTransactionWrapper<any>;\n storage: InternalCrdtStorage;\n}) {\n const intents = tx.executePreparedRaw<[], CrdtChangeIntent>({\n key: \"drain-crdt-change-intents\",\n sql: `delete from ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} returning *`,\n meta: { loggerLevel: \"system\" },\n });\n intents.sort((a, b) => a.seq - b.seq);\n\n const { appliedEvents } = storage.internal.applyOwnIntentsFromTransaction(tx, intents);\n return appliedEvents > 0;\n}\n"],"mappings":"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"}
|