@sqlite-sync/core 0.12.3 → 0.13.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.
Files changed (44) hide show
  1. package/dist/{reset-state-t0tTGqoh.js → crdt-sync-producer-BY-WobgO.js} +55 -241
  2. package/dist/crdt-sync-producer-BY-WobgO.js.map +1 -0
  3. package/dist/{crdt-sync-remote-source-CPXzmSIS.js → crdt-sync-remote-source-yeTEkOjH.js} +344 -12
  4. package/dist/crdt-sync-remote-source-yeTEkOjH.js.map +1 -0
  5. package/dist/{crdt-table-schema-BRm_WdcW.js → crdt-table-schema-D6Yf61Ex.js} +37 -13
  6. package/dist/crdt-table-schema-D6Yf61Ex.js.map +1 -0
  7. package/dist/{index-BHsKLqcK.d.ts → index-CH4lf3iX.d.ts} +166 -72
  8. package/dist/index.common-C8W9VKSY.js +276 -0
  9. package/dist/index.common-C8W9VKSY.js.map +1 -0
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +20 -677
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.native.d.ts +2 -0
  14. package/dist/index.native.js +9 -0
  15. package/dist/internal.d.ts +1 -1
  16. package/dist/internal.js +2 -2
  17. package/dist/local.d.ts +48 -0
  18. package/dist/local.js +255 -0
  19. package/dist/local.js.map +1 -0
  20. package/dist/{make-crdt-table-C6l0F-o_.js → make-crdt-table-DYCXOYB7.js} +13 -14
  21. package/dist/make-crdt-table-DYCXOYB7.js.map +1 -0
  22. package/dist/reset-state-CPOi_45B.js +220 -0
  23. package/dist/reset-state-CPOi_45B.js.map +1 -0
  24. package/dist/server.d.ts +8 -3
  25. package/dist/server.js +2 -2
  26. package/dist/server.js.map +1 -1
  27. package/dist/sqlite-reactive-db-DLS1xRZp.js +296 -0
  28. package/dist/sqlite-reactive-db-DLS1xRZp.js.map +1 -0
  29. package/dist/synced-db-database-B5M7MrBD.js +81 -0
  30. package/dist/synced-db-database-B5M7MrBD.js.map +1 -0
  31. package/dist/{system-schema-Dvl_BLId.js → system-schema-CXf3FmxN.js} +142 -58
  32. package/dist/system-schema-CXf3FmxN.js.map +1 -0
  33. package/dist/worker.d.ts +2 -10
  34. package/dist/worker.js +11 -177
  35. package/dist/worker.js.map +1 -1
  36. package/dist/ws-remote-source-BbPWrHB6.js +161 -0
  37. package/dist/ws-remote-source-BbPWrHB6.js.map +1 -0
  38. package/dist/ws-remote-source-DFBhDc6Z.d.ts +12 -0
  39. package/package.json +9 -1
  40. package/dist/crdt-sync-remote-source-CPXzmSIS.js.map +0 -1
  41. package/dist/crdt-table-schema-BRm_WdcW.js.map +0 -1
  42. package/dist/make-crdt-table-C6l0F-o_.js.map +0 -1
  43. package/dist/reset-state-t0tTGqoh.js.map +0 -1
  44. package/dist/system-schema-Dvl_BLId.js.map +0 -1
package/dist/worker.js CHANGED
@@ -1,174 +1,10 @@
1
- import { d as SQLiteDbWrapper, n as applyWorkerDbSchema, s as workerDbConfig } from "./system-schema-Dvl_BLId.js";
2
- import { l as generateId, o as createDeferredPromise, u as jsonSafeParse } from "./crdt-table-schema-BRm_WdcW.js";
3
- import { C as xxhash, S as createMigrator, T as createImportData, b as verifySyncSchema, d as isWorkerRequestMessage, g as createCrdtStorage, h as createCrdtSyncProducer, i as createResetStateStore, l as isWorkerInitMessage, m as syncDbWorkerLockName, n as createIdbResetStore, o as runWithNoActiveReplicas, p as syncDbClientLockName, r as createReloadRequestHandler, s as createBroadcastChannels, y as formatSchemaVerificationIssues } from "./reset-state-t0tTGqoh.js";
4
- import { a as HLCCounter, t as createCrdtSyncRemoteSource } from "./crdt-sync-remote-source-CPXzmSIS.js";
5
- import { t as HEARTBEAT_PING } from "./heartbeat-CJWQAoGP.js";
1
+ import { d as createKvStateHashStore, h as SQLiteDbWrapper, i as applyWorkerDbSchema, u as workerDbConfig } from "./system-schema-CXf3FmxN.js";
2
+ import { l as generateId, o as createDeferredPromise } from "./crdt-table-schema-D6Yf61Ex.js";
3
+ import { a as formatSchemaVerificationIssues, c as createMigrator, n as createCrdtStorage, t as createCrdtSyncProducer, u as createImportData } from "./crdt-sync-producer-BY-WobgO.js";
4
+ import { o as HLCCounter, t as createCrdtSyncRemoteSource, u as xxhash } from "./crdt-sync-remote-source-yeTEkOjH.js";
5
+ import { _ as createWasmSqliteDriver, d as isWorkerRequestMessage, h as verifySyncSchema, i as createResetStateStore, l as isWorkerInitMessage, m as syncDbWorkerLockName, n as createIdbResetStore, o as runWithNoActiveReplicas, p as syncDbClientLockName, r as createReloadRequestHandler, s as createBroadcastChannels } from "./reset-state-CPOi_45B.js";
6
+ import { i as runWorkerEventLogGc, n as createStorageVersionStore, t as createWsRemoteSource } from "./ws-remote-source-BbPWrHB6.js";
6
7
  import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
