@sqlite-sync/core 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $t as SyncDbSchema, Ct as jsonSafeParse, S as EventsPullResponse, U as ClientDatasetRejectionReason, b as EventsPushResponse, gn as MigratableEvent, yn as SyncDbMigrator } from "./index-UfgvV97h.js";
1
+ import { $t as SyncDbSchema, Ct as jsonSafeParse, S as EventsPullResponse, U as ClientDatasetRejectionReason, b as EventsPushResponse, gn as MigratableEvent, yn as SyncDbMigrator } from "./index-3zVvNWRd.js";
2
2
  import { z } from "zod";
3
3
  //#region src/server/admit-client-push.d.ts
4
4
  type PushedCrdtEvent = MigratableEvent & {
@@ -28,6 +28,10 @@ type AdmitClientPushOptions<T> = {
28
28
  */
29
29
  declare function admitClientPush<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>): AdmitClientPushResult<T>;
30
30
  //#endregion
31
+ //#region src/server/heartbeat.d.ts
32
+ declare const HEARTBEAT_PING = "ping";
33
+ declare const HEARTBEAT_PONG = "pong";
34
+ //#endregion
31
35
  //#region src/server/server-common.d.ts
32
36
  declare const syncServerZodSchema: {
33
37
  pullEvents: z.ZodObject<{
@@ -98,5 +102,5 @@ type ExtractSyncServerRequest<T extends SyncServerRequest["type"]> = Extract<Syn
98
102
  type: T;
99
103
  }>;
100
104
  //#endregion
101
- export { type AdmitClientPushResult, type ExtractSyncServerRequest, type PushRejection, type PushRejectionReason, type PushedCrdtEvent, type SyncServerMessage, type SyncServerRequest, admitClientPush, jsonSafeParse, syncServerZodSchema };
105
+ export { type AdmitClientPushResult, type ExtractSyncServerRequest, HEARTBEAT_PING, HEARTBEAT_PONG, type PushRejection, type PushRejectionReason, type PushedCrdtEvent, type SyncServerMessage, type SyncServerRequest, admitClientPush, jsonSafeParse, syncServerZodSchema };
102
106
  //# sourceMappingURL=server.d.ts.map
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { l as jsonSafeParse, n as isNoOpCrdtEventPayload, t as CRDT_EVENT_NO_OP_PAYLOAD } from "./crdt-table-schema-2w_AIpsV.js";
2
- import { r as validateNewCrdtEvent } from "./crdt-sync-remote-source-CugbiosG.js";
2
+ import { r as validateNewCrdtEvent } from "./crdt-sync-remote-source-Q6P6OiL0.js";
3
3
  import { r as getClientDatasetRejection } from "./admit-client-events-BDrJw4SK.js";
4
+ import { n as HEARTBEAT_PONG, t as HEARTBEAT_PING } from "./heartbeat-CJWQAoGP.js";
4
5
  import { z } from "zod";
5
6
  //#region src/server/admit-client-push.ts
6
7
  const timestampPattern = /^\d{15}:[0-9a-z]{5}:.+$/;
@@ -130,6 +131,6 @@ const syncServerZodSchema = {
130
131
  request: z.discriminatedUnion("type", [pullEventsZodSchema, pushEventsZodSchema])
131
132
  };
132
133
  //#endregion
133
- export { admitClientPush, jsonSafeParse, syncServerZodSchema };
134
+ export { HEARTBEAT_PING, HEARTBEAT_PONG, admitClientPush, jsonSafeParse, syncServerZodSchema };
134
135
 
135
136
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server/admit-client-push.ts","../src/server/server-common.ts"],"sourcesContent":["import { DEFAULT_MAX_DRIFT_MS } from \"../hlc\";\nimport type { MigratableEvent, SyncDbMigrator } from \"../migrations/migrator\";\nimport { type ClientDatasetRejectionReason, getClientDatasetRejection } from \"../schema/admit-client-events\";\nimport { validateNewCrdtEvent } from \"../schema/validate-crdt-event\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { CRDT_EVENT_NO_OP_PAYLOAD, isNoOpCrdtEventPayload } from \"../sqlite-crdt/crdt-table-schema\";\n\nexport type PushedCrdtEvent = MigratableEvent & { timestamp: string };\n\nexport type PushRejectionReason =\n | \"invalid-timestamp\"\n | \"timestamp-too-far-in-future\"\n | \"invalid-schema-version\"\n | \"schema-version-too-new\"\n | \"invalid-payload\"\n | \"migration-failed\"\n | ClientDatasetRejectionReason\n | \"schema-validation-failed\";\n\nexport type PushRejection<T> = {\n event: T;\n reason: PushRejectionReason;\n errors?: string[];\n};\n\nexport type AdmitClientPushResult<T> = {\n admitted: T[];\n rejected: PushRejection<T>[];\n};\n\ntype AdmitClientPushOptions<T> = {\n syncDbSchema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">;\n migrator: Pick<SyncDbMigrator, \"migrateEvent\" | \"latestSchemaVersion\">;\n events: readonly T[];\n now: number;\n};\n\ntype AdmitEventResult<T> = { ok: true; event: T } | { ok: false; reason: PushRejectionReason; errors?: string[] };\n\nconst timestampPattern = /^\\d{15}:[0-9a-z]{5}:.+$/;\n\n/**\n * Checks a client push before the hub persists it. Admitted events are migrated to the latest\n * schema version, target a client-writable base table, and carry a payload that matches the\n * schema. No-op events only need a valid timestamp and schema version. Everything else is\n * rejected with a reason.\n */\nexport function admitClientPush<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>): AdmitClientPushResult<T> {\n const admitted: T[] = [];\n const rejected: PushRejection<T>[] = [];\n for (const event of opts.events) {\n const result = admitEvent(opts, event);\n if (result.ok) {\n admitted.push(result.event);\n } else {\n rejected.push({ event, reason: result.reason, ...(result.errors && { errors: result.errors }) });\n }\n }\n return { admitted, rejected };\n}\n\nfunction admitEvent<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>, event: T): AdmitEventResult<T> {\n const { migrator } = opts;\n\n if (!timestampPattern.test(event.timestamp)) {\n return { ok: false, reason: \"invalid-timestamp\" };\n }\n if (Number(event.timestamp.slice(0, 15)) - opts.now > DEFAULT_MAX_DRIFT_MS) {\n return { ok: false, reason: \"timestamp-too-far-in-future\" };\n }\n if (!Number.isInteger(event.schema_version) || event.schema_version < 0) {\n return { ok: false, reason: \"invalid-schema-version\" };\n }\n if (event.schema_version > migrator.latestSchemaVersion) {\n return { ok: false, reason: \"schema-version-too-new\" };\n }\n if (isNoOpCrdtEventPayload(event.payload)) {\n return { ok: true, event };\n }\n const payload = parseJsonObject(event.payload);\n if (!payload) {\n return { ok: false, reason: \"invalid-payload\" };\n }\n\n let migrated: T | null;\n try {\n migrated = migrator.migrateEvent({ ...event });\n } catch (error) {\n return { ok: false, reason: \"migration-failed\", errors: [error instanceof Error ? error.message : String(error)] };\n }\n if (!migrated) {\n return {\n ok: true,\n event: { ...event, schema_version: migrator.latestSchemaVersion, payload: CRDT_EVENT_NO_OP_PAYLOAD },\n };\n }\n\n const datasetRejection = getClientDatasetRejection(opts.syncDbSchema, migrated.dataset);\n if (datasetRejection) {\n return { ok: false, reason: datasetRejection };\n }\n\n const validation = validateNewCrdtEvent(opts.syncDbSchema, {\n ...migrated,\n payload: migrated.payload === event.payload ? payload : migrated.payload,\n });\n if (!validation.success) {\n return { ok: false, reason: \"schema-validation-failed\", errors: validation.errors };\n }\n\n return { ok: true, event: migrated };\n}\n\nfunction parseJsonObject(payload: string): Record<string, unknown> | null {\n try {\n const value: unknown = JSON.parse(payload);\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n}\n","import { z } from \"zod\";\nimport { type EventsPushResponse, MAX_PUSH_EVENTS } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { EventsPullResponse } from \"../worker-db/worker-common\";\n\nconst pullEventsZodSchema = z.object({\n type: z.literal(\"pull-events\"),\n requestId: z.string(),\n afterSyncId: z.number(),\n excludeNodeId: z.string().optional(),\n});\nconst pushEventsZodSchema = z.object({\n type: z.literal(\"push-events\"),\n requestId: z.string(),\n nodeId: z.string(),\n events: z\n .array(\n z.object({\n schema_version: z.number(),\n timestamp: z.string(),\n type: z.enum([\"item-created\", \"item-updated\", \"item-deleted\"]),\n dataset: z.string(),\n item_id: z.string(),\n payload: z.string(),\n }),\n )\n .max(MAX_PUSH_EVENTS),\n});\n\nexport const syncServerZodSchema = {\n pullEvents: pullEventsZodSchema,\n pushEvents: pushEventsZodSchema,\n request: z.discriminatedUnion(\"type\", [pullEventsZodSchema, pushEventsZodSchema]),\n};\n\nexport type SyncServerMessage =\n | {\n type: \"events-pull-response\";\n requestId: string;\n data: EventsPullResponse;\n }\n | {\n type: \"events-push-response\";\n requestId: string;\n data: EventsPushResponse;\n }\n | {\n type: \"events-applied\";\n newSyncId: number;\n /**\n * Remote's HLC checksum after applying up to `newSyncId`. Used by clients\n * to detect de-sync when they are caught up.\n */\n eventHlcSum: string | null;\n };\n\nexport type SyncServerRequest = z.infer<typeof syncServerZodSchema.request>;\n\nexport type ExtractSyncServerRequest<T extends SyncServerRequest[\"type\"]> = Extract<SyncServerRequest, { type: T }>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server/admit-client-push.ts","../src/server/server-common.ts"],"sourcesContent":["import { DEFAULT_MAX_DRIFT_MS } from \"../hlc\";\nimport type { MigratableEvent, SyncDbMigrator } from \"../migrations/migrator\";\nimport { type ClientDatasetRejectionReason, getClientDatasetRejection } from \"../schema/admit-client-events\";\nimport { validateNewCrdtEvent } from \"../schema/validate-crdt-event\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { CRDT_EVENT_NO_OP_PAYLOAD, isNoOpCrdtEventPayload } from \"../sqlite-crdt/crdt-table-schema\";\n\nexport type PushedCrdtEvent = MigratableEvent & { timestamp: string };\n\nexport type PushRejectionReason =\n | \"invalid-timestamp\"\n | \"timestamp-too-far-in-future\"\n | \"invalid-schema-version\"\n | \"schema-version-too-new\"\n | \"invalid-payload\"\n | \"migration-failed\"\n | ClientDatasetRejectionReason\n | \"schema-validation-failed\";\n\nexport type PushRejection<T> = {\n event: T;\n reason: PushRejectionReason;\n errors?: string[];\n};\n\nexport type AdmitClientPushResult<T> = {\n admitted: T[];\n rejected: PushRejection<T>[];\n};\n\ntype AdmitClientPushOptions<T> = {\n syncDbSchema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">;\n migrator: Pick<SyncDbMigrator, \"migrateEvent\" | \"latestSchemaVersion\">;\n events: readonly T[];\n now: number;\n};\n\ntype AdmitEventResult<T> = { ok: true; event: T } | { ok: false; reason: PushRejectionReason; errors?: string[] };\n\nconst timestampPattern = /^\\d{15}:[0-9a-z]{5}:.+$/;\n\n/**\n * Checks a client push before the hub persists it. Admitted events are migrated to the latest\n * schema version, target a client-writable base table, and carry a payload that matches the\n * schema. No-op events only need a valid timestamp and schema version. Everything else is\n * rejected with a reason.\n */\nexport function admitClientPush<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>): AdmitClientPushResult<T> {\n const admitted: T[] = [];\n const rejected: PushRejection<T>[] = [];\n for (const event of opts.events) {\n const result = admitEvent(opts, event);\n if (result.ok) {\n admitted.push(result.event);\n } else {\n rejected.push({ event, reason: result.reason, ...(result.errors && { errors: result.errors }) });\n }\n }\n return { admitted, rejected };\n}\n\nfunction admitEvent<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>, event: T): AdmitEventResult<T> {\n const { migrator } = opts;\n\n if (!timestampPattern.test(event.timestamp)) {\n return { ok: false, reason: \"invalid-timestamp\" };\n }\n if (Number(event.timestamp.slice(0, 15)) - opts.now > DEFAULT_MAX_DRIFT_MS) {\n return { ok: false, reason: \"timestamp-too-far-in-future\" };\n }\n if (!Number.isInteger(event.schema_version) || event.schema_version < 0) {\n return { ok: false, reason: \"invalid-schema-version\" };\n }\n if (event.schema_version > migrator.latestSchemaVersion) {\n return { ok: false, reason: \"schema-version-too-new\" };\n }\n if (isNoOpCrdtEventPayload(event.payload)) {\n return { ok: true, event };\n }\n const payload = parseJsonObject(event.payload);\n if (!payload) {\n return { ok: false, reason: \"invalid-payload\" };\n }\n\n let migrated: T | null;\n try {\n migrated = migrator.migrateEvent({ ...event });\n } catch (error) {\n return { ok: false, reason: \"migration-failed\", errors: [error instanceof Error ? error.message : String(error)] };\n }\n if (!migrated) {\n return {\n ok: true,\n event: { ...event, schema_version: migrator.latestSchemaVersion, payload: CRDT_EVENT_NO_OP_PAYLOAD },\n };\n }\n\n const datasetRejection = getClientDatasetRejection(opts.syncDbSchema, migrated.dataset);\n if (datasetRejection) {\n return { ok: false, reason: datasetRejection };\n }\n\n const validation = validateNewCrdtEvent(opts.syncDbSchema, {\n ...migrated,\n payload: migrated.payload === event.payload ? payload : migrated.payload,\n });\n if (!validation.success) {\n return { ok: false, reason: \"schema-validation-failed\", errors: validation.errors };\n }\n\n return { ok: true, event: migrated };\n}\n\nfunction parseJsonObject(payload: string): Record<string, unknown> | null {\n try {\n const value: unknown = JSON.parse(payload);\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n}\n","import { z } from \"zod\";\nimport { type EventsPushResponse, MAX_PUSH_EVENTS } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { EventsPullResponse } from \"../worker-db/worker-common\";\n\nconst pullEventsZodSchema = z.object({\n type: z.literal(\"pull-events\"),\n requestId: z.string(),\n afterSyncId: z.number(),\n excludeNodeId: z.string().optional(),\n});\nconst pushEventsZodSchema = z.object({\n type: z.literal(\"push-events\"),\n requestId: z.string(),\n nodeId: z.string(),\n events: z\n .array(\n z.object({\n schema_version: z.number(),\n timestamp: z.string(),\n type: z.enum([\"item-created\", \"item-updated\", \"item-deleted\"]),\n dataset: z.string(),\n item_id: z.string(),\n payload: z.string(),\n }),\n )\n .max(MAX_PUSH_EVENTS),\n});\n\nexport const syncServerZodSchema = {\n pullEvents: pullEventsZodSchema,\n pushEvents: pushEventsZodSchema,\n request: z.discriminatedUnion(\"type\", [pullEventsZodSchema, pushEventsZodSchema]),\n};\n\nexport type SyncServerMessage =\n | {\n type: \"events-pull-response\";\n requestId: string;\n data: EventsPullResponse;\n }\n | {\n type: \"events-push-response\";\n requestId: string;\n data: EventsPushResponse;\n }\n | {\n type: \"events-applied\";\n newSyncId: number;\n /**\n * Remote's HLC checksum after applying up to `newSyncId`. Used by clients\n * to detect de-sync when they are caught up.\n */\n eventHlcSum: string | null;\n };\n\nexport type SyncServerRequest = z.infer<typeof syncServerZodSchema.request>;\n\nexport type ExtractSyncServerRequest<T extends SyncServerRequest[\"type\"]> = Extract<SyncServerRequest, { type: T }>;\n"],"mappings":"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"}
package/dist/worker.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $t as SyncDbSchema, Bt as Logger, C as WorkerConfig, _ as CreateRemoteSourceFactory, b as EventsPushResponse, r as ResetStore, rt as GetEventsBatch, v as EventsPullRequest, y as EventsPushRequest } from "./index-UfgvV97h.js";
1
+ import { $t as SyncDbSchema, Bt as Logger, C as WorkerConfig, _ as CreateRemoteSourceFactory, b as EventsPushResponse, r as ResetStore, rt as GetEventsBatch, v as EventsPullRequest, y as EventsPushRequest } from "./index-3zVvNWRd.js";
2
2
  //#region src/web-socket/ws-remote-source.d.ts
