@sqlite-sync/core 0.11.0 → 0.12.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 (31) hide show
  1. package/README.md +17 -21
  2. package/dist/{crdt-sync-remote-source-CugbiosG.js → crdt-sync-remote-source-CPXzmSIS.js} +131 -96
  3. package/dist/crdt-sync-remote-source-CPXzmSIS.js.map +1 -0
  4. package/dist/{crdt-table-schema-2w_AIpsV.js → crdt-table-schema-BRm_WdcW.js} +19 -2
  5. package/dist/crdt-table-schema-BRm_WdcW.js.map +1 -0
  6. package/dist/heartbeat-CJWQAoGP.js +7 -0
  7. package/dist/heartbeat-CJWQAoGP.js.map +1 -0
  8. package/dist/{index-UfgvV97h.d.ts → index-BHsKLqcK.d.ts} +25 -15
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +213 -181
  11. package/dist/index.js.map +1 -1
  12. package/dist/internal.d.ts +2 -2
  13. package/dist/internal.js +3 -3
  14. package/dist/{make-crdt-table-D8VQ-ptj.js → make-crdt-table-C6l0F-o_.js} +2 -2
  15. package/dist/{make-crdt-table-D8VQ-ptj.js.map → make-crdt-table-C6l0F-o_.js.map} +1 -1
  16. package/dist/{reset-state-CchMrdoA.js → reset-state-t0tTGqoh.js} +23 -342
  17. package/dist/reset-state-t0tTGqoh.js.map +1 -0
  18. package/dist/server.d.ts +6 -2
  19. package/dist/server.js +4 -3
  20. package/dist/server.js.map +1 -1
  21. package/dist/system-schema-Dvl_BLId.js +523 -0
  22. package/dist/system-schema-Dvl_BLId.js.map +1 -0
  23. package/dist/worker.d.ts +2 -1
  24. package/dist/worker.js +73 -90
  25. package/dist/worker.js.map +1 -1
  26. package/package.json +1 -1
  27. package/dist/crdt-sync-remote-source-CugbiosG.js.map +0 -1
  28. package/dist/crdt-table-schema-2w_AIpsV.js.map +0 -1
  29. package/dist/reset-state-CchMrdoA.js.map +0 -1
  30. package/dist/system-schema-C2JZDv6L.js +0 -178
  31. package/dist/system-schema-C2JZDv6L.js.map +0 -1
package/dist/worker.js CHANGED
@@ -1,102 +1,74 @@
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-CchMrdoA.js";
2
- import { a as createDeferredPromise, c as generateId, l as jsonSafeParse } from "./crdt-table-schema-2w_AIpsV.js";
3
- import { a as HLCCounter, t as createCrdtSyncRemoteSource } from "./crdt-sync-remote-source-CugbiosG.js";
4
- import { n as applyWorkerDbSchema, s as workerDbConfig } from "./system-schema-C2JZDv6L.js";
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";
5
6
  import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
6
7
  //#region src/web-socket/ws-remote-source.ts
7
8
  const WS_OPEN = 1;