7
- //#region src/web-socket/ws-remote-source.ts
8
- const WS_OPEN = 1;
9
- const createWsRemoteSource = ({ createWebSocket }) => {
10
- return ({ onEventsAvailable, onClosed }) => {
11
- const socket = createWebSocket();
12
- const opened = createDeferredPromise();
13
- opened.promise.catch(() => {});
14
- const requestsMap = /* @__PURE__ */ new Map();
15
- let onPong;
16
- let closed = false;
17
- const shutdown = () => {
18
- if (closed) return false;
19
- closed = true;
20
- const error = /* @__PURE__ */ new Error("WebSocket closed");
21
- opened.reject(error);
22
- for (const request of requestsMap.values()) request.reject(error);
23
- requestsMap.clear();
24
- socket.close(1e3);
25
- return true;
26
- };
27
- if (socket.readyState === WS_OPEN) opened.resolve();
28
- socket.addEventListener("open", () => {
29
- opened.resolve();
30
- });
31
- socket.addEventListener("close", () => {
32
- if (shutdown()) onClosed();
33
- });
34
- const send = async (request) => {
35
- await opened.promise;
36
- if (closed) throw new Error("WebSocket closed");
37
- const requestId = crypto.randomUUID();
38
- const promise = createDeferredPromise();
39
- requestsMap.set(requestId, promise);
40
- socket.send(JSON.stringify({
41
- ...request,
42
- requestId
43
- }));
44
- return promise.promise;
45
- };
46
- const pushEvents = (request) => send({
47
- type: "push-events",
48
- nodeId: request.nodeId,
49
- events: request.events
50
- });
51
- const pullEvents = (request) => send({
52
- type: "pull-events",
53
- afterSyncId: request.afterSyncId,
54
- excludeNodeId: request.excludeNodeId
55
- });
56
- const ping = () => {
57
- socket.send(HEARTBEAT_PING);
58
- return new Promise((resolve) => {
59
- onPong = resolve;
60
- });
61
- };
62
- socket.onmessage = (event) => {
63
- if (event.data === "pong") {
64
- onPong?.();
65
- return;
66
- }
67
- const result = jsonSafeParse(event.data);
68
- if (!result.success || !("type" in result.data) || !result.data.type) return;
69
- const message = result.data;
70
- switch (message.type) {
71
- case "events-pull-response":
72
- case "events-push-response": {
73
- const promise = requestsMap.get(message.requestId);
74
- if (!promise) return;
75
- promise.resolve(message.data);
76
- requestsMap.delete(message.requestId);
77
- break;
78
- }
79
- case "events-applied":
80
- onEventsAvailable({
81
- newSyncId: message.newSyncId,
82
- remoteEventHlcSum: message.eventHlcSum
83
- });
84
- break;
85
- default: return;
86
- }
87
- };
88
- return {
89
- pushEvents,
90
- pullEvents,
91
- ping,
92
- disconnect: () => {
93
- shutdown();
94
- }
95
- };
96
- };
97
- };
98
- function runWorkerEventLogGc({ db, dbConfig, pushSyncId, eventHlcAccumulator, logger }) {
99
- const eventsTable = dbConfig.eventsTable.fullIdentifier;
100
- if (eventHlcAccumulator.current === "") {
101
- const [{ count }] = db.executePreparedRaw({
102
- key: "event-log-gc-count-applied-events",
103
- sql: `SELECT count(*) AS count FROM ${eventsTable} WHERE "status" = 'applied'`,
104
- meta: { loggerLevel: "system" }
105
- });
106
- if (count > 0) logger?.("worker", "Skipping event log GC until the event HLC checksum has been computed from full history", "info");
107
- return { skipped: true };
108
- }
109
- const [{ deleteBeforeSyncId }] = db.executePreparedRaw({
110
- key: "event-log-gc-delete-before-sync-id",
111
- sql: `
112
- SELECT min("sync_id") AS deleteBeforeSyncId
113
- FROM (
114
- SELECT "sync_id"
115
- FROM ${eventsTable}
116
- WHERE "status" IN ('applied', 'deduped')
117
- ORDER BY "sync_id" DESC
118
- LIMIT ?
119
- )
120
- `,
121
- params: [100],
122
- meta: { loggerLevel: "system" }
123
- });
124
- if (deleteBeforeSyncId === null) return { skipped: false };
125
- db.executePreparedRaw({
126
- key: "event-log-gc-delete-events",
127
- sql: `
128
- DELETE FROM ${eventsTable}
129
- WHERE "sync_id" < ?
130
- AND "status" IN ('applied', 'deduped')
131
- AND ("origin" = 'remote' OR "sync_id" <= ?)
132
- `,
133
- params: [deleteBeforeSyncId, pushSyncId.current],
134
- meta: { loggerLevel: "system" }
135
- });
136
- logger?.("worker", "Event log GC completed", "info");
137
- return { skipped: false };
138
- }
139
- const storageVersionKey = (dbId) => `sqlite-sync-storage-version-${dbId}`;
140
- function formatStorageVersion(appStorageVersion) {
141
- return appStorageVersion === void 0 ? `lib-v2` : `lib-v2:app-${appStorageVersion}`;
142
- }
143
- /**
144
- * Worker-owned durable storage version. The current version combines the
145
- * internal lib version with the dev-provided app version; when the stored
146
- * version does not match (including a missing record), the elected worker
147
- * initializes with `clearOnInit: true`. Wiping on a missing record is
148
- * harmless for fresh installs and correctly resets databases persisted
149
- * before versioning existed.
150
- */
151
- function createStorageVersionStore({ store, dbId, appStorageVersion }) {
152
- const key = storageVersionKey(dbId);
153
- const currentVersion = formatStorageVersion(appStorageVersion);
154
- return {
155
- currentVersion,
156
- async getVersionStatus() {
157
- const storedVersion = await store.get(key);
158
- if (storedVersion === void 0) return "missing";
159
- return storedVersion === currentVersion ? "current" : "outdated";
160
- },
161
- /**
162
- * Record the current version. Must be called only after the worker has
163
- * successfully initialized with the wiped DB, so a failed init can be
164
- * retried by a later elected worker.
165
- */
166
- async markCurrentVersionApplied() {
167
- await store.set(key, currentVersion);
168
- }
169
- };
170
- }
171
- //#endregion
172
8
  //#region src/worker-db/db-worker.ts
173
9
  const defaultLogger = (type, message, level = "info") => {
174
10
  const logMessage = `[${type}] ${message}`;
@@ -215,10 +51,9 @@ async function createDbWorker(config, opts) {
215
51
  initialCapacity: 8
216
52
  });
217
53
  const db = new SQLiteDbWrapper({
218
- db: () => new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`),
54
+ driver: createWasmSqliteDriver(sqlite3, new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`)),
219
55
  logger,
220
- loggerPrefix: "worker",
221
- sqlite3
56
+ loggerPrefix: "worker"
222
57
  });
223
58
  db.execute("PRAGMA locking_mode=exclusive", { loggerLevel: "system" });
224
59
  db.execute("PRAGMA journal_mode=WAL", { loggerLevel: "system" });
@@ -242,7 +77,7 @@ async function createDbWorker(config, opts) {
242
77
  db.invalidateDbSchema();
243
78
  const pullSyncId = kvStore.createNumberStoredValue("pull-sync-id", -1);
244
79
  const pushSyncId = kvStore.createNumberStoredValue("push-sync-id", -1);
245
- const eventHlcAccumulator = kvStore.createStringStoredValue("crdt.consistency.event_hlc_sum.v2", "");
80
+ const stateHash = createKvStateHashStore(kvStore);
246
81
  const storageGeneration = kvStore.createStringStoredValue("storage-generation", "");
247
82
  if (!storageGeneration.current) storageGeneration.current = generateId();
248
83
  if (pendingReset) await resetState.markResetApplied(pendingReset.epoch);
@@ -254,7 +89,6 @@ async function createDbWorker(config, opts) {
254
89
  db,
255
90
  dbConfig: workerDbConfig,
256
91
  pushSyncId,
257
- eventHlcAccumulator,
258
92
  logger
259
93
  });
