@syncular/client 0.0.1 → 0.0.2-126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +23 -0
  2. package/dist/blobs/index.js +3 -3
  3. package/dist/client.d.ts +10 -5
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +70 -21
  6. package/dist/client.js.map +1 -1
  7. package/dist/conflicts.d.ts.map +1 -1
  8. package/dist/conflicts.js +1 -7
  9. package/dist/conflicts.js.map +1 -1
  10. package/dist/create-client.d.ts +5 -1
  11. package/dist/create-client.d.ts.map +1 -1
  12. package/dist/create-client.js +22 -10
  13. package/dist/create-client.js.map +1 -1
  14. package/dist/engine/SyncEngine.d.ts +24 -2
  15. package/dist/engine/SyncEngine.d.ts.map +1 -1
  16. package/dist/engine/SyncEngine.js +290 -43
  17. package/dist/engine/SyncEngine.js.map +1 -1
  18. package/dist/engine/index.js +2 -2
  19. package/dist/engine/types.d.ts +16 -4
  20. package/dist/engine/types.d.ts.map +1 -1
  21. package/dist/handlers/create-handler.d.ts +15 -5
  22. package/dist/handlers/create-handler.d.ts.map +1 -1
  23. package/dist/handlers/create-handler.js +35 -24
  24. package/dist/handlers/create-handler.js.map +1 -1
  25. package/dist/handlers/types.d.ts +5 -5
  26. package/dist/handlers/types.d.ts.map +1 -1
  27. package/dist/index.js +19 -19
  28. package/dist/migrate.d.ts +1 -1
  29. package/dist/migrate.d.ts.map +1 -1
  30. package/dist/migrate.js +148 -28
  31. package/dist/migrate.js.map +1 -1
  32. package/dist/mutations.d.ts +3 -1
  33. package/dist/mutations.d.ts.map +1 -1
  34. package/dist/mutations.js +93 -18
  35. package/dist/mutations.js.map +1 -1
  36. package/dist/outbox.d.ts.map +1 -1
  37. package/dist/outbox.js +1 -11
  38. package/dist/outbox.js.map +1 -1
  39. package/dist/plugins/incrementing-version.d.ts +1 -1
  40. package/dist/plugins/incrementing-version.js +2 -2
  41. package/dist/plugins/index.js +2 -2
  42. package/dist/proxy/dialect.js +1 -1
  43. package/dist/proxy/driver.js +1 -1
  44. package/dist/proxy/index.js +4 -4
  45. package/dist/proxy/mutations.js +1 -1
  46. package/dist/pull-engine.d.ts +29 -3
  47. package/dist/pull-engine.d.ts.map +1 -1
  48. package/dist/pull-engine.js +314 -78
  49. package/dist/pull-engine.js.map +1 -1
  50. package/dist/push-engine.d.ts.map +1 -1
  51. package/dist/push-engine.js +28 -3
  52. package/dist/push-engine.js.map +1 -1
  53. package/dist/query/QueryContext.js +1 -1
  54. package/dist/query/index.js +3 -3
  55. package/dist/query/tracked-select.d.ts +2 -1
  56. package/dist/query/tracked-select.d.ts.map +1 -1
  57. package/dist/query/tracked-select.js +1 -1
  58. package/dist/schema.d.ts +2 -2
  59. package/dist/schema.d.ts.map +1 -1
  60. package/dist/sync-loop.d.ts +5 -1
  61. package/dist/sync-loop.d.ts.map +1 -1
  62. package/dist/sync-loop.js +167 -18
  63. package/dist/sync-loop.js.map +1 -1
  64. package/package.json +30 -6
  65. package/src/client.test.ts +369 -0
  66. package/src/client.ts +101 -22
  67. package/src/conflicts.ts +1 -10
  68. package/src/create-client.ts +33 -5
  69. package/src/engine/SyncEngine.test.ts +157 -0
  70. package/src/engine/SyncEngine.ts +359 -40
  71. package/src/engine/types.ts +22 -4
  72. package/src/handlers/create-handler.ts +86 -37
  73. package/src/handlers/types.ts +10 -4
  74. package/src/migrate.ts +215 -33
  75. package/src/mutations.ts +143 -21
  76. package/src/outbox.ts +1 -15
  77. package/src/plugins/incrementing-version.ts +2 -2
  78. package/src/pull-engine.test.ts +147 -0
  79. package/src/pull-engine.ts +392 -77
  80. package/src/push-engine.ts +33 -1
  81. package/src/query/tracked-select.ts +1 -1
  82. package/src/schema.ts +2 -2
  83. package/src/sync-loop.ts +215 -19
