@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/README.md CHANGED
@@ -5,7 +5,7 @@ Core sync engine for [sqlite-sync](https://github.com/krolebord-dev/sqlite-sync)
5
5
  This package provides:
6
6
 
7
7
  - `createSyncedDb()` for client orchestration (worker attach, snapshot hydration, sync state).
8
- - The schema builder (`createSyncDbSchema`) and migrations (`createMigrations`).
8
+ - Declarative synced tables (`defineSyncSchema`, `t.table`) and migrations (`createMigrations`).
9
9
  - Live query primitives (`db.createLiveQuery(...)`), typed through [Kysely](https://kysely.dev/).
10
10
  - CRDT primitives with Last-Write-Wins per-field replication and HLC timestamps.
11
11
  - The worker runtime (`@sqlite-sync/core/worker`) and server protocol types (`@sqlite-sync/core/server`).
@@ -21,19 +21,12 @@ For React bindings, add [`@sqlite-sync/react`](https://www.npmjs.com/package/@sq
21
21
  ## Quick start
22
22
 
23
23
  ```ts
24
- import { createMigrations, createSyncDbSchema, createSyncedDb } from "@sqlite-sync/core";
25
-
26
- type Todo = {
27
- id: string;
28
- title: string;
29
- completed: boolean;
30
- tombstone?: boolean;
31
- };
24
+ import { createMigrations, createSyncedDb, defineSyncSchema, t } from "@sqlite-sync/core";
32
25
 
33
26
  const migrations = createMigrations((b) => ({
34
27
  0: [
35
- b.createTable("_todo", (t) =>
36
- t
28
+ b.createTable("_todo", (table) =>
29
+ table
37
30
  .addColumn("id", "text", (col) => col.primaryKey().notNull())
38
31
  .addColumn("title", "text", (col) => col.notNull())
39
32
  .addColumn("completed", "boolean", (col) => col.notNull().defaultTo(false))
@@ -42,19 +35,22 @@ const migrations = createMigrations((b) => ({
42
35
  ],
43
36
  }));
44
37
 
45
- export const syncDbSchema = createSyncDbSchema({ migrations })
46
- .addTable<Todo>()
47
- .withConfig({ baseTableName: "_todo", crdtTableName: "todo" })
48
- .build();
38
+ // Queryable as "todo", stored in "_todo". `id` and `tombstone` are added automatically.
39
+ export const syncDbSchema = defineSyncSchema({
40
+ tables: {
41
+ todo: t.table({
42
+ title: t.text(),
43
+ completed: t.boolean().default(false),
44
+ }),
45
+ },
46
+ migrations,
47
+ });
48
+
49
+ export type Todo = typeof syncDbSchema.tables.todo.$row;
49
50
 
50
51
  const worker = new Worker(new URL("./db-worker.ts", import.meta.url), { type: "module" });
51
52
 
52
- export const db = await createSyncedDb({
53
- dbId: "app-db",
54
- worker,
55
- workerProps: undefined,
56
- syncDbSchema,
57
- });
53
+ export const db = await createSyncedDb({ dbId: "app-db", worker, syncDbSchema });
58
54
  ```
59
55
 
60
56
  Start the worker (remote sync optional):
@@ -1,4 +1,4 @@
1
- import { m as tryCatchAsync, o as createTypedEventTarget, s as ensureSingletonExecution } from "./crdt-table-schema-2w_AIpsV.js";
1
+ import { a as createDeferredPromise, m as tryCatchAsync, o as createTypedEventTarget, s as ensureSingletonExecution } from "./crdt-table-schema-2w_AIpsV.js";
2
2
  //#region src/hlc.ts
3
3
  const MAX_COUNTER = 36 ** 5 - 1;
4
4
  const DEFAULT_MAX_DRIFT_MS = 216e5;
@@ -174,52 +174,13 @@ function isCrdtEventType(value) {
174
174
  return typeof value === "string" && crdtEventTypes.includes(value);
175
175
  }
176
176
  //#endregion
177
- //#region src/sqlite-crdt/retry-remote-operation.ts
178
- const REMOTE_RETRY_OPTIONS = {
179
- maxAttempts: 3,
180
- backoffBaseMs: 100,
181
- backoffExponent: 1.5,
182
- backoffJitterMs: 150,
183
- timeoutMs: 1e4
184
- };
185
- var RetryTimeoutError = class extends Error {
186
- previous;
187
- constructor(message, previous) {
188
- super(message);
189
- this.previous = previous;
190
- this.name = "TimeoutError";
191
- }
192
- };
193
- const applyJitter = (delayMs, maxJitterMs) => {
194
- const jitter = Math.random() * maxJitterMs * (Math.random() > .5 ? 1 : -1);
195
- return Math.max(0, delayMs + jitter);
196
- };
197
- const delay = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
198
- const withTimeout = async (operation, timeoutMs, previousError) => {
199
- let timeoutId;
200
- try {
201
- return await Promise.race([operation(), new Promise((_, reject) => {
202
- timeoutId = setTimeout(() => reject(new RetryTimeoutError("Remote operation timed out", previousError)), timeoutMs);
203
- })]);
204
- } finally {
205
- if (timeoutId) clearTimeout(timeoutId);
206
- }
207
- };
208
- const retryRemoteOperation = async (operation, options) => {
209
- let lastError;
210
- for (let attempt = 1; attempt <= options.maxAttempts; attempt++) try {
211
- return await withTimeout(operation, options.timeoutMs, lastError);
212
- } catch (error) {
213
- lastError = error;
214
- if (attempt >= options.maxAttempts || options.shouldRetry?.(error) === false) throw error;
215
- const backoffDelay = applyJitter(options.backoffBaseMs * options.backoffExponent ** (attempt - 1), options.backoffJitterMs);
216
- if (backoffDelay > 0) await delay(backoffDelay);
217
- }
218
- throw lastError;
219
- };
220
- //#endregion
221
177
  //#region src/sqlite-crdt/crdt-sync-remote-source.ts
222
178
  const DISCONNECT_WATCHDOG_MS = 15e3;
179
+ const REQUEST_TIMEOUT_MS = 1e4;
180
+ const RECONNECT_BASE_DELAY_MS = 1e3;
181
+ const RECONNECT_MAX_DELAY_MS = 3e4;
182
+ const RECONNECT_MIN_UPTIME_MS = 5e3;
183
+ const HEARTBEAT_IDLE_MS = 25e3;
223
184
  var SchemaVersionMismatchError = class extends Error {
224
185
  remoteSchemaVersion;
225
186
  localSchemaVersion;
@@ -239,15 +200,18 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
239
200
  schemaVersionMismatched: false
240
201
  };
241
202
  const patchRemoteState = (state) => {
203
+ const previous = remoteState;
242
204
  remoteState = {
243
205
  ...remoteState,
244
206
  ...state
245
207
  };
246
- eventTarget.dispatchEvent("state-changed", remoteState.type);
208
+ if (remoteState.type !== previous.type || remoteState.deSynced !== previous.deSynced || remoteState.schemaVersionMismatched !== previous.schemaVersionMismatched) eventTarget.dispatchEvent("state-changed", remoteState.type);
247
209
  };
248
210
  let current = null;
249
211
  let disposed = false;
250
212
  let wantsOnline = false;
213
+ let reconnectTimer;
214
+ let reconnectAttempt = 0;
251
215
  const disconnectSource = (source) => {
252
216
  const watchdog = setTimeout(() => {
253
217
  console.error(`Remote source did not disconnect within ${DISCONNECT_WATCHDOG_MS}ms`);
@@ -257,64 +221,119 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
257
221
  if (!result.success) console.warn("Error while disconnecting from remote source", result.error);
258
222
  });
259
223
  };
260
- const closeConnection = (connection, reason) => {
261
- if (current !== connection) return;
224
+ const closeConnection = (connection, reason, { reconnect = false } = {}) => {
225
+ if (current !== connection) return false;
262
226
  current = null;
263
- patchRemoteState({
227
+ if (reconnect && scheduleReconnect()) patchRemoteState({ type: "connecting" });
228
+ else patchRemoteState({
264
229
  type: "offline",
265
230
  reason
266
231
  });
267
232
  connection.close();
233
+ return true;
234
+ };
235
+ const cancelReconnect = () => {
236
+ clearTimeout(reconnectTimer);
237
+ reconnectTimer = void 0;
238
+ };
239
+ const scheduleReconnect = () => {
240
+ if (!wantsOnline || current || reconnectTimer || remoteState.schemaVersionMismatched) return false;
241
+ const maxDelay = Math.min(RECONNECT_MAX_DELAY_MS, RECONNECT_BASE_DELAY_MS * 2 ** reconnectAttempt);
242
+ reconnectAttempt++;
243
+ const delay = maxDelay / 2 + Math.random() * maxDelay / 2;
244
+ reconnectTimer = setTimeout(() => {
245
+ reconnectTimer = void 0;
246
+ connect();
247
+ }, delay);
248
+ return true;
268
249
  };
269
250
  const createConnection = (factory) => {
270
251
  const controller = new AbortController();
271
252
  const { signal } = controller;
253
+ const ready = createDeferredPromise();
272
254
  let source = null;
273
- const untilClosed = (operation) => new Promise((resolve, reject) => {
255
+ let establishedAt = null;
256
+ let lastActivityAt = Date.now();
257
+ let heartbeatTimer;
258
+ const untilClosed = (operation, timeoutMs) => new Promise((resolve, reject) => {
274
259
  const onAbort = () => reject(/* @__PURE__ */ new Error("Remote connection closed"));
275
260
  if (signal.aborted) {
276
261
  onAbort();
277
262
  return;
278
263
  }
279
264
  signal.addEventListener("abort", onAbort, { once: true });
280
- operation().then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
265
+ const timeout = timeoutMs === void 0 ? void 0 : setTimeout(() => reject(/* @__PURE__ */ new Error(`Remote request timed out after ${timeoutMs}ms`)), timeoutMs);
266
+ operation().then(resolve, reject).finally(() => {
267
+ clearTimeout(timeout);
268
+ signal.removeEventListener("abort", onAbort);
269
+ });
281
270
  });
282
- const retryOptions = {
283
- ...REMOTE_RETRY_OPTIONS,
284
- shouldRetry: () => !signal.aborted
271
+ const request = async (operation) => {
272
+ const response = await untilClosed(operation, REQUEST_TIMEOUT_MS);
273
+ lastActivityAt = Date.now();
274
+ if (establishedAt === null && !signal.aborted) {
275
+ establishedAt = Date.now();
276
+ patchRemoteState({
277
+ type: "online",
278
+ schemaVersionMismatched: false
279
+ });
280
+ ready.resolve();
281
+ scheduleHeartbeat();
282
+ }
283
+ return response;
284
+ };
285
+ const fail = (reason, { reconnect = true } = {}) => {
286
+ if (current !== connection) return;
287
+ if (establishedAt !== null && Date.now() - establishedAt >= RECONNECT_MIN_UPTIME_MS) reconnectAttempt = 0;
288
+ closeConnection(connection, establishedAt === null ? "INITIALIZATION_FAILED" : reason, { reconnect });
285
289
  };
286
- const connect = async () => {
290
+ const start = () => {
287
291
  if (signal.aborted) return;
288
- const result = await tryCatchAsync(async () => await factory({
289
- onEventsAvailable: ({ newSyncId, remoteEventHlcSum }) => {
290
- pull({
291
- remoteSyncId: newSyncId,
292
- remoteEventHlcSum,
293
- includeSelf: false
294
- });
295
- },
296
- onReconnected: () => {
297
- sync();
298
- },
299
- signal
300
- }));
301
- if (signal.aborted) {
302
- if (result.success) disconnectSource(result.data);
292
+ let created;
293
+ try {
294
+ created = factory({
295
+ onEventsAvailable: ({ newSyncId, remoteEventHlcSum }) => {
296
+ lastActivityAt = Date.now();
297
+ pull({
298
+ remoteSyncId: newSyncId,
299
+ remoteEventHlcSum,
300
+ includeSelf: false
301
+ });
302
+ },
303
+ onClosed: (error) => {
304
+ if (signal.aborted) return;
305
+ if (error !== void 0) console.warn("Remote connection closed", error);
306
+ fail("CONNECTION_LOST");
307
+ }
308
+ });
309
+ } catch (error) {
310
+ console.warn("Failed to create remote source", error);
311
+ fail("INITIALIZATION_FAILED");
303
312
  return;
304
313
  }
305
- if (!result.success) {
306
- console.warn("Failed to create remote source", result.error);
307
- closeConnection(connection, "INITIALIZATION_FAILED");
314
+ if (signal.aborted) {
315
+ disconnectSource(created);
308
316
  return;
309
317
  }
310
- source = result.data;
311
- patchRemoteState({
312
- type: "online",
313
- deSynced: false,
314
- schemaVersionMismatched: false
315
- });
318
+ source = created;
316
319
  sync();
317
320
  };
321
+ const scheduleHeartbeat = () => {
322
+ const ping = source?.ping?.bind(source);
323
+ if (!ping || signal.aborted) return;
324
+ heartbeatTimer = setTimeout(async () => {
325
+ if (Date.now() - lastActivityAt >= HEARTBEAT_IDLE_MS) try {
326
+ await request(ping);
327
+ } catch (error) {
328
+ if (!signal.aborted) {
329
+ console.warn("Remote did not answer a ping", error);
330
+ fail("CONNECTION_LOST");
331
+ }
332
+ return;
333
+ }
334
+ scheduleHeartbeat();
335
+ }, Math.max(0, lastActivityAt + HEARTBEAT_IDLE_MS - Date.now()));
336
+ };
318
337
  let activePull = null;
319
338
  const pull = (request) => {
320
339
  if (!source || signal.aborted) return Promise.resolve();
@@ -338,7 +357,7 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
338
357
  }).catch((error) => {
339
358
  if (signal.aborted) return;
340
359
  console.error("Error pulling events. Going offline.", error);
341
- closeConnection(connection, "REMOTE_PULL_ERROR");
360
+ fail("REMOTE_PULL_ERROR");
342
361
  }).finally(() => {
343
362
  if (activePull === nextPull) activePull = null;
344
363
  const nextTarget = nextPull.requestedSyncId;
@@ -351,10 +370,10 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
351
370
  let hasMore = true;
352
371
  let afterSyncId = opts.afterSyncId;
353
372
  while (hasMore) {
354
- const response = await retryRemoteOperation(() => untilClosed(() => activeSource.pullEvents({
373
+ const response = await request(() => activeSource.pullEvents({
355
374
  ...opts,
356
375
  afterSyncId
357
- })), retryOptions);
376
+ }));
358
377
  if (signal.aborted) return;
359
378
  hasMore = response.hasMore;
360
379
  afterSyncId = response.nextSyncId;
@@ -365,7 +384,7 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
365
384
  remoteSchemaVersion: x.schema_version,
366
385
  localSchemaVersion: migrator.currentSchemaVersion
367
386
  });
368
- if (remoteState.type === "online" && !remoteState.schemaVersionMismatched) patchRemoteState({ schemaVersionMismatched: true });
387
+ if (!remoteState.schemaVersionMismatched) patchRemoteState({ schemaVersionMismatched: true });
369
388
  throw new SchemaVersionMismatchError(x.schema_version, migrator.currentSchemaVersion);
370
389
  }
371
390
  return x;
@@ -389,7 +408,7 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
389
408
  if (eventsBatch.events.length === 0) break;
390
409
  let response;
391
410
  try {
392
- response = await retryRemoteOperation(() => untilClosed(() => activeSource.pushEvents({
411
+ response = await request(() => activeSource.pushEvents({
393
412
  nodeId,
394
413
  events: eventsBatch.events.map((event) => ({
395
414
  schema_version: event.schema_version,
@@ -399,18 +418,18 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
399
418
  item_id: event.item_id,
400
419
  payload: event.payload
401
420
  }))
402
- })), retryOptions);
421
+ }));
403
422
  } catch (error) {
404
423
  if (!signal.aborted) {
405
424
  console.error("Error pushing events. Going offline.", error);
406
- closeConnection(connection, "REMOTE_PUSH_ERROR");
425
+ fail("REMOTE_PUSH_ERROR");
407
426
  }
408
427
  return;
409
428
  }
410
429
  if (signal.aborted) return;
411
430
  if (!response.ok) {
412
431
  console.error("Remote rejected pushed events. Going offline.");
413
- closeConnection(connection, "REMOTE_PUSH_ERROR");
432
+ fail("REMOTE_PUSH_ERROR", { reconnect: false });
414
433
  return;
415
434
  }
416
435
  pushSyncId.current = eventsBatch.nextSyncId;
@@ -426,19 +445,20 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
426
445
  };
427
446
  const close = () => {
428
447
  controller.abort();
448
+ clearTimeout(heartbeatTimer);
449
+ ready.resolve();
429
450
  if (source) disconnectSource(source);
430
451
  };
431
452
  const connection = {
432
- ready: Promise.resolve().then(() => untilClosed(connect)).catch((error) => {
433
- if (!signal.aborted) throw error;
434
- }),
453
+ ready: ready.promise,
454
+ start,
435
455
  close,
436
456
  push,
437
457
  sync
438
458
  };
439
459
  return connection;
440
460
  };
441
- const goOnline = () => {
461
+ const connect = () => {
442
462
  if (disposed) return Promise.resolve();
443
463
  if (current) return current.ready;
444
464
  if (!remoteFactory) {
@@ -449,15 +469,29 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
449
469
  });
450
470
  return Promise.resolve();
451
471
  }
472
+ cancelReconnect();
452
473
  wantsOnline = true;
453
474
  const connection = createConnection(remoteFactory);
454
475
  current = connection;
455
- patchRemoteState({ type: "pending" });
476
+ patchRemoteState({ type: "connecting" });
477
+ connection.start();
456
478
  return connection.ready;
457
479
  };
480
+ const goOnline = () => {
481
+ reconnectAttempt = 0;
482
+ return connect();
483
+ };
484
+ const dropConnection = (reason) => {
485
+ cancelReconnect();
486
+ if (current) closeConnection(current, reason);
487
+ else if (remoteState.type === "connecting") patchRemoteState({
488
+ type: "offline",
489
+ reason
490
+ });
491
+ };
458
492
  const goOffline = async (reason) => {
459
493
  wantsOnline = false;
460
- if (current) closeConnection(current, reason);
494
+ dropConnection(reason);
461
495
  };
462
496
  const syncWithRemote = async () => {
463
497
  if (current) await current.sync();
@@ -472,14 +506,14 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
472
506
  if (localEventHlcSum === remoteEventHlcSum) return;
473
507
  eventTarget.dispatchEvent("de-sync-detected", { reason: "CHECKSUM_MISMATCH" });
474
508
  console.warn(`[sqlite-sync] De-sync detected at syncId ${remoteSyncId}: local HLC checksum ${localEventHlcSum} != remote ${remoteEventHlcSum}. Local and remote have diverged despite being caught up.`);
475
- if (remoteState.type === "online" && !remoteState.deSynced) patchRemoteState({ deSynced: true });
509
+ if (!remoteState.deSynced) patchRemoteState({ deSynced: true });
476
510
  };
477
511
  const eventsAppliedSubscription = storage.addEventListener("events-applied", () => {
478
512
  current?.push();
479
513
  });
480
514
  const remoteEventApplyFailedSubscription = storage.addEventListener("remote-event-apply-failed", () => {
481
515
  eventTarget.dispatchEvent("de-sync-detected", { reason: "ERROR_APPLYING_REMOTE_EVENT" });
482
- if (remoteState.type === "online" && !remoteState.deSynced) patchRemoteState({ deSynced: true });
516
+ if (!remoteState.deSynced) patchRemoteState({ deSynced: true });
483
517
  });
484
518
  const getState = () => ({
485
519
  remoteState: remoteState.type,
@@ -495,6 +529,7 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
495
529
  return {
496
530
  goOnline,
497
531
  goOffline,
532
+ dropConnection,
498
533
  syncWithRemote,
499
534
  getState,
500
535
  dispose,
@@ -505,4 +540,4 @@ const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId,
505
540
  //#endregion
506
541
  export { HLCCounter as a, serializeHLC as c, DEFAULT_MAX_DRIFT_MS as i, CrdtEventValidationError as n, compareHLC as o, validateNewCrdtEvent as r, deserializeHLC as s, createCrdtSyncRemoteSource as t };
507
542
 
508
- //# sourceMappingURL=crdt-sync-remote-source-CugbiosG.js.map
543
+ //# sourceMappingURL=crdt-sync-remote-source-Q6P6OiL0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crdt-sync-remote-source-Q6P6OiL0.js","names":[],"sources":["../src/hlc.ts","../src/schema/validate-crdt-event.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","import type { SyncDbMigrator } from \"../migrations/migrator\";\nimport { createDeferredPromise, 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 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;\nconst REQUEST_TIMEOUT_MS = 10_000;\nconst RECONNECT_BASE_DELAY_MS = 1_000;\nconst RECONNECT_MAX_DELAY_MS = 30_000;\n// Backoff restarts only after a connection stays up this long, so a remote that accepts and then drops\n// connections right away is not hammered.\nconst RECONNECT_MIN_UPTIME_MS = 5_000;\n// Pings a remote that has sent nothing for this long, to catch connections that died without closing.\nconst HEARTBEAT_IDLE_MS = 25_000;\n\nexport type CrdtSyncRemoteSource = ReturnType<typeof createCrdtSyncRemoteSource>;\n\nexport type EventsAvailable = {\n newSyncId: number;\n remoteEventHlcSum: string | null;\n};\n\n/**\n * Opens one connection to the remote. Each call must return a new source: the library calls `disconnect` on every\n * returned source exactly once. Call `onClosed` when the connection ends on its own, at any time including during\n * this call; the library then reconnects with backoff. Requests may wait for the connection to open and should\n * reject once it closes. The library times requests out and shows the remote as online after the first one succeeds.\n * With `ping`, the library also checks idle connections and replaces one whose ping fails or times out. It calls `ping`\n * only on an online connection, one at a time, and never after the connection closes; a ping does not need to settle\n * once the source has closed.\n */\nexport type CreateRemoteSourceFactory = (opts: {\n onEventsAvailable: (event: EventsAvailable) => void;\n onClosed: (error?: unknown) => void;\n}) => RemoteSource;\n\ntype RemoteSource = {\n pullEvents: (request: EventsPullRequest) => Promise<EventsPullResponse>;\n pushEvents: (request: EventsPushRequest) => Promise<EventsPushResponse>;\n ping?: () => Promise<void>;\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: \"connecting\";\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 | \"CONNECTION_LOST\"\n | \"NETWORK_OFFLINE\"\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 const previous = remoteState;\n remoteState = { ...remoteState, ...state } as RemoteSourceState;\n if (\n remoteState.type !== previous.type ||\n remoteState.deSynced !== previous.deSynced ||\n remoteState.schemaVersionMismatched !== previous.schemaVersionMismatched\n ) {\n eventTarget.dispatchEvent(\"state-changed\", remoteState.type);\n }\n };\n\n let current: Connection | null = null;\n let disposed = false;\n let wantsOnline = false;\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined;\n let reconnectAttempt = 0;\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, { reconnect = false } = {}) => {\n if (current !== connection) {\n return false;\n }\n current = null;\n if (reconnect && scheduleReconnect()) {\n patchRemoteState({ type: \"connecting\" });\n } else {\n patchRemoteState({ type: \"offline\", reason });\n }\n connection.close();\n return true;\n };\n\n const cancelReconnect = () => {\n clearTimeout(reconnectTimer);\n reconnectTimer = undefined;\n };\n\n // Retrying after a schema mismatch would fail the same way.\n const scheduleReconnect = () => {\n if (!wantsOnline || current || reconnectTimer || remoteState.schemaVersionMismatched) {\n return false;\n }\n const maxDelay = Math.min(RECONNECT_MAX_DELAY_MS, RECONNECT_BASE_DELAY_MS * 2 ** reconnectAttempt);\n reconnectAttempt++;\n const delay = maxDelay / 2 + (Math.random() * maxDelay) / 2;\n reconnectTimer = setTimeout(() => {\n reconnectTimer = undefined;\n void connect();\n }, delay);\n return true;\n };\n\n type Connection = ReturnType<typeof createConnection>;\n const createConnection = (factory: CreateRemoteSourceFactory) => {\n const controller = new AbortController();\n const { signal } = controller;\n const ready = createDeferredPromise<void>();\n let source: RemoteSource | null = null;\n let establishedAt: number | null = null;\n let lastActivityAt = Date.now();\n let heartbeatTimer: ReturnType<typeof setTimeout> | undefined;\n\n const untilClosed = <T>(operation: () => Promise<T>, timeoutMs?: number) =>\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 const timeout =\n timeoutMs === undefined\n ? undefined\n : setTimeout(() => reject(new Error(`Remote request timed out after ${timeoutMs}ms`)), timeoutMs);\n operation()\n .then(resolve, reject)\n .finally(() => {\n clearTimeout(timeout);\n signal.removeEventListener(\"abort\", onAbort);\n });\n });\n\n const request = async <T>(operation: () => Promise<T>) => {\n const response = await untilClosed(operation, REQUEST_TIMEOUT_MS);\n lastActivityAt = Date.now();\n if (establishedAt === null && !signal.aborted) {\n establishedAt = Date.now();\n patchRemoteState({ type: \"online\", schemaVersionMismatched: false });\n ready.resolve();\n scheduleHeartbeat();\n }\n return response;\n };\n\n const fail = (reason: OfflineReason, { reconnect = true } = {}) => {\n if (current !== connection) {\n return;\n }\n if (establishedAt !== null && Date.now() - establishedAt >= RECONNECT_MIN_UPTIME_MS) {\n reconnectAttempt = 0;\n }\n closeConnection(connection, establishedAt === null ? \"INITIALIZATION_FAILED\" : reason, { reconnect });\n };\n\n // A close during the factory call aborts before `source` is set, so the abort check below disconnects it.\n const start = () => {\n if (signal.aborted) {\n return;\n }\n\n let created: RemoteSource;\n try {\n created = factory({\n onEventsAvailable: ({ newSyncId, remoteEventHlcSum }) => {\n lastActivityAt = Date.now();\n pull({ remoteSyncId: newSyncId, remoteEventHlcSum, includeSelf: false });\n },\n onClosed: (error) => {\n if (signal.aborted) {\n return;\n }\n if (error !== undefined) {\n console.warn(\"Remote connection closed\", error);\n }\n fail(\"CONNECTION_LOST\");\n },\n });\n } catch (error) {\n console.warn(\"Failed to create remote source\", error);\n fail(\"INITIALIZATION_FAILED\");\n return;\n }\n\n if (signal.aborted) {\n disconnectSource(created);\n return;\n }\n source = created;\n void sync();\n };\n\n const scheduleHeartbeat = () => {\n const ping = source?.ping?.bind(source);\n if (!ping || signal.aborted) {\n return;\n }\n heartbeatTimer = setTimeout(\n async () => {\n if (Date.now() - lastActivityAt >= HEARTBEAT_IDLE_MS) {\n try {\n await request(ping);\n } catch (error) {\n if (!signal.aborted) {\n console.warn(\"Remote did not answer a ping\", error);\n fail(\"CONNECTION_LOST\");\n }\n return;\n }\n }\n scheduleHeartbeat();\n },\n Math.max(0, lastActivityAt + HEARTBEAT_IDLE_MS - Date.now()),\n );\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 fail(\"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 request(() =>\n activeSource.pullEvents({\n ...opts,\n afterSyncId,\n }),\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.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 request(() =>\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 } catch (error) {\n if (!signal.aborted) {\n console.error(\"Error pushing events. Going offline.\", error);\n fail(\"REMOTE_PUSH_ERROR\");\n }\n return;\n }\n\n if (signal.aborted) {\n return;\n }\n\n if (!response.ok) {\n // The remote would reject the same batch again, so only an explicit sync retries it.\n console.error(\"Remote rejected pushed events. Going offline.\");\n fail(\"REMOTE_PUSH_ERROR\", { reconnect: false });\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 clearTimeout(heartbeatTimer);\n ready.resolve();\n if (source) {\n disconnectSource(source);\n }\n };\n\n const connection = { ready: ready.promise, start, close, push, sync };\n return connection;\n };\n\n const connect = (): 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 cancelReconnect();\n wantsOnline = true;\n const connection = createConnection(remoteFactory);\n current = connection;\n patchRemoteState({ type: \"connecting\" });\n connection.start();\n return connection.ready;\n };\n\n const goOnline = () => {\n reconnectAttempt = 0;\n return connect();\n };\n\n // Unlike goOffline, leaves syncWithRemote free to reconnect, but does not retry on its own: the caller\n // knows the network is gone and calls syncWithRemote once it is back.\n const dropConnection = (reason: OfflineReason) => {\n cancelReconnect();\n if (current) {\n closeConnection(current, reason);\n } else if (remoteState.type === \"connecting\") {\n patchRemoteState({ type: \"offline\", reason });\n }\n };\n\n const goOffline = async (reason: OfflineReason) => {\n wantsOnline = false;\n dropConnection(reason);\n };\n\n // Reconnects right away, skipping any backoff, unless goOffline or a schema mismatch took us offline.\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.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.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 dropConnection,\n syncWithRemote,\n getState,\n dispose,\n addEventListener: eventTarget.addEventListener,\n removeEventListener: eventTarget.removeEventListener,\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ //#region src/server/heartbeat.ts
2
+ const HEARTBEAT_PING = "ping";
3
+ const HEARTBEAT_PONG = "pong";
4
+ //#endregion
5
+ export { HEARTBEAT_PONG as n, HEARTBEAT_PING as t };
6
+
7
+ //# sourceMappingURL=heartbeat-CJWQAoGP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat-CJWQAoGP.js","names":[],"sources":["../src/server/heartbeat.ts"],"sourcesContent":["// Plain text, not JSON: Cloudflare's `setWebSocketAutoResponse` matches the exact message, so a hibernating Durable\n// Object can answer pings without waking up.\nexport const HEARTBEAT_PING = \"ping\";\nexport const HEARTBEAT_PONG = \"pong\";\n"],"mappings":";AAEA,MAAa,iBAAiB;AAC9B,MAAa,iBAAiB"}
@@ -949,7 +949,7 @@ type WorkerNotificationMessage = {
949
949
  localSchemaVersion: number;
950
950
  };
951
951
  type WorkerState = {
952
- remoteState: "online" | "offline" | "pending";
952
+ remoteState: "online" | "offline" | "connecting";
953
953
  deSynced: boolean;
954
954
  schemaVersionMismatched: boolean;
955
955
  };
@@ -1004,24 +1004,26 @@ type EventsAvailable = {
1004
1004
  remoteEventHlcSum: string | null;
1005
1005
  };
1006
1006
  /**
1007
- * Opens a connection to the remote. Each call must return an independent source: the library calls
1008
- * `disconnect` on every returned source exactly once, including one that resolves after `signal`
1009
- * has aborted. `signal` aborts when the connection attempt is cancelled or the connection is closed.
1010
- * Call `onReconnected` when the transport re-establishes itself without a new source: broadcasts
1011
- * sent while it was down are lost, so the library pulls and pushes to catch up.
1007
+ * Opens one connection to the remote. Each call must return a new source: the library calls `disconnect` on every
1008
+ * returned source exactly once. Call `onClosed` when the connection ends on its own, at any time including during
1009
+ * this call; the library then reconnects with backoff. Requests may wait for the connection to open and should
1010
+ * reject once it closes. The library times requests out and shows the remote as online after the first one succeeds.
1011
+ * With `ping`, the library also checks idle connections and replaces one whose ping fails or times out. It calls `ping`
1012
+ * only on an online connection, one at a time, and never after the connection closes; a ping does not need to settle
1013
+ * once the source has closed.
1012
1014
  */
1013
1015
  type CreateRemoteSourceFactory = (opts: {
1014
1016
  onEventsAvailable: (event: EventsAvailable) => void;
1015
- onReconnected: () => void;
1016
- signal: AbortSignal;
1017
- }) => RemoteSource | Promise<RemoteSource>;
1017
+ onClosed: (error?: unknown) => void;
1018
+ }) => RemoteSource;
1018
1019
  type RemoteSource = {
1019
1020
  pullEvents: (request: EventsPullRequest) => Promise<EventsPullResponse>;
1020
1021
  pushEvents: (request: EventsPushRequest) => Promise<EventsPushResponse>;
1022
+ ping?: () => Promise<void>;
1021
1023
  disconnect?: () => void | Promise<void>;
1022
1024
  };
1023
1025
  type RemoteSourceState = {
1024
- type: "pending";
1026
+ type: "connecting";
1025
1027
  deSynced: boolean;
1026
1028
  schemaVersionMismatched: boolean;
1027
1029
  } | {
@@ -1034,11 +1036,12 @@ type RemoteSourceState = {
1034
1036
  deSynced: boolean;
1035
1037
  schemaVersionMismatched: boolean;
1036
1038
  };
1037
- type OfflineReason = "NOT_INITIALIZED" | "INITIALIZATION_FAILED" | "REMOTE_PUSH_ERROR" | "REMOTE_PULL_ERROR" | "DISCONNECTED";
1039
+ type OfflineReason = "NOT_INITIALIZED" | "INITIALIZATION_FAILED" | "REMOTE_PUSH_ERROR" | "REMOTE_PULL_ERROR" | "CONNECTION_LOST" | "NETWORK_OFFLINE" | "DISCONNECTED";
1038
1040
  type DeSyncDetectedReason = "CHECKSUM_MISMATCH" | "ERROR_APPLYING_REMOTE_EVENT";
1039
1041
  declare const createCrdtSyncRemoteSource: ({ bufferSize, storage, migrator, pullSyncId, pushSyncId, nodeId, remoteFactory }: CrdtSyncRemoteSourceConfig) => {
1040
1042
  goOnline: () => Promise<void>;
1041
1043
  goOffline: (reason: OfflineReason) => Promise<void>;
1044
+ dropConnection: (reason: OfflineReason) => void;
1042
1045
  syncWithRemote: () => Promise<void>;
1043
1046
  getState: () => WorkerState;
1044
1047
  dispose: () => Promise<void>;
@@ -1092,9 +1095,12 @@ declare function drainCrdtChangeIntents({ tx, storage }: {
1092
1095
  type SyncedDbOptions<Database, Props = undefined> = {
1093
1096
  dbId: string;
1094
1097
  worker: Worker;
1095
- workerProps: Props;
1096
1098
  syncDbSchema: SyncDbSchema<Database>;
1097
- };
1099
+ } & (undefined extends Props ? {
1100
+ workerProps?: Props;
1101
+ } : {
1102
+ workerProps: Props;
1103
+ });
1098
1104
  /** SQL execution that participates in CRDT intent draining. */
1099
1105
  type SyncedDbSqlExecutor<Database> = {
1100
1106
  execute<TResult = unknown>(query: ExecuteParams | string | CompiledQuery<TResult>): ExecuteResult<TResult>;
@@ -1219,4 +1225,4 @@ type ResetStore = {
1219
1225
  declare const RESET_REQUEST_TTL_MS: number;
1220
1226
  //#endregion
1221
1227
  export { createExportData as $, SyncDbSchema as $t, PendingCrdtEvent as A, PersistedCrdtEvent as An, ExecuteParams as At, DefinedSyncSchema as B, Logger as Bt, WorkerConfig as C, createStoredValue as Cn, jsonSafeParse as Ct, CrdtStorageMutator as D, CrdtEventType as Dn, KvStoreItem as Dt, createCrdtSyncProducer as E, CrdtEventStatus as En, tryCatchAsync as Et, verifySyncSchema as F, PreparedStatement as Ft, SQLiteReactiveDb as G, introspectDb as Gt, AdmitClientEventsResult as H, ColumnMetadata as Ht, CrdtEventValidationError as I, QueryBuilderOutput as It, parametersAreEqual as J, compareHLC as Jt, SharedLiveQuery as K, HLC as Kt, NewCrdtEventValidationResult as L, SQLiteDbWrapper as Lt, createSQLiteCrdtApplyFunction as M, dummyKysely as Mn, InternalSQLiteWrapper as Mt, SchemaVerificationIssue as N, KyselyQueryFactory as Nt, SnapshotOptions as O, CrdtUpdateLogItem as On, createKvStoreTableQuery as Ot, formatSchemaVerificationIssues as P, KyselyStatementFactory as Pt, SyncedDbExport as Q, ReadonlyTable as Qt, validateNewCrdtEvent as R, SQLiteTransactionWrapper as Rt, EventsPullResponse as S, StoredValue as Sn, generateId as St, WorkerState as T, CrdtEventOrigin as Tn, tryCatch as Tt, ClientDatasetRejectionReason as U, DatabaseIntrospection as Ut, defineSyncSchema as V, startPerformanceLogger as Vt, admitClientEvents as W, TableMetadata as Wt, ImportDataOptions as X, serializeHLC as Xt, xxhash as Y, deserializeHLC as Yt, ImportDataResult as Z, CrdtTableConfig as Zt, CreateRemoteSourceFactory as _, Migrations as _n, TypedBroadcastChannel as _t, SyncedDbDatabase as a, ColumnMeta as an, OwnCrdtSnapshot as at, EventsPushResponse as b, createMigrations as bn, createDeferredPromise as bt, SyncedDbTransaction as c, PayloadValidationResult as cn, SystemMigration as ct, CRDT_CHANGE_INTENTS_TABLE as d, TableBuilder as dn, applyWorkerDbSchema as dt, AiAccess as en, createImportData as et, crdtViewColumnsFromTable as f, TableColumns as fn, baseSystemMigrations as ft, CrdtSyncRemoteSource as g, MigratableEvent as gn, DistributiveOmit as gt, makeCrdtTable as h, t as hn, DeferredPromise as ht, SyncedDb as i, ColumnKind as in, OwnCrdtEvent as it, createCrdtApplyFunction as j, isNoOpCrdtEventPayload as jn, ExecuteResult as jt, createCrdtStorageMutator as k, CrdtUpdateLogPayload as kn, createSQLiteKvStore as kt, UnsafeSyncedDbSqlExecutor as l, SqliteStorageType as ln, SystemMigrationContext as lt, drainCrdtChangeIntents as m, WriteOrigin as mn, runSystemMigrations as mt, ResetRequest as n, AnyTableBuilder as nn, CrdtStorage as nt, SyncedDbOptions as o, ExportImportMode as on, createCrdtStorage as ot, createCrdtViewStatements as p, TableOptions as pn, createSystemDbConfig as pt, SharedLiveQuerySnapshot as q, HLCCounter as qt, ResetStore as r, ColumnBuilder as rn, GetEventsBatch as rt, SyncedDbSqlExecutor as s, InferRow as sn, SystemDbConfig as st, RESET_REQUEST_TTL_MS as t, AnyColumnBuilder as tn, CrdtChangeIntent as tt, createSyncedDb as u, SyncSchemaTables as un, applyMemoryDbSchema as ut, EventsPullRequest as v, MigrationsDb as vn, TypedEvent as vt, WorkerNotificationMessage as w, CRDT_EVENT_NO_OP_PAYLOAD as wn, quoteId as wt, createCrdtSyncRemoteSource as x, createMigrator as xn, createTypedEventTarget as xt, EventsPushRequest as y, SyncDbMigrator as yn, TypedEventTarget as yt, DefineSyncSchemaConfig as z, LogLevel as zt };
1222
- //# sourceMappingURL=index-UfgvV97h.d.ts.map
1228
+ //# sourceMappingURL=index-3zVvNWRd.d.ts.map