260
94
  }
@@ -266,7 +100,7 @@ async function createDbWorker(config, opts) {
266
100
  hlc: new HLCCounter(config.clientId, () => Date.now()),
267
101
  db,
268
102
  dbConfig: workerDbConfig,
269
- eventHlcAccumulator,
103
+ stateHash,
270
104
  schema: opts.syncDbSchema
271
105
  });
272
106
  createCrdtSyncProducer({
@@ -275,7 +109,7 @@ async function createDbWorker(config, opts) {
275
109
  broadcastChannels.responses.postMessage({
276
110
  notificationType: "new-event-chunk-applied",
277
111
  newSyncId: chunk.newSyncId,
278
- eventHlcSum: chunk.eventHlcSum
112
+ stateHash: chunk.stateHash
279
113
  });
280
114
  }
281
115
  });
@@ -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 { HEARTBEAT_PING, HEARTBEAT_PONG } from \"../server/heartbeat\";\nimport type { CreateRemoteSourceFactory } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport { createDeferredPromise, type DeferredPromise, type DistributiveOmit, jsonSafeParse } from \"../utils\";\nimport type { EventsPullRequest, EventsPushRequest, EventsPushResponse, GetEventsBatch } from \"../worker\";\n\nconst WS_OPEN = 1;\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 ({ onEventsAvailable, onClosed }) => {\n const socket = createWebSocket();\n const opened = createDeferredPromise<void>();\n opened.promise.catch(() => {});\n const requestsMap = new Map<string, DeferredPromise<unknown>>();\n let onPong: (() => void) | undefined;\n let closed = false;\n\n const shutdown = () => {\n if (closed) {\n return false;\n }\n closed = true;\n const error = new Error(\"WebSocket closed\");\n opened.reject(error);\n for (const request of requestsMap.values()) {\n request.reject(error);\n }\n requestsMap.clear();\n // Servers may not answer a close frame without a code, leaving the socket stuck in CLOSING.\n socket.close(1000);\n return true;\n };\n\n if (socket.readyState === WS_OPEN) {\n opened.resolve();\n }\n socket.addEventListener(\"open\", () => {\n opened.resolve();\n });\n // Closing on the first drop stops wrappers like PartySocket from reconnecting behind the library's back.\n socket.addEventListener(\"close\", () => {\n if (shutdown()) {\n onClosed();\n }\n });\n\n const send = async <T>(request: DistributiveOmit<SyncServerRequest, \"requestId\">): Promise<T> => {\n await opened.promise;\n if (closed) {\n throw new Error(\"WebSocket closed\");\n }\n const requestId = crypto.randomUUID();\n const promise = createDeferredPromise<T>();\n requestsMap.set(requestId, promise as DeferredPromise<unknown>);\n socket.send(JSON.stringify({ ...request, requestId }));\n return promise.promise;\n };\n\n const pushEvents = (request: EventsPushRequest) =>\n send<EventsPushResponse>({ type: \"push-events\", nodeId: request.nodeId, events: request.events });\n\n const pullEvents = (request: EventsPullRequest) =>\n send<GetEventsBatch>({\n type: \"pull-events\",\n afterSyncId: request.afterSyncId,\n excludeNodeId: request.excludeNodeId,\n });\n\n const ping = () => {\n socket.send(HEARTBEAT_PING);\n return new Promise<void>((resolve) => {\n onPong = resolve;\n });\n };\n\n socket.onmessage = (event) => {\n if (event.data === HEARTBEAT_PONG) {\n onPong?.();\n return;\n }\n\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 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 ping,\n disconnect: () => {\n shutdown();\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 getVersionStatus(): Promise<\"current\" | \"missing\" | \"outdated\"> {\n const storedVersion = await store.get<string>(key);\n if (storedVersion === undefined) {\n return \"missing\";\n }\n return storedVersion === currentVersion ? \"current\" : \"outdated\";\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, generateId } from \"../utils\";\nimport { runWorkerEventLogGc } from \"./event-log-gc\";\nimport { runWithNoActiveReplicas } from \"./replica-lock\";\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, versionStatus] = await Promise.all([\n resetState.resolvePendingReset(),\n storageVersion.getVersionStatus(),\n ]);\n const isVersionMismatch = versionStatus !== \"current\";\n\n if (versionStatus === \"missing\") {\n logger(\"worker\", `Initializing local DB storage version ${storageVersion.currentVersion}`, \"info\");\n } else if (versionStatus === \"outdated\") {\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 const storageGeneration = kvStore.createStringStoredValue(\"storage-generation\", \"\");\n if (!storageGeneration.current) {\n storageGeneration.current = generateId();\n }\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 await runWithNoActiveReplicas({\n dbId: config.dbId,\n run: () => {\n runWorkerEventLogGc({\n db,\n dbConfig: workerDbConfig,\n pushSyncId,\n eventHlcAccumulator,\n logger,\n });\n },\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 storageGeneration: storageGeneration.current,\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 storageGeneration: storageGeneration.current,\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 dropConnection: () => remoteSource.dropConnection(\"NETWORK_OFFLINE\"),\n sync: () => {\n remoteSource.syncWithRemote().catch((error) => console.warn(\"Sync failed\", error));\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 * Skipped while an existing tab holds an in-memory replica of this DB.\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":";;;;;;;AAMA,MAAM,UAAU;AAShB,MAAa,wBAAwB,EAAE,sBAAuE;CAC5G,QAAQ,EAAE,mBAAmB,eAAe;EAC1C,MAAM,SAAS,gBAAgB;EAC/B,MAAM,SAAS,sBAA4B;EAC3C,OAAO,QAAQ,YAAY,CAAC,CAAC;EAC7B,MAAM,8BAAc,IAAI,IAAsC;EAC9D,IAAI;EACJ,IAAI,SAAS;EAEb,MAAM,iBAAiB;GACrB,IAAI,QACF,OAAO;GAET,SAAS;GACT,MAAM,wBAAQ,IAAI,MAAM,kBAAkB;GAC1C,OAAO,OAAO,KAAK;GACnB,KAAK,MAAM,WAAW,YAAY,OAAO,GACvC,QAAQ,OAAO,KAAK;GAEtB,YAAY,MAAM;GAElB,OAAO,MAAM,GAAI;GACjB,OAAO;EACT;EAEA,IAAI,OAAO,eAAe,SACxB,OAAO,QAAQ;EAEjB,OAAO,iBAAiB,cAAc;GACpC,OAAO,QAAQ;EACjB,CAAC;EAED,OAAO,iBAAiB,eAAe;GACrC,IAAI,SAAS,GACX,SAAS;EAEb,CAAC;EAED,MAAM,OAAO,OAAU,YAA0E;GAC/F,MAAM,OAAO;GACb,IAAI,QACF,MAAM,IAAI,MAAM,kBAAkB;GAEpC,MAAM,YAAY,OAAO,WAAW;GACpC,MAAM,UAAU,sBAAyB;GACzC,YAAY,IAAI,WAAW,OAAmC;GAC9D,OAAO,KAAK,KAAK,UAAU;IAAE,GAAG;IAAS;GAAU,CAAC,CAAC;GACrD,OAAO,QAAQ;EACjB;EAEA,MAAM,cAAc,YAClB,KAAyB;GAAE,MAAM;GAAe,QAAQ,QAAQ;GAAQ,QAAQ,QAAQ;EAAO,CAAC;EAElG,MAAM,cAAc,YAClB,KAAqB;GACnB,MAAM;GACN,aAAa,QAAQ;GACrB,eAAe,QAAQ;EACzB,CAAC;EAEH,MAAM,aAAa;GACjB,OAAO,KAAK,cAAc;GAC1B,OAAO,IAAI,SAAe,YAAY;IACpC,SAAS;GACX,CAAC;EACH;EAEA,OAAO,aAAa,UAAU;GAC5B,IAAI,MAAM,SAAA,QAAyB;IACjC,SAAS;IACT;GACF;GAEA,MAAM,SAAS,cAAiC,MAAM,IAAI;GAE1D,IAAI,CAAC,OAAO,WAAW,EAAE,UAAU,OAAO,SAAS,CAAC,OAAO,KAAK,MAC9D;GAGF,MAAM,UAAU,OAAO;GAEvB,QAAQ,QAAQ,MAAhB;IACE,KAAK;IACL,KAAK,wBAAwB;KAC3B,MAAM,UAAU,YAAY,IAAI,QAAQ,SAAS;KACjD,IAAI,CAAC,SACH;KAEF,QAAQ,QAAQ,QAAQ,IAAI;KAC5B,YAAY,OAAO,QAAQ,SAAS;KACpC;IACF;IACA,KAAK;KACH,kBAAkB;MAAE,WAAW,QAAQ;MAAW,mBAAmB,QAAQ;KAAY,CAAC;KAC1F;IACF,SAEE;GACJ;EACF;EAEA,OAAO;GACL;GACA;GACA;GACA,kBAAkB;IAChB,SAAS;GACX;EACF;CACF;AACF;AC9GA,SAAgB,oBAAoB,EAAE,IAAI,UAAU,YAAY,qBAAqB,UAA6B;CAChH,MAAM,cAAc,SAAS,YAAY;CAEzC,IAAI,oBAAoB,YAAY,IAAI;EACtC,MAAM,CAAC,EAAE,WAAW,GAAG,mBAA0C;GAC/D,KAAK;GACL,KAAK,iCAAiC,YAAY;GAClD,MAAM,EAAE,aAAa,SAAS;EAChC,CAAC;EACD,IAAI,QAAQ,GACV,SACE,UACA,0FACA,MACF;EAEF,OAAO,EAAE,SAAS,KAAc;CAClC;CAEA,MAAM,CAAC,EAAE,wBAAwB,GAAG,mBAAoE;EACtG,KAAK;EACL,KAAK;;;;eAIM,YAAY;;;;;;EAMvB,QAAQ,CAAA,GAAwC;EAChD,MAAM,EAAE,aAAa,SAAS;CAChC,CAAC;CAED,IAAI,uBAAuB,MACzB,OAAO,EAAE,SAAS,MAAe;CAGnC,GAAG,mBAA4C;EAC7C,KAAK;EACL,KAAK;oBACW,YAAY;;;;;EAK5B,QAAQ,CAAC,oBAAoB,WAAW,OAAO;EAC/C,MAAM,EAAE,aAAa,SAAS;CAChC,CAAC;CAED,SAAS,UAAU,0BAA0B,MAAM;CACnD,OAAO,EAAE,SAAS,MAAe;AACnC;AC3DA,MAAM,qBAAqB,SAAiB,+BAA+B;AAE3E,SAAgB,qBAAqB,mBAA+C;CAClF,OAAO,sBAAsB,KAAA,IACzB,WACA,cAAmC;AACzC;;;;;;;;;AAmBA,SAAgB,0BAA0B,EAAE,OAAO,MAAM,qBAAiD;CACxG,MAAM,MAAM,kBAAkB,IAAI;CAClC,MAAM,iBAAiB,qBAAqB,iBAAiB;CAE7D,OAAO;EACL;EACA,MAAM,mBAAgE;GACpE,MAAM,gBAAgB,MAAM,MAAM,IAAY,GAAG;GACjD,IAAI,kBAAkB,KAAA,GACpB,OAAO;GAET,OAAO,kBAAkB,iBAAiB,YAAY;EACxD;;;;;;EAMA,MAAM,4BAA2C;GAC/C,MAAM,MAAM,IAAI,KAAK,cAAc;EACrC;CACF;AACF;;;ACnBA,MAAM,iBAAyB,MAAM,SAAS,QAAQ,WAAW;CAC/D,MAAM,aAAa,IAAI,KAAK,IAAI;CAChC,QAAQ,OAAR;EACE,KAAK;GACH,QAAQ,IAAI,UAAU;GACtB;EACF,KAAK;GACH,QAAQ,KAAK,UAAU;GACvB;EACF,KAAK;GACH,QAAQ,MAAM,UAAU;GACxB;EACF,KAAK,SACH,QAAQ,MAAM,UAAU;CAE5B;AACF;AAEA,eAAe,eAAe,QAAsB,MAAqB;CACvE,MAAM,oBAAoB,wBAAwB,OAAO,IAAI;CAC7D,MAAM,SAAS,KAAK,UAAU;CAE9B,IAAI,KAAK,cAAc;EACrB,MAAM,SAAS,MAAM,iBAAiB,KAAK,YAAY;EACvD,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,MAAM,+BAA+B,MAAM,CAAC;EAExD,OAAO,UAAU,oEAAoE,MAAM;CAC7F;CAEA,MAAM,CAAC,WAAW,MAAM,QAAQ,IAAI,CAAC,kBAAkB,GAAG,OAAO,aAAa,CAAC,CAAC;CAEhF,MAAM,aAAa,KAAK,cAAc,oBAAoB;CAC1D,MAAM,aAAa,sBAAsB;EACvC,OAAO;EACP,MAAM,OAAO;CACf,CAAC;CACD,MAAM,iBAAiB,0BAA0B;EAC/C,OAAO;EACP,MAAM,OAAO;EACb,mBAAmB,KAAK;CAC1B,CAAC;CACD,MAAM,CAAC,cAAc,iBAAiB,MAAM,QAAQ,IAAI,CACtD,WAAW,oBAAoB,GAC/B,eAAe,iBAAiB,CAClC,CAAC;CACD,MAAM,oBAAoB,kBAAkB;CAE5C,IAAI,kBAAkB,WACpB,OAAO,UAAU,yCAAyC,eAAe,kBAAkB,MAAM;MAC5F,IAAI,kBAAkB,YAC3B,OAAO,UAAU,oDAAoD,eAAe,kBAAkB,SAAS;CAGjH,MAAM,OAAO,MAAM,QAAQ,sBAAsB;EAC/C,MAAM,OAAO;EACb,WAAW,IAAI,OAAO;EACtB,aAAa,CAAC,CAAC,gBAAgB;EAC/B,iBAAiB;CACnB,CAAC;CAED,MAAM,KAAK,IAAI,gBAAgC;EAC7C,UAAU,IAAI,KAAK,cAAc,IAAI,OAAO,KAAK,SAAS;EAClD;EACR,cAAc;EACd;CACF,CAAC;CAED,GAAG,QAAQ,iCAAiC,EAAE,aAAa,SAAS,CAAC;CACrE,GAAG,QAAQ,2BAA2B,EAAE,aAAa,SAAS,CAAC;CAC/D,GAAG,QAAQ,6BAA6B,EAAE,aAAa,SAAS,CAAC;CAEjE,GAAG,QAAQ,qBAAqB,OAAO,KAAK,wBAAwB,EAAE,aAAa,SAAS,CAAC;CAC7F,GAAG,QAAQ,wCAAwC,EAAE,aAAa,SAAS,CAAC;CAC5E,GAAG,QAAQ,kCAAkC,EAAE,aAAa,SAAS,CAAC;CACtE,GAAG,QAAQ,oCAAoC,EAAE,aAAa,SAAS,CAAC;CAExE,MAAM,EAAE,YAAY,oBAAoB,EAAE;CAE1C,MAAM,WAAW,eAAe;EAC9B,YAAY,KAAK,aAAa;EAC9B,eAAe,QAAQ,wBAAwB,kBAAkB,EAAE;EACnE,oBAAoB,eAAe,eAAe;CACpD,CAAC;CACD,SAAS,kBAAkB,EACzB,mBAAmB,aAAa;EAC9B,GAAG,oBAAoB,OACrB,SAAS,EAAE,UAAU,KAAK,YAAY,SAAS,GAAG,QAAQ;GAAE;GAAK;EAAW,GAAG,IAAI,EAAE,CAAC,CACxF;CACF,EACF,CAAC;CACD,GAAG,mBAAmB;CAEtB,MAAM,aAAa,QAAQ,wBAAwB,gBAAgB,EAAE;CACrE,MAAM,aAAa,QAAQ,wBAAwB,gBAAgB,EAAE;CACrE,MAAM,sBAAsB,QAAQ,wBAAwB,qCAAqC,EAAE;CACnG,MAAM,oBAAoB,QAAQ,wBAAwB,sBAAsB,EAAE;CAClF,IAAI,CAAC,kBAAkB,SACrB,kBAAkB,UAAU,WAAW;CAMzC,IAAI,cACF,MAAM,WAAW,iBAAiB,aAAa,KAAK;CAEtD,IAAI,mBACF,MAAM,eAAe,0BAA0B;CAGjD,IAAI,KAAK,eAAe,MACtB,MAAM,wBAAwB;EAC5B,MAAM,OAAO;EACb,WAAW;GACT,oBAAoB;IAClB;IACA,UAAU;IACV;IACA;IACA;GACF,CAAC;EACH;CACF,CAAC;CAGH,MAAM,cAAc,kBAAkB;EACpC,QAAQ,OAAO;EACf,oBAAoB,aAAa,IAAI,MAAM;EAC3C;EACA,KAAK,IAAI,WAAW,OAAO,gBAAgB,KAAK,IAAI,CAAC;EACrD;EACA,UAAU;EACV;EACA,QAAQ,KAAK;CACf,CAAC;CAED,uBAAuB;EACrB,SAAS;EACT,kBAAkB,UAAU;GAC1B,kBAAkB,UAAU,YAAY;IACtC,kBAAkB;IAClB,WAAW,MAAM;IACjB,aAAa,MAAM;GACrB,CAAC;EACH;CACF,CAAC;CAED,MAAM,eAAe,mBAAmB;EACtC;EACA;EACA,UAAU,OAAO;EACjB;EACA;EACA,eAAe,KAAK;CACtB,CAAC;CACD,aAAa,SAAS;CAEtB,MAAM,yBAAyB,iBAA4C;EACzE,kBAAkB,UAAU,YAAY,YAAY;CACtD;CAEA,MAAM,kBAAkB;EACtB,sBAAsB;GACpB,kBAAkB;GAClB,OAAO,aAAa,SAAS;GAC7B,mBAAmB,kBAAkB;EACvC,CAAC;CACH;CACA,MAAM,2BAA2B,aAAa,iBAAiB,uBAAuB;EACpF,UAAU;CACZ,CAAC;CACD,MAAM,6BAA6B,aAAa,iBAAiB,qBAAqB,UAAU;EAC9F,sBAAsB;GACpB,kBAAkB;GAClB,QAAQ,MAAM,QAAQ;EACxB,CAAC;CACH,CAAC;CACD,MAAM,0CAA0C,aAAa,iBAC3D,mCACC,UAAU;EACT,sBAAsB;GACpB,kBAAkB;GAClB,qBAAqB,MAAM,QAAQ;GACnC,oBAAoB,MAAM,QAAQ;EACpC,CAAC;CACH,CACF;CAEA,MAAM,YAAuB;EAC3B,UAAU,UAAU,GAAG,QAAQ,KAAK;EACpC,mBAAmB;GACjB,MAAM,gBAAgB,aAAa,IAAI,SAAS;GAChD,GAAG,QAAQ,2BAA2B,EAAE,aAAa,SAAS,CAAC;GAC/D,MAAM,OAAO,GAAG,eAAe;GAC/B,GAAG,QAAQ,2BAA2B,EAAE,aAAa,SAAS,CAAC;GAC/D,OAAO;IACL;IACA,QAAQ;IACR,eAAe,SAAS;IACxB,mBAAmB,kBAAkB;GACvC;EACF;EACA,YAAY,iBAAiB;GAC3B;GACA,cAAc,KAAK,aAAa;GAChC,aAAa,OAAO,WAAW;IAC7B,YAAY,eAAe,MAAM;IACjC,MAAM,YAAY,SAAS,sBAAsB;GACnD;EACF,CAAC;EACD;EACA,gBAAgB,YAAY;GAC1B,MAAM,EAAE,cAAc,gBAAgB,YAAY,mBAAmB,QAAQ,QAAQ,QAAQ,MAAM;GACnG,OAAO;IACL,IAAI;IACJ;IACA;GACF;EACF;EACA,aAAa,YAAY;GACvB,OAAO,YAAY,eAAe;IAChC,aAAa,QAAQ;IACrB,QAAQ;IACR,eAAe,QAAQ,iBAAiB;IACxC,OAAO;GACT,CAAC;EACH;EACA,gBAAgB,aAAa,SAAS;EACtC,iBAAiB,aAAa,UAAU,cAAc;EACtD,sBAAsB,aAAa,eAAe,iBAAiB;EACnE,YAAY;GACV,aAAa,eAAe,CAAC,CAAC,OAAO,UAAU,QAAQ,KAAK,eAAe,KAAK,CAAC;EACnF;EACA,eAAe,2BAA2B;GACxC;GACA,WAAW;EACb,CAAC;CACH;CAEA,kBAAkB,SAAS,aAAa,UAAU;EAChD,MAAM,UAAU,MAAM;EAEtB,IAAI,CAAC,uBAAuB,OAAO,GACjC;EAGF,MAAM,aAAa,UAAmB;GACpC,MAAM,WAAuC;IAC3C,MAAM;IACN,WAAW,QAAQ;IACnB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D;GACA,kBAAkB,UAAU,YAAY,QAAQ;EAClD;EAEA,IAAI;GAEF,MAAM,OADS,UAAU,QAAQ,OACd,CAAC,MAAM,MAAM,QAAQ,IAAU;GAElD,IAAI,gBAAgB,SAClB,KACG,MAAM,WAAW;IAChB,MAAM,WAAkC;KACtC,MAAM;KACN,WAAW,QAAQ;KACnB,MAAM;IACR;IACA,kBAAkB,UAAU,YAAY,QAAQ;GAClD,CAAC,CAAC,CACD,MAAM,SAAS;QACb;IACL,MAAM,WAAkC;KACtC,MAAM;KACN,WAAW,QAAQ;KACnB;IACF;IACA,kBAAkB,UAAU,YAAY,QAAQ;GAClD;EACF,SAAS,OAAO;GACd,UAAU,KAAK;EACjB;CACF;CAEA,UAAU,UAAU;CAEpB,OAAO,YAAY;EACjB,yBAAyB,YAAY;EACrC,2BAA2B,YAAY;EACvC,wCAAwC,YAAY;EACpD,MAAM,aAAa,QAAQ;EAC3B,kBAAkB,SAAS,MAAM;EACjC,kBAAkB,UAAU,MAAM;EAClC,GAAG,MAAM;CACX;AACF;AAWA,SAAS,mBAAmB,EAC1B,UACA,aACA,UACA,YACA,YACA,iBACoB;CACpB,OAAO,2BAA2B;EAChC,YAAA;EACA;EACA;EACA,QAAQ;EACR,SAAS;EACT;EACA;CACF,CAAC;AACH;AAEA,eAAsB,kBAA+D;CACnF,IAAI,YAAY;CAChB,MAAM,kBAAkB,sBAAoC;CAE5D,KAAK,aAAa,UAAiC;EACjD,IAAI,WAAW;GACb,QAAQ,MAAM,2BAA2B;GACzC;EACF;EAEA,MAAM,UAAU,MAAM;EACtB,IAAI,CAAC,oBAAoB,OAAO,GAC9B;EAGF,gBAAgB,QAAQ,QAAQ,MAAM;EACtC,YAAY;CACd;CAEA,OAAO,gBAAgB;AACzB;AA+BA,eAAsB,cAAc,MAAqB;CACvD,MAAM,SAAS,KAAK,gBAAiB,MAAM,gBAAgB;CAE3D,MAAM,UAAU,MAAM,QAAQ,GAAG,qBAAqB,GAAG,OAAO,QAAQ,EAAE,MAAM,YAAY,GAAG,OAAO,SAAS;EAC7G,IAAI,CAAC,MACH;EAGF,MAAM,UAAU,MAAM,eAAe,QAAQ,IAAI;EAEjD,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,OAAO;EACzD,MAAM,IAAI,SAAe,YAAY;GACnC,MAAM,WAAW,YAAY,YAAY;IACvC,MAAM,EAAE,SAAS,MAAM,UAAU,MAAM,MAAM;IAE7C,IAAI,CADe,MAAM,MAAM,MAAM,EAAE,SAAS,kBAAkB,EAAE,SAAS,QAAQ,GACpE;KACf,cAAc,QAAQ;KACtB,QAAQ;IACV;GACF,GAAG,GAAK;EACV,CAAC;EAED,MAAM,QAAQ;CAChB,CAAC;CAED,KAAK,MAAM;AACb;AAEA,SAAS,aAAa,IAAqC,iBAAqC;CAC9F,MAAM,CAAC,UAAU,GAAG,gBAClB,mBACA,EAAmB,gBAAmC,IACrD,IAAI,WACH,GACG,WAAW,oBAAoB,CAAC,CAChC,MAAM,UAAU,MAAM,OAAO,iBAAiB,CAAC,CAAC,CAChD,QAAQ,OAAO,GAAG,GAAG,IAAI,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC,GACtD,EAAE,aAAa,SAAS,CAC1B;CAEA,OAAO,QAAQ,WAAW;AAC5B"}
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../src/worker-db/db-worker.ts"],"sourcesContent":["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 } from \"../schema/verify-sync-schema\";\nimport { verifySyncSchema } from \"../schema/verify-sync-schema-wasm\";\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 { createKvStateHashStore, type KvStore } from \"../sqlite-kv-store\";\nimport { createDeferredPromise, generateId } from \"../utils\";\nimport { createWasmSqliteDriver } from \"../wasm-driver\";\nimport { runWorkerEventLogGc } from \"./event-log-gc\";\nimport { runWithNoActiveReplicas } from \"./replica-lock\";\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, versionStatus] = await Promise.all([\n resetState.resolvePendingReset(),\n storageVersion.getVersionStatus(),\n ]);\n const isVersionMismatch = versionStatus !== \"current\";\n\n if (versionStatus === \"missing\") {\n logger(\"worker\", `Initializing local DB storage version ${storageVersion.currentVersion}`, \"info\");\n } else if (versionStatus === \"outdated\") {\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 driver: createWasmSqliteDriver(sqlite3, new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`)),\n logger: logger,\n loggerPrefix: \"worker\",\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 stateHash = createKvStateHashStore(kvStore);\n const storageGeneration = kvStore.createStringStoredValue(\"storage-generation\", \"\");\n if (!storageGeneration.current) {\n storageGeneration.current = generateId();\n }\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 await runWithNoActiveReplicas({\n dbId: config.dbId,\n run: () => {\n runWorkerEventLogGc({\n db,\n dbConfig: workerDbConfig,\n pushSyncId,\n logger,\n });\n },\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 stateHash,\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 stateHash: chunk.stateHash,\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 storageGeneration: storageGeneration.current,\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 storageGeneration: storageGeneration.current,\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 dropConnection: () => remoteSource.dropConnection(\"NETWORK_OFFLINE\"),\n sync: () => {\n remoteSource.syncWithRemote().catch((error) => console.warn(\"Sync failed\", error));\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 * Skipped while an existing tab holds an in-memory replica of this DB.\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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"}
@@ -0,0 +1,161 @@
1
+ import { l as generateId, o as createDeferredPromise, u as jsonSafeParse } from "./crdt-table-schema-D6Yf61Ex.js";
2
+ import { t as HEARTBEAT_PING } from "./heartbeat-CJWQAoGP.js";
3
+ function runWorkerEventLogGc({ db, dbConfig, pushSyncId, logger }) {
4
+ const eventsTable = dbConfig.eventsTable.fullIdentifier;
5
+ const [{ deleteBeforeSyncId }] = db.executePreparedRaw({
6
+ key: "event-log-gc-delete-before-sync-id",
7
+ sql: `
8
+ SELECT min("sync_id") AS deleteBeforeSyncId
9
+ FROM (
10
+ SELECT "sync_id"
11
+ FROM ${eventsTable}
12
+ WHERE "status" IN ('applied', 'deduped')
13
+ ORDER BY "sync_id" DESC
14
+ LIMIT ?
15
+ )
16
+ `,
17
+ params: [100],
18
+ meta: { loggerLevel: "system" }
19
+ });
20
+ if (deleteBeforeSyncId === null) return;
21
+ db.executePreparedRaw({
22
+ key: "event-log-gc-delete-events",
23
+ sql: `
24
+ DELETE FROM ${eventsTable}
25
+ WHERE "sync_id" < ?
26
+ AND "status" IN ('applied', 'deduped')
27
+ AND ("origin" = 'remote' OR "sync_id" <= ?)
28
+ `,
29
+ params: [deleteBeforeSyncId, pushSyncId.current],
30
+ meta: { loggerLevel: "system" }
31
+ });
32
+ logger?.("worker", "Event log GC completed", "info");
33
+ }
34
+ const storageVersionKey = (dbId) => `sqlite-sync-storage-version-${dbId}`;
35
+ function formatStorageVersion(appStorageVersion) {
36
+ return appStorageVersion === void 0 ? `lib-v2` : `lib-v2:app-${appStorageVersion}`;
37
+ }
38
+ /**
39
+ * Worker-owned durable storage version. The current version combines the
40
+ * internal lib version with the dev-provided app version; when the stored
41
+ * version does not match (including a missing record), the elected worker
42
+ * initializes with `clearOnInit: true`. Wiping on a missing record is
43
+ * harmless for fresh installs and correctly resets databases persisted
44
+ * before versioning existed.
45
+ */
46
+ function createStorageVersionStore({ store, dbId, appStorageVersion }) {
47
+ const key = storageVersionKey(dbId);
48
+ const currentVersion = formatStorageVersion(appStorageVersion);
49
+ return {
50
+ currentVersion,
51
+ async getVersionStatus() {
52
+ const storedVersion = await store.get(key);
53
+ if (storedVersion === void 0) return "missing";
54
+ return storedVersion === currentVersion ? "current" : "outdated";
55
+ },
56
+ /**
57
+ * Record the current version. Must be called only after the worker has
58
+ * successfully initialized with the wiped DB, so a failed init can be
59
+ * retried by a later elected worker.
60
+ */
61
+ async markCurrentVersionApplied() {
62
+ await store.set(key, currentVersion);
63
+ }
64
+ };
65
+ }
66
+ //#endregion
67
+ //#region src/web-socket/ws-remote-source.ts
68
+ const WS_OPEN = 1;
69
+ const createWsRemoteSource = ({ createWebSocket }) => {
70
+ return ({ onEventsAvailable, onClosed }) => {
71
+ const socket = createWebSocket();
72
+ const opened = createDeferredPromise();
73
+ opened.promise.catch(() => {});
74
+ const requestsMap = /* @__PURE__ */ new Map();
75
+ let onPong;
76
+ let closed = false;
77
+ const shutdown = () => {
78
+ if (closed) return false;
79
+ closed = true;
80
+ const error = /* @__PURE__ */ new Error("WebSocket closed");
81
+ opened.reject(error);
82
+ for (const request of requestsMap.values()) request.reject(error);
83
+ requestsMap.clear();
84
+ socket.close(1e3);
85
+ return true;
86
+ };
87
+ if (socket.readyState === WS_OPEN) opened.resolve();
88
+ socket.addEventListener("open", () => {
89
+ opened.resolve();
90
+ });
91
+ socket.addEventListener("close", () => {
92
+ if (shutdown()) onClosed();
93
+ });
94
+ const send = async (request) => {
95
+ await opened.promise;
96
+ if (closed) throw new Error("WebSocket closed");
97
+ const requestId = generateId();
98
+ const promise = createDeferredPromise();
99
+ requestsMap.set(requestId, promise);
100
+ socket.send(JSON.stringify({
101
+ ...request,
102
+ requestId
103
+ }));
104
+ return promise.promise;
105
+ };
106
+ const pushEvents = (request) => send({
107
+ type: "push-events",
108
+ nodeId: request.nodeId,
109
+ events: request.events
110
+ });
111
+ const pullEvents = (request) => send({
112
+ type: "pull-events",
113
+ afterSyncId: request.afterSyncId,
114
+ excludeNodeId: request.excludeNodeId
115
+ });
116
+ const ping = () => {
117
+ socket.send(HEARTBEAT_PING);
118
+ return new Promise((resolve) => {
119
+ onPong = resolve;
120
+ });
121
+ };
122
+ socket.onmessage = (event) => {
123
+ if (event.data === "pong") {
124
+ onPong?.();
125
+ return;
126
+ }
127
+ const result = jsonSafeParse(event.data);
128
+ if (!result.success || !("type" in result.data) || !result.data.type) return;
129
+ const message = result.data;
130
+ switch (message.type) {
131
+ case "events-pull-response":
132
+ case "events-push-response": {
133
+ const promise = requestsMap.get(message.requestId);
134
+ if (!promise) return;
135
+ promise.resolve(message.data);
136
+ requestsMap.delete(message.requestId);
137
+ break;
138
+ }
139
+ case "events-applied":
140
+ onEventsAvailable({
141
+ newSyncId: message.newSyncId,
142
+ remoteStateHash: message.stateHash ?? null
143
+ });
144
+ break;
145
+ default: return;
146
+ }
147
+ };
148
+ return {
149
+ pushEvents,
150
+ pullEvents,
151
+ ping,
152
+ disconnect: () => {
153
+ shutdown();
154
+ }
155
+ };
156
+ };
157
+ };
158
+ //#endregion
159
+ export { runWorkerEventLogGc as i, createStorageVersionStore as n, formatStorageVersion as r, createWsRemoteSource as t };
160
+
161
+ //# sourceMappingURL=ws-remote-source-BbPWrHB6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-remote-source-BbPWrHB6.js","names":[],"sources":["../src/worker-db/event-log-gc.ts","../src/worker-db/storage-version.ts","../src/web-socket/ws-remote-source.ts"],"sourcesContent":["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 logger?: Logger;\n};\n\nexport function runWorkerEventLogGc({ db, dbConfig, pushSyncId, logger }: EventLogGcOptions) {\n const eventsTable = dbConfig.eventsTable.fullIdentifier;\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;\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}\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 getVersionStatus(): Promise<\"current\" | \"missing\" | \"outdated\"> {\n const storedVersion = await store.get<string>(key);\n if (storedVersion === undefined) {\n return \"missing\";\n }\n return storedVersion === currentVersion ? \"current\" : \"outdated\";\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 type { SyncServerMessage, SyncServerRequest } from \"../server\";\nimport { HEARTBEAT_PING, HEARTBEAT_PONG } from \"../server/heartbeat\";\nimport type { CreateRemoteSourceFactory } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport {\n createDeferredPromise,\n type DeferredPromise,\n type DistributiveOmit,\n generateId,\n jsonSafeParse,\n} from \"../utils\";\nimport type { EventsPullRequest, EventsPushRequest, EventsPushResponse, GetEventsBatch } from \"../worker\";\n\nconst WS_OPEN = 1;\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 ({ onEventsAvailable, onClosed }) => {\n const socket = createWebSocket();\n const opened = createDeferredPromise<void>();\n opened.promise.catch(() => {});\n const requestsMap = new Map<string, DeferredPromise<unknown>>();\n let onPong: (() => void) | undefined;\n let closed = false;\n\n const shutdown = () => {\n if (closed) {\n return false;\n }\n closed = true;\n const error = new Error(\"WebSocket closed\");\n opened.reject(error);\n for (const request of requestsMap.values()) {\n request.reject(error);\n }\n requestsMap.clear();\n // Servers may not answer a close frame without a code, leaving the socket stuck in CLOSING.\n socket.close(1000);\n return true;\n };\n\n if (socket.readyState === WS_OPEN) {\n opened.resolve();\n }\n socket.addEventListener(\"open\", () => {\n opened.resolve();\n });\n // Closing on the first drop stops wrappers like PartySocket from reconnecting behind the library's back.\n socket.addEventListener(\"close\", () => {\n if (shutdown()) {\n onClosed();\n }\n });\n\n const send = async <T>(request: DistributiveOmit<SyncServerRequest, \"requestId\">): Promise<T> => {\n await opened.promise;\n if (closed) {\n throw new Error(\"WebSocket closed\");\n }\n const requestId = generateId();\n const promise = createDeferredPromise<T>();\n requestsMap.set(requestId, promise as DeferredPromise<unknown>);\n socket.send(JSON.stringify({ ...request, requestId }));\n return promise.promise;\n };\n\n const pushEvents = (request: EventsPushRequest) =>\n send<EventsPushResponse>({ type: \"push-events\", nodeId: request.nodeId, events: request.events });\n\n const pullEvents = (request: EventsPullRequest) =>\n send<GetEventsBatch>({\n type: \"pull-events\",\n afterSyncId: request.afterSyncId,\n excludeNodeId: request.excludeNodeId,\n });\n\n const ping = () => {\n socket.send(HEARTBEAT_PING);\n return new Promise<void>((resolve) => {\n onPong = resolve;\n });\n };\n\n socket.onmessage = (event) => {\n if (event.data === HEARTBEAT_PONG) {\n onPong?.();\n return;\n }\n\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 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, remoteStateHash: message.stateHash ?? null });\n break;\n default:\n message satisfies never;\n return;\n }\n };\n\n return {\n pushEvents,\n pullEvents,\n ping,\n disconnect: () => {\n shutdown();\n },\n };\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { S as CreateRemoteSourceFactory } from "./index-CH4lf3iX.js";
2
+ //#region src/web-socket/ws-remote-source.d.ts
3
+ type WsRemoteSourceConfig = {
4
+ createWebSocket: () => Pick<WebSocket, "onmessage" | "close" | "addEventListener" | "removeEventListener"> & {
5
+ readyState: number;
6
+ send: (data: string) => void;
7
+ };
8
+ };
9
+ declare const createWsRemoteSource: ({ createWebSocket }: WsRemoteSourceConfig) => CreateRemoteSourceFactory;
10
+ //#endregion
11
+ export { createWsRemoteSource as t };
12
+ //# sourceMappingURL=ws-remote-source-DFBhDc6Z.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlite-sync/core",
3
- "version": "0.12.3",
3
+ "version": "0.13.1",
4
4
  "description": "SQLite synchronization library with CRDT support",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,6 +21,10 @@
21
21
  "types": "./dist/index.d.ts",
22
22
  "exports": {
23
23
  ".": {
24
+ "react-native": {
25
+ "types": "./dist/index.native.d.ts",
26
+ "default": "./dist/index.native.js"
27
+ },
24
28
  "types": "./dist/index.d.ts",
25
29
  "import": "./dist/index.js"
26
30
  },
@@ -28,6 +32,10 @@
28
32
  "types": "./dist/internal.d.ts",
29
33
  "import": "./dist/internal.js"
30
34
  },
35
+ "./local": {
36
+ "types": "./dist/local.d.ts",
37
+ "default": "./dist/local.js"
38
+ },
31
39
  "./worker": {
32
40
  "types": "./dist/worker.d.ts",
33
41
  "import": "./dist/worker.js"