@@ -1 +1 @@
1
- {"version":3,"file":"create-client.js","sourceRoot":"","sources":["../src/create-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"create-client.js","sourceRoot":"","sources":["../src/create-client.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@
4
4
  * Event-driven sync engine that manages push/pull cycles, connection state,
5
5
  * and provides a clean API for framework bindings to consume.
6
6
  */
7
- import type { SyncSubscriptionRequest } from '@syncular/core';
7
+ import { type SyncSubscriptionRequest } from '@syncular/core';
8
8
  import { type Kysely } from 'kysely';
9
9
  import type { SyncClientDb } from '../schema';
10
10
  import type { ConflictInfo, OutboxStats, PresenceEntry, SyncEngineConfig, SyncEngineState, SyncEventListener, SyncEventType, SyncResult } from './types';
@@ -21,12 +21,20 @@ export declare class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
21
21
  private syncPromise;
22
22
  private syncRequestedWhileRunning;
23
23
  private retryTimeoutId;
24
+ private realtimeCatchupTimeoutId;
25
+ private hasRealtimeConnectedOnce;
24
26
  /**
25
27
  * In-memory map tracking local mutation timestamps by rowId.
26
28
  * Used for efficient fingerprint-based rerender optimization.
27
29
  * Key format: `${table}:${rowId}`, Value: timestamp (Date.now())
28
30
  */
29
31
  private mutationTimestamps;
32
+ /**
33
+ * In-memory map tracking table-level mutation timestamps.
34
+ * Used for coarse invalidation during large bootstrap snapshots to avoid
35
+ * storing timestamps for every row.
36
+ */
37
+ private tableMutationTimestamps;
30
38
  /**
31
39
  * In-memory presence state by scope key.
32
40
  * Updated via realtime presence events.
@@ -118,12 +126,18 @@ export declare class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
118
126
  /**
119
127
  * Trigger a manual sync
120
128
  */
121
- sync(): Promise<SyncResult>;
129
+ sync(opts?: {
130
+ trigger?: 'ws' | 'local' | 'poll';
131
+ }): Promise<SyncResult>;
122
132
  private performSyncLoop;
123
133
  private performSyncOnce;
124
134
  private extractChangedTables;
135
+ private applyWsDeliveredChanges;
136
+ private handleWsDelivery;
125
137
  private timestampCounter;
138
+ private nextPreciseTimestamp;
126
139
  private bumpMutationTimestamp;
140
+ private bumpTableMutationTimestamp;
127
141
  /**
128
142
  * Record local mutations that were already applied to the DB.
129
143
  *
@@ -142,12 +156,20 @@ export declare class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
142
156
  private recordMutationTimestampsFromPullResponse;
143
157
  private scheduleRetry;
144
158
  private handleError;
159
+ private triggerSyncInBackground;
145
160
  private setupPolling;
146
161
  private stopPolling;
147
162
  private setupRealtime;
148
163
  private stopRealtime;
164
+ private scheduleRealtimeReconnectCatchupSync;
149
165
  private startFallbackPolling;
150
166
  private stopFallbackPolling;
167
+ /**
168
+ * Clear all in-memory mutation state and emit data:change so UI re-renders.
169
+ * Call this after deleting local data (e.g. reset flow) so that React hooks
170
+ * recompute fingerprints from scratch instead of seeing stale timestamps.
171
+ */
172
+ resetLocalState(): void;
151
173
  /**
152
174
  * Reconnect
153
175
  */
@@ -1 +1 @@
1
- {"version":3,"file":"SyncEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SyncEngine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SyncEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SyncEngine.ts"],"names":[],"mappings":"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"}
@@ -4,14 +4,16 @@
4
4
  * Event-driven sync engine that manages push/pull cycles, connection state,
5
5
  * and provides a clean API for framework bindings to consume.
6
6
  */
7
+ import { captureSyncException, countSyncMetric, distributionSyncMetric, isRecord, startSyncSpan, } from '@syncular/core';
7
8
  import { sql } from 'kysely';
8
- import { syncPushOnce } from '../push-engine';
9
- import { syncOnce } from '../sync-loop';
9
+ import { syncPushOnce } from '../push-engine.js';
10
+ import { syncOnce } from '../sync-loop.js';
10
11
  const DEFAULT_POLL_INTERVAL_MS = 10_000;
11
12
  const DEFAULT_MAX_RETRIES = 5;
12
13
  const INITIAL_RETRY_DELAY_MS = 1000;
13
14
  const MAX_RETRY_DELAY_MS = 60000;
14
15
  const EXPONENTIAL_FACTOR = 2;
16
+ const REALTIME_RECONNECT_CATCHUP_DELAY_MS = 500;
15
17
  function calculateRetryDelay(attemptIndex) {
16
18
  return Math.min(INITIAL_RETRY_DELAY_MS * EXPONENTIAL_FACTOR ** attemptIndex, MAX_RETRY_DELAY_MS);
17
19
  }
@@ -28,8 +30,8 @@ function createSyncError(code, message, cause) {
28
30
  timestamp: Date.now(),
29
31
  };
30
32
  }