8
9
  const createWsRemoteSource = ({ createWebSocket }) => {
9
- return async ({ onEventsAvailable, onReconnected, signal }) => {
10
+ return ({ onEventsAvailable, onClosed }) => {
10
11
  const socket = createWebSocket();
11
- const openPromise = createDeferredPromise({
12
- timeout: 5e3,
13
- onTimeout: () => {
14
- socket.close();
15
- }
16
- });
17
- socket.addEventListener("open", () => {
18
- openPromise.resolve(void 0);
19
- });
20
- const abortOpen = () => {
21
- socket.close();
22
- openPromise.reject(signal.reason);
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;
23
26
  };
24
- signal.addEventListener("abort", abortOpen, { once: true });
25
- try {
26
- await openPromise.promise;
27
- } finally {
28
- signal.removeEventListener("abort", abortOpen);
29
- }
27
+ if (socket.readyState === WS_OPEN) opened.resolve();
30
28
  socket.addEventListener("open", () => {
31
- onReconnected();
29
+ opened.resolve();
32
30
  });
33
- const waitForOpen = () => new Promise((resolve, reject) => {
34
- if (socket.readyState === WS_OPEN) {
35
- resolve();
36
- return;
37
- }
38
- if (signal.aborted) {
39
- reject(signal.reason);
40
- return;
41
- }
42
- const onOpen = () => {
43
- signal.removeEventListener("abort", onAbort);
44
- resolve();
45
- };
46
- const onAbort = () => {
47
- socket.removeEventListener("open", onOpen);
48
- reject(signal.reason);
49
- };
50
- socket.addEventListener("open", onOpen, { once: true });
51
- signal.addEventListener("abort", onAbort, { once: true });
31
+ socket.addEventListener("close", () => {
32
+ if (shutdown()) onClosed();
52
33
  });
53
- const requestsMap = /* @__PURE__ */ new Map();
54
- const pushEvents = async (request) => {
55
- await waitForOpen();
34
+ const send = async (request) => {
35
+ await opened.promise;
36
+ if (closed) throw new Error("WebSocket closed");
56
37
  const requestId = crypto.randomUUID();
57
- const promise = createDeferredPromise({
58
- timeout: 5e3,
59
- onTimeout: () => requestsMap.delete(requestId)
60
- });
38
+ const promise = createDeferredPromise();
61
39
  requestsMap.set(requestId, promise);
62
- const wsRequest = {
63
- type: "push-events",
64
- requestId,
65
- nodeId: request.nodeId,
66
- events: request.events
67
- };
68
- socket.send(JSON.stringify(wsRequest));
40
+ socket.send(JSON.stringify({
41
+ ...request,
42
+ requestId
43
+ }));
69
44
  return promise.promise;
70
45
  };
71
- const pullEvents = async (request) => {
72
- await waitForOpen();
73
- const requestId = crypto.randomUUID();
74
- const promise = createDeferredPromise({
75
- timeout: 2e3,
76
- onTimeout: () => requestsMap.delete(requestId)
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;
77
60
  });
78
- requestsMap.set(requestId, promise);
79
- const wsRequest = {
80
- type: "pull-events",
81
- requestId,
82
- afterSyncId: request.afterSyncId,
83
- excludeNodeId: request.excludeNodeId
84
- };
85
- socket.send(JSON.stringify(wsRequest));
86
- return promise.promise;
87
61
  };
88
62
  socket.onmessage = (event) => {
63
+ if (event.data === "pong") {
64
+ onPong?.();
65
+ return;
66
+ }
89
67
  const result = jsonSafeParse(event.data);
90
68
  if (!result.success || !("type" in result.data) || !result.data.type) return;
91
69
  const message = result.data;
92
70
  switch (message.type) {
93
- case "events-pull-response": {
94
- const promise = requestsMap.get(message.requestId);
95
- if (!promise) return;
96
- promise.resolve(message.data);
97
- requestsMap.delete(message.requestId);
98
- break;
99
- }
71
+ case "events-pull-response":
100
72
  case "events-push-response": {
101
73
  const promise = requestsMap.get(message.requestId);
102
74
  if (!promise) return;
@@ -116,8 +88,9 @@ const createWsRemoteSource = ({ createWebSocket }) => {
116
88
  return {
117
89
  pushEvents,
118
90
  pullEvents,
91
+ ping,
119
92
  disconnect: () => {
120
- socket.close();
93
+ shutdown();
121
94
  }
122
95
  };
123
96
  };
@@ -180,8 +153,10 @@ function createStorageVersionStore({ store, dbId, appStorageVersion }) {
180
153
  const currentVersion = formatStorageVersion(appStorageVersion);
181
154
  return {
182
155
  currentVersion,
183
- async isVersionMismatch() {
184
- return await store.get(key) !== currentVersion;
156
+ async getVersionStatus() {
157
+ const storedVersion = await store.get(key);
158
+ if (storedVersion === void 0) return "missing";
159
+ return storedVersion === currentVersion ? "current" : "outdated";
185
160
  },
186
161
  /**
187
162
  * Record the current version. Must be called only after the worker has
@@ -229,8 +204,10 @@ async function createDbWorker(config, opts) {
229
204
  dbId: config.dbId,
230
205
  appStorageVersion: opts.storageVersion
231
206
  });
232
- const [pendingReset, isVersionMismatch] = await Promise.all([resetState.resolvePendingReset(), storageVersion.isVersionMismatch()]);
233
- if (isVersionMismatch) logger("worker", `Storage version mismatch — resetting local DB to ${storageVersion.currentVersion}`, "warning");
207
+ const [pendingReset, versionStatus] = await Promise.all([resetState.resolvePendingReset(), storageVersion.getVersionStatus()]);
208
+ const isVersionMismatch = versionStatus !== "current";
209
+ if (versionStatus === "missing") logger("worker", `Initializing local DB storage version ${storageVersion.currentVersion}`, "info");
210
+ else if (versionStatus === "outdated") logger("worker", `Storage version mismatch — resetting local DB to ${storageVersion.currentVersion}`, "warning");
234
211
  const pool = await sqlite3.installOpfsSAHPoolVfs({
235
212
  name: config.dbId,
236
213
  directory: `.${config.dbId}`,
@@ -270,12 +247,17 @@ async function createDbWorker(config, opts) {
270
247
  if (!storageGeneration.current) storageGeneration.current = generateId();
271
248
  if (pendingReset) await resetState.markResetApplied(pendingReset.epoch);
272
249
  if (isVersionMismatch) await storageVersion.markCurrentVersionApplied();
273
- if (opts.eventLogGc === true) runWorkerEventLogGc({
274
- db,
275
- dbConfig: workerDbConfig,
276
- pushSyncId,
277
- eventHlcAccumulator,
278
- logger
250
+ if (opts.eventLogGc === true) await runWithNoActiveReplicas({
251
+ dbId: config.dbId,
252
+ run: () => {
253
+ runWorkerEventLogGc({
254
+ db,
255
+ dbConfig: workerDbConfig,
256
+ pushSyncId,
257
+ eventHlcAccumulator,
258
+ logger
259
+ });
260
+ }
279
261
  });
280
262
  const crdtStorage = createCrdtStorage({
281
263
  nodeId: config.clientId,
@@ -373,6 +355,7 @@ async function createDbWorker(config, opts) {
373
355
  },
374
356
  goOnline: () => remoteSource.goOnline(),
375
357
  goOffline: () => remoteSource.goOffline("DISCONNECTED"),
358
+ dropConnection: () => remoteSource.dropConnection("NETWORK_OFFLINE"),
376
359
  sync: () => {
377
360
  remoteSource.syncWithRemote().catch((error) => console.warn("Sync failed", error));
378
361
  },
@@ -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\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 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 === WS_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, generateId } 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 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 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 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 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 */\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 { 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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlite-sync/core",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "SQLite synchronization library with CRDT support",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"file":"crdt-sync-remote-source-CugbiosG.js","names":[],"sources":["../src/hlc.ts","../src/schema/validate-crdt-event.ts","../src/sqlite-crdt/retry-remote-operation.ts","../src/sqlite-crdt/crdt-sync-remote-source.ts"],"sourcesContent":["export interface HLC {\n timestamp: number;\n counter: number;\n nodeId: string;\n}\n\nconst MAX_COUNTER = 36 ** 5 - 1; // 60,466,175 — max value that fits in 5-char base36\nexport const DEFAULT_MAX_DRIFT_MS = 6 * 60 * 60 * 1000; // 6 hours\n\nexport class HLCCounter {\n private timestamp: number;\n private counter: number;\n private nodeId: string;\n\n private readonly getTimestamp: () => number;\n private readonly maxDrift: number;\n\n constructor(nodeId: string, getTimestamp: () => number, maxDrift: number = DEFAULT_MAX_DRIFT_MS) {\n this.timestamp = getTimestamp();\n this.counter = 0;\n this.nodeId = nodeId;\n this.getTimestamp = getTimestamp;\n this.maxDrift = maxDrift;\n }\n\n getCurrentHLC(): HLC {\n return {\n timestamp: this.timestamp,\n counter: this.counter,\n nodeId: this.nodeId,\n };\n }\n\n getNextHLC(): HLC {\n const now = this.getTimestamp();\n\n if (now > this.timestamp) {\n this.timestamp = now;\n this.counter = 0;\n return this.getCurrentHLC();\n }\n\n this.counter++;\n this.rollOverCounter();\n return this.getCurrentHLC();\n }\n\n /** Restore already-persisted causal state, regardless of wall-clock drift. */\n restoreHLC(hlc: HLC) {\n if (hlc.timestamp > this.timestamp) {\n this.timestamp = hlc.timestamp;\n this.counter = hlc.counter;\n } else if (hlc.timestamp === this.timestamp) {\n this.counter = Math.max(this.counter, hlc.counter);\n }\n }\n\n mergeHLC(hlc: HLC) {\n const now = this.getTimestamp();\n if (hlc.timestamp - now > this.maxDrift) {\n console.warn(\n `HLC: ignoring far-future timestamp (remote=${hlc.timestamp}, local=${now}, drift=${hlc.timestamp - now}ms)`,\n );\n return;\n }\n\n if (this.timestamp === hlc.timestamp) {\n this.counter = Math.max(this.counter, hlc.counter) + 1;\n } else if (this.timestamp > hlc.timestamp) {\n this.counter++;\n } else {\n this.timestamp = hlc.timestamp;\n this.counter = hlc.counter + 1;\n }\n this.rollOverCounter();\n }\n\n private rollOverCounter() {\n if (this.counter > MAX_COUNTER) {\n this.timestamp += 1;\n this.counter = 0;\n }\n }\n}\n\nexport function serializeHLC(hlc: HLC) {\n return `${hlc.timestamp.toString().padStart(15, \"0\")}:${hlc.counter.toString(36).padStart(5, \"0\")}:${hlc.nodeId}`;\n}\n\nexport function deserializeHLC(serialized: string): HLC {\n const parts = serialized.split(\":\");\n if (parts.length < 3) {\n throw new Error(`Invalid HLC format: expected at least 3 colon-separated segments, got ${parts.length}`);\n }\n\n const timestamp = parseInt(parts[0], 10);\n const counter = parseInt(parts[1], 36);\n\n if (Number.isNaN(timestamp) || Number.isNaN(counter)) {\n throw new Error(`Invalid HLC values: timestamp=${parts[0]}, counter=${parts[1]}`);\n }\n\n return {\n timestamp,\n counter,\n nodeId: parts.slice(2).join(\":\"),\n };\n}\n\nexport function compareHLC(one: HLC, two: HLC) {\n if (one.timestamp === two.timestamp) {\n if (one.counter === two.counter) {\n if (one.nodeId === two.nodeId) {\n return 0;\n }\n return one.nodeId < two.nodeId ? -1 : 1;\n }\n return one.counter - two.counter;\n }\n return one.timestamp - two.timestamp;\n}\n","import type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport type { CrdtEventType, NewCrdtEvent } from \"../sqlite-crdt/crdt-table-schema\";\nimport type { AnyTableBuilder } from \"./table-builder\";\n\nexport type NewCrdtEventValidationResult =\n | { success: true; event: NewCrdtEvent }\n | { success: false; errors: string[] };\n\nexport class CrdtEventValidationError extends Error {\n constructor(readonly errors: string[]) {\n super(`Invalid CRDT events: ${errors.join(\"; \")}`);\n this.name = \"CrdtEventValidationError\";\n }\n}\n\nconst crdtEventTypes = [\"item-created\", \"item-updated\", \"item-deleted\"] as const;\n\nexport function validateNewCrdtEvent(\n schema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">,\n input: unknown,\n): NewCrdtEventValidationResult {\n const errors: string[] = [];\n\n if (!isRecord(input)) {\n return { success: false, errors: [\"Event must be an object\"] } satisfies NewCrdtEventValidationResult;\n }\n\n const type = input.type;\n const dataset = input.dataset;\n const itemId = input.item_id;\n\n let payload = input.payload;\n let payloadParseFailed = false;\n if (typeof payload === \"string\") {\n try {\n payload = JSON.parse(payload);\n } catch {\n payloadParseFailed = true;\n errors.push(`Event payload is not valid JSON`);\n }\n }\n\n if (!isCrdtEventType(type)) {\n errors.push(`Invalid event type \"${String(type)}\"`);\n }\n if (typeof dataset !== \"string\") {\n errors.push(`Event dataset must be a string`);\n }\n if (typeof itemId !== \"string\") {\n errors.push(`Event item_id must be a string`);\n }\n if (!payloadParseFailed && !isRecord(payload)) {\n errors.push(`Event payload must be an object`);\n }\n\n const datasetMatch = typeof dataset === \"string\" ? findTableForDataset(schema, dataset) : null;\n if (typeof dataset === \"string\" && !datasetMatch) {\n errors.push(`Unknown dataset \"${dataset}\"`);\n }\n\n if (isCrdtEventType(type) && datasetMatch && isRecord(payload)) {\n switch (type) {\n case \"item-created\": {\n const result = datasetMatch.table.validatePayload(payload, { event: \"item-created\" });\n if (!result.success) {\n errors.push(...result.errors.map((error) => `payload: ${error}`));\n }\n if (typeof itemId === \"string\" && \"id\" in payload && payload.id !== itemId) {\n errors.push(`payload: Column \"id\" must match item_id \"${itemId}\"`);\n }\n break;\n }\n case \"item-updated\": {\n const result = datasetMatch.table.validatePayload(payload, { event: \"item-updated\" });\n if (!result.success) {\n errors.push(...result.errors.map((error) => `payload: ${error}`));\n }\n break;\n }\n case \"item-deleted\":\n if (Object.keys(payload).length > 0) {\n errors.push(`payload: item-deleted events must have an empty payload`);\n }\n break;\n }\n }\n\n if (errors.length > 0) {\n return { success: false, errors } satisfies NewCrdtEventValidationResult;\n }\n\n return {\n success: true,\n event: {\n type: type as CrdtEventType,\n dataset: datasetMatch?.baseTableName ?? (dataset as string),\n item_id: itemId as string,\n payload: payload as Record<string, unknown>,\n },\n } satisfies NewCrdtEventValidationResult;\n}\n\nfunction findTableForDataset(\n schema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">,\n dataset: string,\n): { table: AnyTableBuilder; baseTableName: string } | null {\n const config = schema.tablesConfig.find(\n (tableConfig) => tableConfig.baseTableName === dataset || tableConfig.crdtTableName === dataset,\n );\n if (config) {\n const table = schema.tables[config.crdtTableName];\n return table ? { table, baseTableName: config.baseTableName } : null;\n }\n const table = Object.hasOwn(schema.tables, dataset) ? schema.tables[dataset] : undefined;\n return table ? { table, baseTableName: dataset } : null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isCrdtEventType(value: unknown): value is CrdtEventType {\n return typeof value === \"string\" && crdtEventTypes.includes(value as CrdtEventType);\n}\n","type RetryOptions = {\n maxAttempts: number;\n backoffBaseMs: number;\n backoffExponent: number;\n backoffJitterMs: number;\n timeoutMs: number;\n shouldRetry?: (error: unknown) => boolean;\n};\n\nexport const REMOTE_RETRY_OPTIONS: RetryOptions = {\n maxAttempts: 3,\n backoffBaseMs: 100,\n backoffExponent: 1.5,\n backoffJitterMs: 150,\n timeoutMs: 10000,\n};\n\nclass RetryTimeoutError extends Error {\n constructor(\n message: string,\n public previous?: unknown,\n ) {\n super(message);\n this.name = \"TimeoutError\";\n }\n}\n\nconst applyJitter = (delayMs: number, maxJitterMs: number): number => {\n const jitter = Math.random() * maxJitterMs * (Math.random() > 0.5 ? 1 : -1);\n return Math.max(0, delayMs + jitter);\n};\n\nconst delay = (delayMs: number) => new Promise((resolve) => setTimeout(resolve, delayMs));\n\nconst withTimeout = async <T>(operation: () => Promise<T>, timeoutMs: number, previousError?: unknown): Promise<T> => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n try {\n return await Promise.race([\n operation(),\n new Promise<never>((_, reject) => {\n timeoutId = setTimeout(\n () => reject(new RetryTimeoutError(\"Remote operation timed out\", previousError)),\n timeoutMs,\n );\n }),\n ]);\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n};\n\nexport const retryRemoteOperation = async <T>(operation: () => Promise<T>, options: RetryOptions): Promise<T> => {\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {\n try {\n return await withTimeout(operation, options.timeoutMs, lastError);\n } catch (error) {\n lastError = error;\n\n if (attempt >= options.maxAttempts || options.shouldRetry?.(error) === false) {\n throw error;\n }\n\n const backoffDelay = applyJitter(\n options.backoffBaseMs * options.backoffExponent ** (attempt - 1),\n options.backoffJitterMs,\n );\n if (backoffDelay > 0) {\n await delay(backoffDelay);\n }\n }\n }\n\n throw lastError;\n};\n","import type { SyncDbMigrator } from \"../migrations/migrator\";\nimport { createTypedEventTarget, ensureSingletonExecution, tryCatchAsync } from \"../utils\";\nimport type { EventsPullResponse, WorkerState } from \"../worker-db/worker-common\";\nimport type { PendingCrdtEvent } from \"./apply-crdt-event\";\nimport type { CrdtStorage } from \"./crdt-storage\";\nimport { REMOTE_RETRY_OPTIONS, retryRemoteOperation } from \"./retry-remote-operation\";\nimport type { StoredValue } from \"./stored-value\";\n\ntype CrdtSyncRemoteSourceConfig = {\n bufferSize: number;\n storage: CrdtStorage;\n migrator: SyncDbMigrator;\n pullSyncId: StoredValue<number>;\n pushSyncId: StoredValue<number>;\n nodeId: string;\n remoteFactory?: CreateRemoteSourceFactory;\n};\n\nexport type EventsPullRequest = {\n afterSyncId: number;\n excludeNodeId?: string;\n};\n\nexport const PUSH_BATCH_SIZE = 75;\nexport const MAX_PUSH_EVENTS = 100;\n\nexport type EventsPushRequest = {\n nodeId: string;\n events: (PendingCrdtEvent & { schema_version: number })[];\n};\nexport type EventsPushResponse = {\n ok: boolean;\n /** Remote sync_id right before the pushed events were enqueued. */\n beforeSyncId?: number;\n /** Remote sync_id right after the pushed events were enqueued. */\n afterSyncId?: number;\n};\n\nconst DISCONNECT_WATCHDOG_MS = 15_000;\n\nexport type CrdtSyncRemoteSource = ReturnType<typeof createCrdtSyncRemoteSource>;\n\nexport type EventsAvailable = {\n newSyncId: number;\n remoteEventHlcSum: string | null;\n};\n\n/**\n * Opens a connection to the remote. Each call must return an independent source: the library calls\n * `disconnect` on every returned source exactly once, including one that resolves after `signal`\n * has aborted. `signal` aborts when the connection attempt is cancelled or the connection is closed.\n * Call `onReconnected` when the transport re-establishes itself without a new source: broadcasts\n * sent while it was down are lost, so the library pulls and pushes to catch up.\n */\nexport type CreateRemoteSourceFactory = (opts: {\n onEventsAvailable: (event: EventsAvailable) => void;\n onReconnected: () => void;\n signal: AbortSignal;\n}) => RemoteSource | Promise<RemoteSource>;\n\ntype RemoteSource = {\n pullEvents: (request: EventsPullRequest) => Promise<EventsPullResponse>;\n pushEvents: (request: EventsPushRequest) => Promise<EventsPushResponse>;\n disconnect?: () => void | Promise<void>;\n};\n\nexport class SchemaVersionMismatchError extends Error {\n constructor(\n public remoteSchemaVersion: number,\n public localSchemaVersion: number,\n ) {\n super(`Schema version mismatch: remote ${remoteSchemaVersion} != local ${localSchemaVersion}`);\n this.name = \"SchemaVersionMismatchError\";\n }\n}\n\ntype RemoteSourceState =\n | {\n type: \"pending\";\n deSynced: boolean;\n schemaVersionMismatched: boolean;\n }\n | {\n type: \"offline\";\n reason: OfflineReason;\n deSynced: boolean;\n schemaVersionMismatched: boolean;\n }\n | {\n type: \"online\";\n deSynced: boolean;\n schemaVersionMismatched: boolean;\n };\n\nexport type OfflineReason =\n | \"NOT_INITIALIZED\"\n | \"INITIALIZATION_FAILED\"\n | \"REMOTE_PUSH_ERROR\"\n | \"REMOTE_PULL_ERROR\"\n | \"DISCONNECTED\";\n\nexport type DeSyncDetectedReason = \"CHECKSUM_MISMATCH\" | \"ERROR_APPLYING_REMOTE_EVENT\";\n\nexport const createCrdtSyncRemoteSource = ({\n bufferSize,\n storage,\n migrator,\n pullSyncId,\n pushSyncId,\n nodeId,\n remoteFactory,\n}: CrdtSyncRemoteSourceConfig) => {\n const eventTarget = createTypedEventTarget<{\n \"state-changed\": RemoteSourceState[\"type\"];\n \"de-sync-detected\": {\n reason: DeSyncDetectedReason;\n };\n \"remote-schema-version-mismatch\": {\n remoteSchemaVersion: number;\n localSchemaVersion: number;\n };\n }>();\n\n let remoteState: RemoteSourceState = {\n type: \"offline\",\n reason: \"NOT_INITIALIZED\",\n deSynced: false,\n schemaVersionMismatched: false,\n };\n\n const patchRemoteState = (state: Partial<RemoteSourceState>) => {\n remoteState = { ...remoteState, ...state } as RemoteSourceState;\n eventTarget.dispatchEvent(\"state-changed\", remoteState.type);\n };\n\n let current: Connection | null = null;\n let disposed = false;\n let wantsOnline = false;\n\n const disconnectSource = (source: RemoteSource) => {\n const watchdog = setTimeout(() => {\n console.error(`Remote source did not disconnect within ${DISCONNECT_WATCHDOG_MS}ms`);\n }, DISCONNECT_WATCHDOG_MS);\n void tryCatchAsync(async () => await source.disconnect?.()).then((result) => {\n clearTimeout(watchdog);\n if (!result.success) {\n console.warn(\"Error while disconnecting from remote source\", result.error);\n }\n });\n };\n\n const closeConnection = (connection: Connection, reason: OfflineReason) => {\n if (current !== connection) {\n return;\n }\n current = null;\n patchRemoteState({ type: \"offline\", reason });\n connection.close();\n };\n\n type Connection = ReturnType<typeof createConnection>;\n const createConnection = (factory: CreateRemoteSourceFactory) => {\n const controller = new AbortController();\n const { signal } = controller;\n let source: RemoteSource | null = null;\n\n const untilClosed = <T>(operation: () => Promise<T>) =>\n new Promise<T>((resolve, reject) => {\n const onAbort = () => reject(new Error(\"Remote connection closed\"));\n if (signal.aborted) {\n onAbort();\n return;\n }\n signal.addEventListener(\"abort\", onAbort, { once: true });\n operation()\n .then(resolve, reject)\n .finally(() => signal.removeEventListener(\"abort\", onAbort));\n });\n\n const retryOptions = { ...REMOTE_RETRY_OPTIONS, shouldRetry: () => !signal.aborted };\n\n const connect = async () => {\n if (signal.aborted) {\n return;\n }\n\n const result = await tryCatchAsync(\n async () =>\n await factory({\n onEventsAvailable: ({ newSyncId, remoteEventHlcSum }) => {\n pull({ remoteSyncId: newSyncId, remoteEventHlcSum, includeSelf: false });\n },\n onReconnected: () => {\n void sync();\n },\n signal,\n }),\n );\n\n if (signal.aborted) {\n if (result.success) {\n disconnectSource(result.data);\n }\n return;\n }\n\n if (!result.success) {\n console.warn(\"Failed to create remote source\", result.error);\n closeConnection(connection, \"INITIALIZATION_FAILED\");\n return;\n }\n\n source = result.data;\n patchRemoteState({ type: \"online\", deSynced: false, schemaVersionMismatched: false });\n void sync();\n };\n\n type ActivePull = { promise: Promise<void>; requestedSyncId: number | null };\n let activePull: ActivePull | null = null;\n const pull = (request?: {\n remoteSyncId?: number;\n remoteEventHlcSum?: string | null;\n includeSelf?: boolean;\n }): Promise<void> => {\n if (!source || signal.aborted) {\n return Promise.resolve();\n }\n const activeSource = source;\n\n const remoteSyncId = request?.remoteSyncId;\n\n if (remoteSyncId !== undefined && remoteSyncId <= pullSyncId.current) {\n // We are already caught up to this broadcast, so there is nothing to pull.\n // This is the quiescent moment to verify we have not diverged from the\n // remote (the check is a no-op unless we are exactly aligned: remoteSyncId\n // === pullSyncId.current).\n checkRemoteConsistency(remoteSyncId, request?.remoteEventHlcSum ?? null);\n return Promise.resolve();\n }\n\n if (activePull) {\n if (remoteSyncId !== undefined && (!activePull.requestedSyncId || activePull.requestedSyncId < remoteSyncId)) {\n activePull.requestedSyncId = remoteSyncId;\n }\n return activePull.promise;\n }\n\n const nextPull: ActivePull = { promise: Promise.resolve(), requestedSyncId: null };\n nextPull.promise = pullAllEvents(activeSource, {\n afterSyncId: pullSyncId.current,\n excludeNodeId: request?.includeSelf ? undefined : nodeId,\n })\n .catch((error) => {\n if (signal.aborted) {\n return;\n }\n console.error(\"Error pulling events. Going offline.\", error);\n closeConnection(connection, \"REMOTE_PULL_ERROR\");\n })\n .finally(() => {\n if (activePull === nextPull) {\n activePull = null;\n }\n\n const nextTarget = nextPull.requestedSyncId;\n if (nextTarget && nextTarget > pullSyncId.current) {\n pull({ remoteSyncId: nextTarget });\n }\n });\n activePull = nextPull;\n return nextPull.promise;\n };\n\n const pullAllEvents = async (activeSource: RemoteSource, opts: EventsPullRequest) => {\n let hasMore = true;\n let afterSyncId = opts.afterSyncId;\n while (hasMore) {\n const response = await retryRemoteOperation(\n () =>\n untilClosed(() =>\n activeSource.pullEvents({\n ...opts,\n afterSyncId,\n }),\n ),\n retryOptions,\n );\n if (signal.aborted) {\n return;\n }\n hasMore = response.hasMore;\n afterSyncId = response.nextSyncId;\n\n if (response.events) {\n const { processed } = storage.enqueueRemoteEvents(\n response.events.map((x) => {\n if (x.schema_version > migrator.currentSchemaVersion) {\n eventTarget.dispatchEvent(\"remote-schema-version-mismatch\", {\n remoteSchemaVersion: x.schema_version,\n localSchemaVersion: migrator.currentSchemaVersion,\n });\n if (remoteState.type === \"online\" && !remoteState.schemaVersionMismatched) {\n patchRemoteState({ schemaVersionMismatched: true });\n }\n throw new SchemaVersionMismatchError(x.schema_version, migrator.currentSchemaVersion);\n }\n return x;\n }),\n );\n await untilClosed(() => processed);\n if (signal.aborted) {\n return;\n }\n }\n if (response.nextSyncId <= pullSyncId.current) {\n break;\n }\n if (response.nextSyncId > pullSyncId.current) {\n pullSyncId.current = response.nextSyncId;\n }\n }\n };\n\n const push = ensureSingletonExecution(async () => {\n while (source && !signal.aborted) {\n const activeSource = source;\n const eventsBatch = storage.getEventsBatch({\n status: \"applied\",\n afterSyncId: pushSyncId.current,\n excludeOrigin: \"remote\",\n limit: bufferSize,\n });\n if (eventsBatch.events.length === 0) {\n break;\n }\n\n let response: EventsPushResponse;\n try {\n response = await retryRemoteOperation(\n () =>\n untilClosed(() =>\n activeSource.pushEvents({\n nodeId,\n events: eventsBatch.events.map((event) => ({\n schema_version: event.schema_version,\n timestamp: event.timestamp,\n type: event.type,\n dataset: event.dataset,\n item_id: event.item_id,\n payload: event.payload,\n })),\n }),\n ),\n retryOptions,\n );\n } catch (error) {\n if (!signal.aborted) {\n console.error(\"Error pushing events. Going offline.\", error);\n closeConnection(connection, \"REMOTE_PUSH_ERROR\");\n }\n return;\n }\n\n if (signal.aborted) {\n return;\n }\n\n if (!response.ok) {\n console.error(\"Remote rejected pushed events. Going offline.\");\n closeConnection(connection, \"REMOTE_PUSH_ERROR\");\n return;\n }\n\n pushSyncId.current = eventsBatch.nextSyncId;\n\n // Fast-forward the pull cursor: the remote assigns sync ids for the pushed\n // events synchronously, so (beforeSyncId, afterSyncId] contains only this\n // node's own events. If we are caught up to at least beforeSyncId, the skipped\n // range (pullSyncId, afterSyncId] contains only our own events, so there is\n // nothing to pull up to afterSyncId.\n if (\n response.beforeSyncId !== undefined &&\n response.afterSyncId !== undefined &&\n response.beforeSyncId <= pullSyncId.current &&\n response.afterSyncId > pullSyncId.current\n ) {\n pullSyncId.current = response.afterSyncId;\n }\n if (!eventsBatch.hasMore) {\n break;\n }\n }\n });\n\n // Waits out in-flight operations first: they may have started before the caller's request.\n const sync = async () => {\n await activePull?.promise;\n await pull();\n await push.promise();\n await push();\n };\n\n const close = () => {\n controller.abort();\n if (source) {\n disconnectSource(source);\n }\n };\n\n const ready = Promise.resolve()\n .then(() => untilClosed(connect))\n .catch((error) => {\n if (!signal.aborted) {\n throw error;\n }\n });\n\n const connection = { ready, close, push, sync };\n return connection;\n };\n\n const goOnline = (): Promise<void> => {\n if (disposed) {\n return Promise.resolve();\n }\n if (current) {\n return current.ready;\n }\n if (!remoteFactory) {\n console.warn(\"Remote source factory not provided. Going offline.\");\n patchRemoteState({ type: \"offline\", reason: \"NOT_INITIALIZED\" });\n return Promise.resolve();\n }\n\n wantsOnline = true;\n const connection = createConnection(remoteFactory);\n current = connection;\n patchRemoteState({ type: \"pending\" });\n return connection.ready;\n };\n\n const goOffline = async (reason: OfflineReason) => {\n wantsOnline = false;\n if (current) {\n closeConnection(current, reason);\n }\n };\n\n // Reconnects only when a connection error, not goOffline, took us offline. Retrying after a schema\n // mismatch would fail the same way.\n const syncWithRemote = async () => {\n if (current) {\n await current.sync();\n } else if (wantsOnline && !remoteState.schemaVersionMismatched) {\n await goOnline();\n }\n };\n\n // De-sync detection: when we are exactly caught up to the remote's broadcast\n // sync id and fully quiescent, our applied-event set must equal the remote's,\n // so our HLC checksums must match. A mismatch means the nodes have diverged.\n const checkRemoteConsistency = (remoteSyncId: number, remoteEventHlcSum: string | null) => {\n // A remote with no accumulator gives us nothing to compare against.\n if (remoteEventHlcSum === null) {\n return;\n }\n\n // Only meaningful when we are exactly caught up: if we are behind we still\n // need to pull; if we are ahead our state covers events the remote checksum\n // does not.\n if (remoteSyncId !== pullSyncId.current) {\n return;\n }\n\n // Quiescence: the accumulator only matches the remote's when nothing is left\n // to apply locally and no local applied events are still waiting to be pushed\n // (those are in our accumulator but the remote has not seen them yet).\n if (!storage.checkIsQuiescent(pushSyncId.current)) {\n return;\n }\n\n const localEventHlcSum = storage.getEventHlcAccumulator();\n if (localEventHlcSum === null) {\n return;\n }\n\n if (localEventHlcSum === remoteEventHlcSum) {\n // No de-sync detected.\n return;\n }\n\n eventTarget.dispatchEvent(\"de-sync-detected\", { reason: \"CHECKSUM_MISMATCH\" });\n console.warn(\n `[sqlite-sync] De-sync detected at syncId ${remoteSyncId}: local HLC checksum ${localEventHlcSum} != remote ${remoteEventHlcSum}. Local and remote have diverged despite being caught up.`,\n );\n if (remoteState.type === \"online\" && !remoteState.deSynced) {\n patchRemoteState({ deSynced: true });\n }\n };\n\n const eventsAppliedSubscription = storage.addEventListener(\"events-applied\", () => {\n current?.push();\n });\n\n const remoteEventApplyFailedSubscription = storage.addEventListener(\"remote-event-apply-failed\", () => {\n eventTarget.dispatchEvent(\"de-sync-detected\", { reason: \"ERROR_APPLYING_REMOTE_EVENT\" });\n if (remoteState.type === \"online\" && !remoteState.deSynced) {\n patchRemoteState({ deSynced: true });\n }\n });\n\n const getState = (): WorkerState => ({\n remoteState: remoteState.type,\n deSynced: remoteState.deSynced,\n schemaVersionMismatched: remoteState.schemaVersionMismatched,\n });\n\n const dispose = async () => {\n disposed = true;\n eventsAppliedSubscription.unsubscribe();\n remoteEventApplyFailedSubscription.unsubscribe();\n await goOffline(\"DISCONNECTED\");\n };\n\n return {\n goOnline,\n goOffline,\n syncWithRemote,\n getState,\n dispose,\n addEventListener: eventTarget.addEventListener,\n removeEventListener: eventTarget.removeEventListener,\n };\n};\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"crdt-table-schema-2w_AIpsV.js","names":[],"sources":["../src/utils.ts","../src/sqlite-crdt/crdt-table-schema.ts"],"sourcesContent":["export type DeferredPromise<T> = {\n promise: Promise<T>;\n resolve: (value: T) => void;\n reject: (error: Error) => void;\n};\n\ntype DeferredPromiseOptions = {\n timeout?: number;\n onTimeout?: () => void;\n};\n\nexport function createDeferredPromise<T>(opts?: DeferredPromiseOptions): DeferredPromise<T> {\n let resolve!: (value: T | PromiseLike<T>) => void;\n let reject!: (reason?: unknown) => void;\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const promise = new Promise<T>((_resolve, _reject) => {\n resolve = (value) => {\n if (timeoutId) clearTimeout(timeoutId);\n _resolve(value);\n };\n reject = (reason) => {\n if (timeoutId) clearTimeout(timeoutId);\n _reject(reason);\n };\n\n if (opts?.timeout) {\n timeoutId = setTimeout(rejectTimeout, opts.timeout, _reject, opts);\n }\n });\n\n return { promise, resolve, reject };\n}\n\nfunction rejectTimeout(reject: (reason?: unknown) => void, opts: DeferredPromiseOptions) {\n reject(new Error(`Promise timed out after ${opts.timeout}ms`));\n tryCatch(() => opts?.onTimeout?.());\n}\n\nexport const generateId = () => {\n return crypto.randomUUID();\n};\n\nexport type DistributiveOmit<T, K extends keyof T> = T extends any ? Omit<T, K> : never;\n\nexport function ensureSingletonExecution<T, TArgs extends any[]>(\n fn: (...args: TArgs) => Promise<T>,\n opts: { queueReExecution?: boolean } = { queueReExecution: true },\n) {\n let executingPromise: Promise<T> | null = null;\n let shouldReExecute = false;\n\n const wrappedFn = (...args: TArgs) => {\n if (executingPromise) {\n shouldReExecute = true;\n return executingPromise;\n }\n\n executingPromise = fn(...args).finally(() => {\n executingPromise = null;\n\n if (shouldReExecute && opts?.queueReExecution) {\n shouldReExecute = false;\n wrappedFn(...args);\n }\n });\n return executingPromise;\n };\n\n wrappedFn.promise = () => executingPromise;\n wrappedFn.isExecuting = () => !!executingPromise;\n\n return wrappedFn;\n}\n\nexport function createAutoFlushBuffer<T>({ size, flush }: { size: number; flush: (items: T[]) => void }) {\n const buffer: T[] = [];\n\n return {\n add(item: T) {\n buffer.push(item);\n if (buffer.length >= size) {\n flush(buffer);\n buffer.length = 0;\n }\n },\n flush() {\n flush(buffer);\n buffer.length = 0;\n },\n };\n}\n\nexport function createAsyncAutoFlushBuffer<T>({\n size,\n flush,\n}: {\n size: number;\n flush: (items: T[]) => void | Promise<void>;\n}) {\n const buffer: T[] = [];\n\n return {\n async add(item: T) {\n buffer.push(item);\n if (buffer.length >= size) {\n await this.flush();\n }\n },\n async flush() {\n const itemsToFlush = buffer.splice(0);\n if (itemsToFlush.length === 0) {\n return;\n }\n await flush(itemsToFlush);\n },\n };\n}\n\nexport class TypedBroadcastChannel<TMessage> {\n private readonly channel: BroadcastChannel;\n\n constructor(name: string) {\n this.channel = new BroadcastChannel(name);\n }\n\n postMessage(message: TMessage) {\n this.channel.postMessage(message);\n }\n\n set onmessage(callback: ((event: MessageEvent<TMessage>) => void) | null) {\n this.channel.onmessage = callback;\n }\n\n close() {\n this.channel.close();\n }\n}\n\nexport class TypedEvent<T = unknown> extends Event {\n readonly payload: T;\n constructor(type: string, payload: T) {\n super(type);\n this.payload = payload;\n }\n}\n\nexport type TypedEventTarget<T extends Record<string, unknown>> = {\n addEventListener: <K extends keyof T & string>(\n type: K,\n listener: (event: TypedEvent<T[K]>) => void,\n ) => { unsubscribe: () => void };\n removeEventListener: <K extends keyof T & string>(type: K, listener: (event: TypedEvent<T[K]>) => void) => void;\n dispatchEvent: <K extends keyof T & string>(type: K, payload: T[K]) => void;\n};\n\nexport const createTypedEventTarget = <T extends Record<string, unknown>>(): TypedEventTarget<T> => {\n const eventTarget = new EventTarget();\n\n const addEventListener = <K extends keyof T & string>(type: K, listener: (event: TypedEvent<T[K]>) => void) => {\n eventTarget.addEventListener(type, listener as (e: Event) => void);\n\n return {\n unsubscribe: () => {\n eventTarget.removeEventListener(type, listener as (e: Event) => void);\n },\n };\n };\n\n const removeEventListener = <K extends keyof T & string>(type: K, listener: (event: TypedEvent<T[K]>) => void) => {\n eventTarget.removeEventListener(type, listener as (e: Event) => void);\n };\n\n const dispatchEvent = <K extends keyof T & string>(type: K, payload: T[K]) => {\n eventTarget.dispatchEvent(new TypedEvent(type, payload));\n };\n\n return {\n addEventListener,\n removeEventListener,\n dispatchEvent,\n };\n};\n\ntype TryCatchResult<T> =\n | {\n success: true;\n data: T;\n }\n | {\n success: false;\n error: unknown;\n };\n\nexport function tryCatch<T>(fn: () => T): TryCatchResult<T> {\n try {\n return {\n success: true,\n data: fn(),\n };\n } catch (error) {\n return {\n success: false,\n error,\n };\n }\n}\n\nexport async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<TryCatchResult<T>> {\n try {\n return {\n success: true,\n data: await fn(),\n };\n } catch (error) {\n return {\n success: false,\n error,\n };\n }\n}\n\nexport function jsonSafeParse<T>(json: string) {\n return tryCatch(() => JSON.parse(json) as T);\n}\n\n/** Quote a SQLite identifier (table/column name), handling dot-separated schema qualifiers. */\nexport function quoteId(name: string): string {\n return name\n .split(\".\")\n .map((s) => `\"${s.replace(/\"/g, '\"\"')}\"`)\n .join(\".\");\n}\n\nexport function noop(..._args: unknown[]): void {}\n\nexport type ParsedTableName = {\n schema: \"main\" | (string & {});\n table: string;\n fullIdentifier: string;\n};\n\nexport function parseTableName(tableName: string): ParsedTableName {\n if (!tableName?.trim()) {\n throw new Error(\"Parse table: missing table name\");\n }\n const parts = tableName.split(\".\");\n if (parts.length > 2) {\n throw new Error(\"Parse table: too many dot-delimited segments in table name\");\n }\n\n return parts.length === 1\n ? {\n schema: \"main\",\n table: parts[0],\n fullIdentifier: parts[0],\n }\n : {\n schema: parts[0],\n table: parts[1],\n fullIdentifier: `${parts[0]}.${parts[1]}`,\n };\n}\n","import type { SchemaModule } from \"kysely\";\n\nexport type NewCrdtEvent = {\n type: CrdtEventType;\n dataset: string;\n item_id: string;\n payload: Record<string, unknown>;\n};\n\nexport type CrdtEventType = \"item-created\" | \"item-updated\" | \"item-deleted\";\n\nexport type CrdtEventStatus = \"pending\" | \"applied\" | \"failed\" | \"deduped\";\n\n/** Persisted on applied events that were accepted but did not mutate materialized state. */\nexport const CRDT_EVENT_NO_OP_PAYLOAD = \"no-op\";\n\nexport function isNoOpCrdtEventPayload(payload: string) {\n return payload === CRDT_EVENT_NO_OP_PAYLOAD;\n}\n\nexport type CrdtEventOrigin = \"remote\" | \"own\" | \"own-applied\" | \"local\";\n\nexport type PersistedCrdtEvent = {\n schema_version: number;\n sync_id: number;\n status: CrdtEventStatus;\n type: CrdtEventType;\n timestamp: string;\n origin: CrdtEventOrigin;\n source_node_id: string;\n dataset: string;\n item_id: string;\n payload: string;\n};\n\nexport type CrdtUpdateLogItem = {\n dataset: string;\n item_id: string;\n payload: string;\n};\n\nexport type CrdtUpdateLogPayload = Record<string, string>;\n\nexport const crdtSchema = {\n persistedEventsTable: createPersistedEventsTable,\n crdtUpdateLogTable: createCrdtUpdateLogTableQuery,\n};\n\nfunction createPersistedEventsTable(schema: SchemaModule, tableName: string) {\n return schema\n .createTable(tableName)\n .ifNotExists()\n .addColumn(\"sync_id\", \"integer\", (col) => col.notNull().primaryKey())\n .addColumn(\"schema_version\", \"integer\", (col) => col.notNull())\n .addColumn(\"status\", \"text\", (col) => col.notNull())\n .addColumn(\"type\", \"text\", (col) => col.notNull())\n .addColumn(\"timestamp\", \"text\", (col) => col.notNull())\n .addColumn(\"origin\", \"text\", (col) => col.notNull())\n .addColumn(\"dataset\", \"text\", (col) => col.notNull())\n .addColumn(\"item_id\", \"text\", (col) => col.notNull())\n .addColumn(\"payload\", \"text\", (col) => col.notNull());\n}\n\nfunction createCrdtUpdateLogTableQuery(schema: SchemaModule, tableName: string) {\n return schema\n .createTable(tableName)\n .ifNotExists()\n .addColumn(\"dataset\", \"text\", (col) => col.notNull())\n .addColumn(\"item_id\", \"text\", (col) => col.notNull())\n .addColumn(\"payload\", \"text\", (col) => col.notNull())\n .addPrimaryKeyConstraint(`pk_${tableName}`, [\"item_id\", \"dataset\"]);\n}\n"],"mappings":"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"}