3
3
  type WsRemoteSourceConfig = {
4
4
  createWebSocket: () => Pick<WebSocket, "onmessage" | "close" | "addEventListener" | "removeEventListener"> & {
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";
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-BQi96NLn.js";
2
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";
3
+ import { a as HLCCounter, t as createCrdtSyncRemoteSource } from "./crdt-sync-remote-source-Q6P6OiL0.js";
4
4
  import { n as applyWorkerDbSchema, s as workerDbConfig } from "./system-schema-C2JZDv6L.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}`,
@@ -373,6 +350,7 @@ async function createDbWorker(config, opts) {
373
350
  },
374
351
  goOnline: () => remoteSource.goOnline(),
375
352
  goOffline: () => remoteSource.goOffline("DISCONNECTED"),
353
+ dropConnection: () => remoteSource.dropConnection("NETWORK_OFFLINE"),
376
354
  sync: () => {
377
355
  remoteSource.syncWithRemote().catch((error) => console.warn("Sync failed", error));
378
356
  },
@@ -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 { 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 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 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 */\n eventLogGc?: boolean;\n};\n\nexport async function startDbWorker(opts: WorkerOptions) {\n const config = opts.workerConfig ?? (await getWorkerConfig());\n\n await navigator.locks.request(`${syncDbWorkerLockName}-${config.dbId}`, { mode: \"exclusive\" }, async (lock) => {\n if (!lock) {\n return;\n }\n\n const cleanup = await createDbWorker(config, opts);\n\n const clientLockName = `${syncDbClientLockName}-${config.dbId}`;\n await new Promise<void>((resolve) => {\n const interval = setInterval(async () => {\n const { held } = await navigator.locks.query();\n const hasClients = held?.some((l) => l.name === clientLockName && l.mode === \"shared\");\n if (!hasClients) {\n clearInterval(interval);\n resolve();\n }\n }, 5_000);\n });\n\n await cleanup();\n });\n\n self.close();\n}\n\nfunction getMaxSyncId(db: SQLiteDbWrapper<WorkerDbSchema>, excludingStatus: \"none\" | \"pending\") {\n const [result] = db.executePrepared(\n \"get-max-sync-id\",\n { excludingStatus: excludingStatus as CrdtEventStatus },\n (db, params) =>\n db\n .selectFrom(\"worker.crdt_events\")\n .where(\"status\", \"!=\", params(\"excludingStatus\"))\n .select((eb) => eb.fn.max(\"sync_id\").as(\"sync_id\")),\n { loggerLevel: \"system\" },\n );\n\n return result?.sync_id ?? 0;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlite-sync/core",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
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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"}