31
- function isRecord(value) {
32
- return typeof value === 'object' && value !== null && !Array.isArray(value);
33
+ function resolveSyncTriggerLabel(trigger) {
34
+ return trigger ?? 'auto';
33
35
  }
34
36
  export class SyncEngine {
35
37
  config;
@@ -44,12 +46,20 @@ export class SyncEngine {
44
46
  syncPromise = null;
45
47
  syncRequestedWhileRunning = false;
46
48
  retryTimeoutId = null;
49
+ realtimeCatchupTimeoutId = null;
50
+ hasRealtimeConnectedOnce = false;
47
51
  /**
48
52
  * In-memory map tracking local mutation timestamps by rowId.
49
53
  * Used for efficient fingerprint-based rerender optimization.
50
54
  * Key format: `${table}:${rowId}`, Value: timestamp (Date.now())
51
55
  */
52
56
  mutationTimestamps = new Map();
57
+ /**
58
+ * In-memory map tracking table-level mutation timestamps.
59
+ * Used for coarse invalidation during large bootstrap snapshots to avoid
60
+ * storing timestamps for every row.
61
+ */
62
+ tableMutationTimestamps = new Map();
53
63
  /**
54
64
  * In-memory presence state by scope key.
55
65
  * Updated via realtime presence events.
@@ -65,7 +75,9 @@ export class SyncEngine {
65
75
  * Returns 0 if row has no recorded mutation timestamp.
66
76
  */
67
77
  getMutationTimestamp(table, rowId) {
68
- return this.mutationTimestamps.get(`${table}:${rowId}`) ?? 0;
78
+ const rowTs = this.mutationTimestamps.get(`${table}:${rowId}`) ?? 0;
79
+ const tableTs = this.tableMutationTimestamps.get(table) ?? 0;
80
+ return Math.max(rowTs, tableTs);
69
81
  }
70
82
  /**
71
83
  * Get presence entries for a scope key.
@@ -187,7 +199,7 @@ export class SyncEngine {
187
199
  clientId.length > 0);
188
200
  }
189
201
  detectTransportMode() {
190
- if (this.config.realtimeEnabled &&
202
+ if (this.config.realtimeEnabled !== false &&
191
203
  isRealtimeTransport(this.config.transport)) {
192
204
  return 'realtime';
193
205
  }
@@ -345,11 +357,15 @@ export class SyncEngine {
345
357
  clearTimeout(this.retryTimeoutId);
346
358
  this.retryTimeoutId = null;
347
359
  }
360
+ if (this.realtimeCatchupTimeoutId) {
361
+ clearTimeout(this.realtimeCatchupTimeoutId);
362
+ this.realtimeCatchupTimeoutId = null;
363
+ }
348
364
  }
349
365
  /**
350
366
  * Trigger a manual sync
351
367
  */
352
- async sync() {
368
+ async sync(opts) {
353
369
  // Dedupe concurrent sync calls
354
370
  if (this.syncPromise) {
355
371
  // A sync is already in-flight; queue one more run so we don't miss
@@ -368,7 +384,7 @@ export class SyncEngine {
368
384
  error: createSyncError('SYNC_ERROR', 'Sync not enabled'),
369
385
  };
370
386
  }
371
- this.syncPromise = this.performSyncLoop();
387
+ this.syncPromise = this.performSyncLoop(opts?.trigger);
372
388
  try {
373
389
  return await this.syncPromise;
374
390
  }
@@ -376,7 +392,7 @@ export class SyncEngine {
376
392
  this.syncPromise = null;
377
393
  }
378
394
  }
379
- async performSyncLoop() {
395
+ async performSyncLoop(trigger) {
380
396
  let lastResult = {
381
397
  success: false,
382
398
  pushedCommits: 0,
@@ -386,7 +402,9 @@ export class SyncEngine {
386
402
  };
387
403
  do {
388
404
  this.syncRequestedWhileRunning = false;
389
- lastResult = await this.performSyncOnce();
405
+ lastResult = await this.performSyncOnce(trigger);
406
+ // After the first iteration, clear trigger context
407
+ trigger = undefined;
390
408
  // If the sync failed, let retry logic handle backoff instead of tight looping.
391
409
  if (!lastResult.success)
392
410
  break;
@@ -395,22 +413,34 @@ export class SyncEngine {
395
413
  this.isEnabled());
396
414
  return lastResult;
397
415
  }
398
- async performSyncOnce() {
416
+ async performSyncOnce(trigger) {
399
417
  const timestamp = Date.now();
418
+ const startedAtMs = timestamp;
419
+ const triggerLabel = resolveSyncTriggerLabel(trigger);
400
420
  this.updateState({ isSyncing: true });
401
421
  this.emit('sync:start', { timestamp });
422
+ countSyncMetric('sync.client.sync.attempts', 1, {
423
+ attributes: { trigger: triggerLabel },
424
+ });
402
425
  try {
403
426
  const pullApplyTimestamp = Date.now();
404
- const result = await syncOnce(this.config.db, this.config.transport, this.config.shapes, {
427
+ const result = await startSyncSpan({
428
+ name: 'sync.client.sync',
429
+ op: 'sync.client.sync',
430
+ attributes: { trigger: triggerLabel },
431
+ }, () => syncOnce(this.config.db, this.config.transport, this.config.handlers, {
405
432
  clientId: this.config.clientId,
406
433
  actorId: this.config.actorId ?? undefined,
407
434
  plugins: this.config.plugins,
408
- subscriptions: this.config.subscriptions,
435
+ subscriptions: this.config
436
+ .subscriptions,
409
437
  limitCommits: this.config.limitCommits,
410
438
  limitSnapshotRows: this.config.limitSnapshotRows,
411
439
  maxSnapshotPages: this.config.maxSnapshotPages,
412
440
  stateId: this.config.stateId,
413
- });
441
+ sha256: this.config.sha256,
442
+ trigger,
443
+ }));
414
444
  const syncResult = {
415
445
  success: true,
416
446
  pushedCommits: result.pushedCommits,
@@ -442,8 +472,30 @@ export class SyncEngine {
442
472
  });
443
473
  this.config.onDataChange?.(changedTables);
444
474
  }
445
- // Refresh outbox stats
446
- await this.refreshOutboxStats();
475
+ // Refresh outbox stats (fire-and-forget — don't block sync:complete)
476
+ this.refreshOutboxStats().catch((error) => {
477
+ console.warn('[SyncEngine] Failed to refresh outbox stats after sync:', error);
478
+ });
479
+ const durationMs = Math.max(0, Date.now() - startedAtMs);
480
+ countSyncMetric('sync.client.sync.results', 1, {
481
+ attributes: {
482
+ trigger: triggerLabel,
483
+ status: 'success',
484
+ },
485
+ });
486
+ distributionSyncMetric('sync.client.sync.duration_ms', durationMs, {
487
+ unit: 'millisecond',
488
+ attributes: {
489
+ trigger: triggerLabel,
490
+ status: 'success',
491
+ },
492
+ });
493
+ distributionSyncMetric('sync.client.sync.pushed_commits', result.pushedCommits, {
494
+ attributes: { trigger: triggerLabel },
495
+ });
496
+ distributionSyncMetric('sync.client.sync.pull_rounds', result.pullRounds, {
497
+ attributes: { trigger: triggerLabel },
498
+ });
447
499
  return syncResult;
448
500
  }
449
501
  catch (err) {
@@ -455,6 +507,24 @@ export class SyncEngine {
455
507
  isRetrying: false,
456
508
  });
457
509
  this.handleError(error);
510
+ const durationMs = Math.max(0, Date.now() - startedAtMs);
511
+ countSyncMetric('sync.client.sync.results', 1, {
512
+ attributes: {
513
+ trigger: triggerLabel,
514
+ status: 'error',
515
+ },
516
+ });
517
+ distributionSyncMetric('sync.client.sync.duration_ms', durationMs, {
518
+ unit: 'millisecond',
519
+ attributes: {
520
+ trigger: triggerLabel,
521
+ status: 'error',
522
+ },
523
+ });
524
+ captureSyncException(err, {
525
+ event: 'sync.client.sync',
526
+ trigger: triggerLabel,
527
+ });
458
528
  // Schedule retry if under max retries
459
529
  const maxRetries = this.config.maxRetries ?? DEFAULT_MAX_RETRIES;
460
530
  if (this.state.retryCount < maxRetries) {
@@ -489,16 +559,141 @@ export class SyncEngine {
489
559
  }
490
560
  return Array.from(tables);
491
561
  }
562
+ /**
563
+ * Apply changes delivered inline over WebSocket for instant UI updates.
564
+ * Returns true if changes were applied and cursor updated successfully,
565
+ * false if anything failed (caller should fall back to HTTP sync).
566
+ */
567
+ async applyWsDeliveredChanges(changes, cursor) {
568
+ try {
569
+ await this.config.db.transaction().execute(async (trx) => {
570
+ for (const change of changes) {
571
+ const handler = this.config.handlers.get(change.table);
572
+ if (!handler) {
573
+ throw new Error(`Missing client table handler for WS change table "${change.table}"`);
574
+ }
575
+ await handler.applyChange({ trx }, change);
576
+ }
577
+ // Update subscription cursors
578
+ const stateId = this.config.stateId ?? 'default';
579
+ await sql `
580
+ update ${sql.table('sync_subscription_state')}
581
+ set ${sql.ref('cursor')} = ${sql.val(cursor)}
582
+ where ${sql.ref('state_id')} = ${sql.val(stateId)}
583
+ and ${sql.ref('cursor')} < ${sql.val(cursor)}
584
+ `.execute(trx);
585
+ });
586
+ // Update mutation timestamps BEFORE emitting data:change so that
587
+ // React hooks re-querying the DB see fresh fingerprints immediately.
588
+ const now = Date.now();
589
+ for (const change of changes) {
590
+ if (!change.table || !change.row_id)
591
+ continue;
592
+ if (change.op === 'delete') {
593
+ this.mutationTimestamps.delete(`${change.table}:${change.row_id}`);
594
+ }
595
+ else {
596
+ this.bumpMutationTimestamp(change.table, change.row_id, now);
597
+ }
598
+ }
599
+ // Emit data change for immediate UI update
600
+ const changedTables = [...new Set(changes.map((c) => c.table))];
601
+ if (changedTables.length > 0) {
602
+ this.emit('data:change', {
603
+ scopes: changedTables,
604
+ timestamp: Date.now(),
605
+ });
606
+ this.config.onDataChange?.(changedTables);
607
+ }
608
+ return true;
609
+ }
610
+ catch {
611
+ return false;
612
+ }
613
+ }
614
+ /**
615
+ * Handle WS-delivered changes: apply them and decide whether to skip HTTP pull.
616
+ * Falls back to full HTTP sync when conditions require it.
617
+ */
618
+ async handleWsDelivery(changes, cursor) {
619
+ // If a sync is already in-flight, let it handle everything
620
+ if (this.syncPromise) {
621
+ countSyncMetric('sync.client.ws.delivery.events', 1, {
622
+ attributes: { path: 'inflight_sync' },
623
+ });
624
+ this.triggerSyncInBackground({ trigger: 'ws' }, 'ws delivery with in-flight sync');
625
+ return;
626
+ }
627
+ // If there are pending outbox commits, need to push via HTTP
628
+ if (this.state.pendingCount > 0) {
629
+ countSyncMetric('sync.client.ws.delivery.events', 1, {
630
+ attributes: { path: 'pending_outbox' },
631
+ });
632
+ this.triggerSyncInBackground({ trigger: 'ws' }, 'ws delivery with pending outbox');
633
+ return;
634
+ }
635
+ // If afterPull plugins exist, inline WS changes may require transforms
636
+ // (e.g. decryption). Fall back to HTTP sync and do not apply inline payload.
637
+ const hasAfterPullPlugins = this.config.plugins?.some((p) => typeof p.afterPull === 'function');
638
+ if (hasAfterPullPlugins) {
639
+ countSyncMetric('sync.client.ws.delivery.events', 1, {
640
+ attributes: { path: 'after_pull_plugins' },
641
+ });
642
+ this.triggerSyncInBackground({ trigger: 'ws' }, 'ws delivery with afterPull plugins');
643
+ return;
644
+ }
645
+ // Apply changes + update cursor
646
+ const inlineApplyStartedAtMs = Date.now();
647
+ const applied = await this.applyWsDeliveredChanges(changes, cursor);
648
+ const inlineApplyDurationMs = Math.max(0, Date.now() - inlineApplyStartedAtMs);
649
+ distributionSyncMetric('sync.client.ws.inline_apply.duration_ms', inlineApplyDurationMs, {
650
+ unit: 'millisecond',
651
+ });
652
+ if (!applied) {
653
+ countSyncMetric('sync.client.ws.delivery.events', 1, {
654
+ attributes: { path: 'inline_fallback' },
655
+ });
656
+ this.triggerSyncInBackground({ trigger: 'ws' }, 'ws inline apply fallback');
657
+ return;
658
+ }
659
+ // All clear — skip HTTP pull entirely
660
+ countSyncMetric('sync.client.ws.delivery.events', 1, {
661
+ attributes: { path: 'inline_applied' },
662
+ });
663
+ this.updateState({
664
+ lastSyncAt: Date.now(),
665
+ error: null,
666
+ retryCount: 0,
667
+ isRetrying: false,
668
+ });
669
+ this.emit('sync:complete', {
670
+ timestamp: Date.now(),
671
+ pushedCommits: 0,
672
+ pullRounds: 0,
673
+ pullResponse: { ok: true, subscriptions: [] },
674
+ });
675
+ this.refreshOutboxStats().catch((error) => {
676
+ console.warn('[SyncEngine] Failed to refresh outbox stats after WS apply:', error);
677
+ });
678
+ }
492
679
  timestampCounter = 0;
493
- bumpMutationTimestamp(table, rowId, now) {
494
- const key = `${table}:${rowId}`;
680
+ nextPreciseTimestamp(now) {
495
681
  // Use sub-millisecond precision by combining timestamp with atomic counter
496
682
  // This prevents race conditions in concurrent mutations while maintaining
497
- // millisecond-level compatibility with existing code
498
- const preciseNow = now + (this.timestampCounter++ % 1000) / 1000;
683
+ // millisecond-level compatibility with existing code.
684
+ return now + (this.timestampCounter++ % 1000) / 1000;
685
+ }
686
+ bumpMutationTimestamp(table, rowId, now) {
687
+ const key = `${table}:${rowId}`;
688
+ const preciseNow = this.nextPreciseTimestamp(now);
499
689
  const prev = this.mutationTimestamps.get(key) ?? 0;
500
690
  this.mutationTimestamps.set(key, Math.max(preciseNow, prev + 0.001));
501
691
  }
692
+ bumpTableMutationTimestamp(table, now) {
693
+ const preciseNow = this.nextPreciseTimestamp(now);
694
+ const prev = this.tableMutationTimestamps.get(table) ?? 0;
695
+ this.tableMutationTimestamps.set(table, Math.max(preciseNow, prev + 0.001));
696
+ }
502
697
  /**
503
698
  * Record local mutations that were already applied to the DB.
504
699
  *
@@ -531,17 +726,12 @@ export class SyncEngine {
531
726
  }
532
727
  recordMutationTimestampsFromPullResponse(response, now) {
533
728
  for (const sub of response.subscriptions ?? []) {
534
- // Mark snapshot rows as changed so bootstrap/resnapshot updates propagate
535
- // in wa-sqlite mode.
729
+ // Mark snapshot tables as changed so bootstrap/resnapshot updates
730
+ // propagate without storing per-row timestamps for massive snapshots.
536
731
  for (const snapshot of sub.snapshots ?? []) {
537
- const table = snapshot.table;
538
- for (const row of snapshot.rows ?? []) {
539
- // Snapshot rows are untyped - access id dynamically
540
- const id = row?.id;
541
- if (id == null)
542
- continue;
543
- this.bumpMutationTimestamp(table, String(id), now);
544
- }
732
+ if (!snapshot.table)
733
+ continue;
734
+ this.bumpTableMutationTimestamp(snapshot.table, now);
545
735
  }
546
736
  for (const commit of sub.commits ?? []) {
547
737
  for (const change of commit.changes ?? []) {
@@ -568,7 +758,7 @@ export class SyncEngine {
568
758
  this.retryTimeoutId = setTimeout(() => {
569
759
  this.retryTimeoutId = null;
570
760
  if (!this.isDestroyed) {
571
- this.sync();
761
+ this.triggerSyncInBackground(undefined, 'retry timer');
572
762
  }
573
763
  }, delay);
574
764
  }
@@ -576,12 +766,17 @@ export class SyncEngine {
576
766
  this.emit('sync:error', error);
577
767
  this.config.onError?.(error);
578
768
  }
769
+ triggerSyncInBackground(opts, reason = 'background') {
770
+ void this.sync(opts).catch((error) => {
771
+ console.error(`[SyncEngine] Unexpected sync failure during ${reason}:`, error);
772
+ });
773
+ }
579
774
  setupPolling() {
580
775
  this.stopPolling();
581
776
  const interval = this.config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
582
777
  this.pollerId = setInterval(() => {
583
778
  if (!this.state.isSyncing && !this.isDestroyed) {
584
- this.sync();
779
+ this.triggerSyncInBackground(undefined, 'polling interval');
585
780
  }
586
781
  }, interval);
587
782
  this.setConnectionState('connected');
@@ -622,15 +817,36 @@ export class SyncEngine {
622
817
  }
623
818
  this.realtimeDisconnect = transport.connect({ clientId: this.config.clientId }, (event) => {
624
819
  if (event.event === 'sync') {
625
- this.sync();
820
+ countSyncMetric('sync.client.ws.events', 1, {
821
+ attributes: { type: 'sync' },
822
+ });
823
+ const hasInlineChanges = Array.isArray(event.data.changes) && event.data.changes.length > 0;
824
+ const cursor = event.data.cursor;
825
+ if (hasInlineChanges && typeof cursor === 'number') {
826
+ // WS delivered changes + cursor — may skip HTTP pull
827
+ this.handleWsDelivery(event.data.changes, cursor);
828
+ }
829
+ else {
830
+ // Cursor-only wake-up or no cursor — must HTTP sync
831
+ countSyncMetric('sync.client.ws.delivery.events', 1, {
832
+ attributes: { path: 'cursor_wakeup' },
833
+ });
834
+ this.triggerSyncInBackground({ trigger: 'ws' }, 'ws cursor wakeup');
835
+ }
626
836
  }
627
837
  }, (state) => {
628
838
  switch (state) {
629
- case 'connected':
839
+ case 'connected': {
840
+ const wasConnectedBefore = this.hasRealtimeConnectedOnce;
841
+ this.hasRealtimeConnectedOnce = true;
630
842
  this.setConnectionState('connected');
631
843
  this.stopFallbackPolling();
632
- this.sync();
844
+ this.triggerSyncInBackground(undefined, 'realtime connected state');
845
+ if (wasConnectedBefore) {
846
+ this.scheduleRealtimeReconnectCatchupSync();
847
+ }
633
848
  break;
849
+ }
634
850
  case 'connecting':
635
851
  this.setConnectionState('connecting');
636
852
  break;
@@ -642,6 +858,10 @@ export class SyncEngine {
642
858
  });
643
859
  }
644
860
  stopRealtime() {
861
+ if (this.realtimeCatchupTimeoutId) {
862
+ clearTimeout(this.realtimeCatchupTimeoutId);
863
+ this.realtimeCatchupTimeoutId = null;
864
+ }
645
865
  if (this.realtimePresenceUnsub) {
646
866
  this.realtimePresenceUnsub();
647
867
  this.realtimePresenceUnsub = null;
@@ -652,13 +872,26 @@ export class SyncEngine {
652
872
  }
653
873
  this.stopFallbackPolling();
654
874
  }
875
+ scheduleRealtimeReconnectCatchupSync() {
876
+ if (this.realtimeCatchupTimeoutId) {
877
+ clearTimeout(this.realtimeCatchupTimeoutId);
878
+ }
879
+ this.realtimeCatchupTimeoutId = setTimeout(() => {
880
+ this.realtimeCatchupTimeoutId = null;
881
+ if (this.isDestroyed || !this.isEnabled())
882
+ return;
883
+ if (this.state.connectionState !== 'connected')
884
+ return;
885
+ this.triggerSyncInBackground(undefined, 'realtime reconnect catchup');
886
+ }, REALTIME_RECONNECT_CATCHUP_DELAY_MS);
887
+ }
655
888
  startFallbackPolling() {
656
889
  if (this.fallbackPollerId)
657
890
  return;
658
891
  const interval = this.config.realtimeFallbackPollMs ?? 30_000;
659
892
  this.fallbackPollerId = setInterval(() => {
660
893
  if (!this.state.isSyncing && !this.isDestroyed) {
661
- this.sync();
894
+ this.triggerSyncInBackground(undefined, 'realtime fallback poll');
662
895
  }
663
896
  }, interval);
664
897
  }
@@ -668,6 +901,23 @@ export class SyncEngine {
668
901
  this.fallbackPollerId = null;
669
902
  }
670
903
  }
904
+ /**
905
+ * Clear all in-memory mutation state and emit data:change so UI re-renders.
906
+ * Call this after deleting local data (e.g. reset flow) so that React hooks
907
+ * recompute fingerprints from scratch instead of seeing stale timestamps.
908
+ */
909
+ resetLocalState() {
910
+ const tables = [...this.tableMutationTimestamps.keys()];
911
+ this.mutationTimestamps.clear();
912
+ this.tableMutationTimestamps.clear();
913
+ if (tables.length > 0) {
914
+ this.emit('data:change', {
915
+ scopes: tables,
916
+ timestamp: Date.now(),
917
+ });
918
+ this.config.onDataChange?.(tables);
919
+ }
920
+ }
671
921
  /**
672
922
  * Reconnect
673
923
  */
@@ -688,10 +938,7 @@ export class SyncEngine {
688
938
  // Polling mode: restart the poller and trigger a sync immediately.
689
939
  if (this.state.transportMode === 'polling') {
690
940
  this.setupPolling();
691
- // Trigger sync in background - errors are handled internally by sync()
692
- this.sync().catch((err) => {
693
- console.error('Unexpected error during reconnect sync:', err);
694
- });
941
+ this.triggerSyncInBackground(undefined, 'reconnect');
695
942
  }
696
943
  }
697
944
  /**
@@ -825,7 +1072,7 @@ export class SyncEngine {
825
1072
  updateSubscriptions(subscriptions) {
826
1073
  this.config.subscriptions = subscriptions;
827
1074
  // Trigger a sync to apply new subscriptions
828
- this.sync();
1075
+ this.triggerSyncInBackground(undefined, 'subscription update');
829
1076
  }
830
1077
  /**
831
1078
  * Apply local mutations immediately to the database and emit change events.
@@ -833,12 +1080,12 @@ export class SyncEngine {
833
1080
  */
834
1081
  async applyLocalMutation(inputs) {
835
1082
  const db = this.config.db;
836
- const shapes = this.config.shapes;
1083
+ const handlers = this.config.handlers;
837
1084
  const affectedTables = new Set();
838
1085
  const now = Date.now();
839
1086
  await db.transaction().execute(async (trx) => {
840
1087
  for (const input of inputs) {
841
- const handler = shapes.get(input.table);
1088
+ const handler = handlers.get(input.table);
842
1089
  if (!handler)
843
1090
  continue;
844
1091
  affectedTables.add(input.table);