@synclib-io/sync 0.1.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.
@@ -0,0 +1,978 @@
1
+ import { Change, SynclibDatabase } from '@synclib-io/core';
2
+
3
+ /**
4
+ * Base interface for all sync protocol messages
5
+ */
6
+ interface SyncMessage {
7
+ type: string;
8
+ toMap(): Record<string, any>;
9
+ }
10
+ /**
11
+ * Initial handshake message from client to server
12
+ */
13
+ interface HelloMessage extends SyncMessage {
14
+ type: 'hello';
15
+ client_id: string;
16
+ last_seqnum?: number;
17
+ schema_version: number;
18
+ metadata?: Record<string, any>;
19
+ }
20
+ /**
21
+ * Represents a single database change to sync
22
+ */
23
+ interface ChangeMessage extends SyncMessage {
24
+ type: 'change';
25
+ table: string;
26
+ operation: 'insert' | 'update' | 'delete';
27
+ row_id: string;
28
+ data?: Record<string, any>;
29
+ seqnum?: number;
30
+ timestamp?: Date;
31
+ }
32
+ /**
33
+ * Batch of changes (for efficient syncing)
34
+ */
35
+ interface ChangesBatchMessage extends SyncMessage {
36
+ type: 'changes_batch';
37
+ changes: ChangeMessage[];
38
+ from_seqnum?: number;
39
+ to_seqnum?: number;
40
+ }
41
+ /**
42
+ * Acknowledgment that changes were received and processed
43
+ */
44
+ interface AckMessage extends SyncMessage {
45
+ type: 'ack';
46
+ seqnum: number;
47
+ success: boolean;
48
+ error?: string;
49
+ /** Server-assigned seqnum for the row (set by Postgres trigger).
50
+ * Client should update the local row's seqnum column with this value. */
51
+ server_seqnum?: number;
52
+ /** Server-computed row_hash (set by Postgres trigger).
53
+ * Client should store this value locally — never compute row_hash on client. */
54
+ row_hash?: string;
55
+ }
56
+ /**
57
+ * Request for changes from server
58
+ */
59
+ interface RequestChangesMessage extends SyncMessage {
60
+ type: 'request_changes';
61
+ since_seqnum?: number;
62
+ limit?: number;
63
+ table?: string;
64
+ }
65
+ /**
66
+ * Error message
67
+ */
68
+ interface ErrorMessage extends SyncMessage {
69
+ type: 'error';
70
+ code: string;
71
+ message: string;
72
+ details?: Record<string, any>;
73
+ }
74
+ /**
75
+ * Phoenix channel reply message (for hello response)
76
+ */
77
+ interface PhoenixReplyMessage extends SyncMessage {
78
+ type: 'phx_reply';
79
+ status: string;
80
+ response: Record<string, any>;
81
+ }
82
+ /**
83
+ * Schema migration confirmation message
84
+ */
85
+ interface SchemaConfirmMessage extends SyncMessage {
86
+ type: 'schema_migrated';
87
+ version: number;
88
+ }
89
+ /**
90
+ * Snapshot batch message (streaming table data)
91
+ */
92
+ interface SnapshotBatchMessage extends SyncMessage {
93
+ type: 'snapshot_batch';
94
+ stream_id: string;
95
+ table: string;
96
+ rows: Array<Record<string, any>>;
97
+ }
98
+ /**
99
+ * Snapshot complete message (marks end of snapshot stream)
100
+ */
101
+ interface SnapshotCompleteMessage extends SyncMessage {
102
+ type: 'snapshot_complete';
103
+ stream_id: string;
104
+ channel_id: string;
105
+ }
106
+ /**
107
+ * Job update message (from ECS tasks via webhook)
108
+ */
109
+ interface JobUpdateMessage extends SyncMessage {
110
+ type: 'job_update';
111
+ current_step: number;
112
+ total_steps: number;
113
+ step_type: string;
114
+ job_id: string;
115
+ user_id: string;
116
+ phoenix_channel_id: string;
117
+ filename?: string;
118
+ }
119
+ /**
120
+ * Schema update notification message
121
+ */
122
+ interface SchemaUpdateMessage extends SyncMessage {
123
+ type: 'schema_update';
124
+ new_version: number;
125
+ migrations?: Array<{
126
+ version: number;
127
+ description: string;
128
+ sql: string[];
129
+ }>;
130
+ timestamp: number;
131
+ }
132
+ /**
133
+ * Livestream event message
134
+ */
135
+ interface LivestreamMessage extends SyncMessage {
136
+ type: 'livestream:started' | 'livestream:stopped';
137
+ stream_id?: string;
138
+ tribe_id?: string;
139
+ user_id?: string;
140
+ hls_url?: string;
141
+ timestamp: number;
142
+ }
143
+ /**
144
+ * Conversation event message
145
+ */
146
+ interface ConversationMessage extends SyncMessage {
147
+ type: 'conversation:user_joined' | 'conversation:user_left' | 'conversation:message_sent' | 'conversation:online_count';
148
+ conversation_id?: string;
149
+ user_id?: string;
150
+ message_id?: string;
151
+ online_count?: number;
152
+ metadata?: Record<string, any>;
153
+ timestamp: number;
154
+ }
155
+ /**
156
+ * Single change acknowledgment (used within ChangeAcksMessage)
157
+ */
158
+ interface ChangeAck {
159
+ local_seqnum: number;
160
+ success: boolean;
161
+ server_seqnum?: number;
162
+ error?: string;
163
+ /** Server-computed row_hash (set by Postgres trigger). */
164
+ row_hash?: string;
165
+ }
166
+ /**
167
+ * Batch of change acknowledgments (unified sync protocol)
168
+ */
169
+ interface ChangeAcksMessage extends SyncMessage {
170
+ type: 'change_acks';
171
+ stream_id?: string;
172
+ acks: ChangeAck[];
173
+ }
174
+ /**
175
+ * Unified sync data batch from server
176
+ */
177
+ interface SyncDataBatchMessage extends SyncMessage {
178
+ type: 'sync_data_batch' | 'sync_batch';
179
+ stream_id?: string;
180
+ table: string;
181
+ rows: Array<Record<string, any>>;
182
+ }
183
+ /**
184
+ * Unified sync completion signal with statistics
185
+ */
186
+ interface SyncCompleteMessage extends SyncMessage {
187
+ type: 'sync_complete';
188
+ stream_id?: string;
189
+ schema_version: number;
190
+ table_seqnums: Record<string, number>;
191
+ stats?: {
192
+ schema_upgraded?: boolean;
193
+ migrations_applied?: number;
194
+ push_total?: number;
195
+ push_success?: number;
196
+ push_failed?: number;
197
+ push_by_table?: Record<string, Record<string, any>>;
198
+ pull_total?: number;
199
+ pull_by_table?: Record<string, Record<string, any>>;
200
+ elapsed_ms?: number;
201
+ };
202
+ }
203
+ /**
204
+ * Server response to merkle_push_blocks (client pushes rows to server)
205
+ */
206
+ interface MerklePushBlocksResponse extends SyncMessage {
207
+ type: 'merkle_push_blocks_response';
208
+ table: string;
209
+ block_index: number;
210
+ applied: number;
211
+ rejected: number;
212
+ deleted: number;
213
+ errors: Array<Record<string, any>>;
214
+ }
215
+ /**
216
+ * Server response to merkle_lww_blocks (last-write-wins resolution)
217
+ */
218
+ interface MerkleLwwBlocksResponse extends SyncMessage {
219
+ type: 'merkle_lww_blocks_response';
220
+ table: string;
221
+ block_index: number;
222
+ client_wins: string[];
223
+ server_wins: Array<Record<string, any>>;
224
+ applied_from_client: number;
225
+ sent_to_client: number;
226
+ }
227
+ /**
228
+ * Message factory functions
229
+ */
230
+ declare const MessageFactory: {
231
+ /**
232
+ * Create a HelloMessage
233
+ */
234
+ hello(clientId: string, schemaVersion?: number, lastSeqnum?: number, metadata?: Record<string, any>): HelloMessage;
235
+ /**
236
+ * Create a ChangeMessage from a synclib Change object
237
+ */
238
+ fromChange(change: Change): ChangeMessage;
239
+ /**
240
+ * Create a ChangesBatchMessage
241
+ */
242
+ changesBatch(changes: ChangeMessage[], fromSeqnum?: number, toSeqnum?: number): ChangesBatchMessage;
243
+ /**
244
+ * Create a RequestChangesMessage
245
+ */
246
+ requestChanges(sinceSeqnum?: number, limit?: number, table?: string): RequestChangesMessage;
247
+ /**
248
+ * Create a SchemaConfirmMessage
249
+ */
250
+ schemaConfirm(version: number): SchemaConfirmMessage;
251
+ /**
252
+ * Parse a message from a map
253
+ */
254
+ fromMap(map: Record<string, any>): SyncMessage;
255
+ };
256
+
257
+ /**
258
+ * Connection state
259
+ */
260
+ declare enum ConnectionState {
261
+ DISCONNECTED = "disconnected",
262
+ CONNECTING = "connecting",
263
+ CONNECTED = "connected",
264
+ RECONNECTING = "reconnecting",
265
+ FAILED = "failed",
266
+ AUTH_FAILED = "auth_failed"
267
+ }
268
+ /**
269
+ * WebSocket connection manager using Phoenix Channels
270
+ */
271
+ declare class WebSocketManager {
272
+ private url;
273
+ private reconnectDelay;
274
+ private maxReconnectAttempts;
275
+ private socket;
276
+ private channels;
277
+ private state;
278
+ private messageListeners;
279
+ private stateListeners;
280
+ private reconnectAttempts;
281
+ private connectionParams?;
282
+ private connectStartTime;
283
+ private intentionalDisconnect;
284
+ private quickFailureCount;
285
+ private static readonly QUICK_FAILURE_THRESHOLD;
286
+ private onAuthFailureCallback?;
287
+ private authFailureFired;
288
+ constructor(url: string, reconnectDelay?: number, maxReconnectAttempts?: number);
289
+ /**
290
+ * Register a callback fired when the connection is rejected due to auth
291
+ * (e.g. expired JWT). The callback can refresh the token via setToken() and
292
+ * then call connect() again to retry.
293
+ */
294
+ setOnAuthFailure(callback: () => void): void;
295
+ /**
296
+ * Update the JWT (or other auth param) used for subsequent connect/reconnect
297
+ * attempts. The Socket reads its `params` via a thunk so the next reconnect
298
+ * picks up the new value without recreating the socket.
299
+ *
300
+ * If the connection is currently in AUTH_FAILED (reconnects were halted), a
301
+ * fresh token implies the caller wants to retry — we tear down the failed
302
+ * socket, reset state, and schedule a new connect.
303
+ */
304
+ setToken(token: string): void;
305
+ /**
306
+ * Build the string params map from current connectionParams. Called on every
307
+ * Phoenix reconnect attempt so token refreshes (via setToken) take effect
308
+ * without tearing down the socket.
309
+ */
310
+ private buildStringParams;
311
+ /**
312
+ * Connect to WebSocket server
313
+ */
314
+ connect(params?: Record<string, any>): Promise<void>;
315
+ /**
316
+ * Join a Phoenix channel
317
+ */
318
+ joinChannel(topic: string, params?: Record<string, any>): Promise<Record<string, any>>;
319
+ /**
320
+ * Check if a channel is currently joined
321
+ */
322
+ isChannelJoined(topic: string): boolean;
323
+ /**
324
+ * Get all currently joined channel topics
325
+ */
326
+ getJoinedChannels(): string[];
327
+ /**
328
+ * Leave a specific channel by topic
329
+ */
330
+ leaveChannel(topic: string): Promise<void>;
331
+ /**
332
+ * Disconnect from WebSocket server
333
+ */
334
+ disconnect(): Promise<void>;
335
+ /**
336
+ * Send a message to the server
337
+ */
338
+ send(message: SyncMessage, channelTopic?: string): Promise<void>;
339
+ /**
340
+ * Send a raw message with custom event and payload, returns server response
341
+ */
342
+ sendRaw(event: string, payload: Record<string, any>, channelTopic?: string): Promise<Record<string, any>>;
343
+ /**
344
+ * Handle incoming message
345
+ */
346
+ private handleIncomingMessage;
347
+ /**
348
+ * Handle channel reply messages
349
+ */
350
+ private handleChannelReply;
351
+ /**
352
+ * Handle WebSocket error
353
+ */
354
+ private onError;
355
+ /**
356
+ * Handle WebSocket close
357
+ */
358
+ private onClose;
359
+ /**
360
+ * Schedule automatic reconnection
361
+ */
362
+ private scheduleReconnect;
363
+ /**
364
+ * Update connection state and notify listeners
365
+ */
366
+ private updateState;
367
+ /**
368
+ * Check if connected
369
+ */
370
+ isConnected(): boolean;
371
+ /**
372
+ * Get current connection state
373
+ */
374
+ getState(): ConnectionState;
375
+ /**
376
+ * Subscribe to messages
377
+ */
378
+ onMessage(listener: (message: SyncMessage) => void): () => void;
379
+ /**
380
+ * Subscribe to state changes
381
+ */
382
+ onStateChange(listener: (state: ConnectionState) => void): () => void;
383
+ /**
384
+ * Dispose resources
385
+ */
386
+ dispose(): Promise<void>;
387
+ }
388
+
389
+ /**
390
+ * Sync readiness state (legacy - kept for backward compatibility)
391
+ */
392
+ declare enum SyncReadyState {
393
+ WAITING_FOR_HELLO = "waiting_for_hello",
394
+ APPLYING_MIGRATIONS = "applying_migrations",
395
+ READY = "ready"
396
+ }
397
+ /**
398
+ * Unified sync state - lifecycle of sync operations
399
+ */
400
+ declare enum SyncState {
401
+ DISCONNECTED = "disconnected",
402
+ CONNECTING = "connecting",
403
+ SYNCING = "syncing",
404
+ READY = "ready",
405
+ ERROR = "error"
406
+ }
407
+ /**
408
+ * Progress during sync operation
409
+ */
410
+ interface SyncProgress {
411
+ phase: 'pushing' | 'pulling' | 'migrating' | 'complete';
412
+ table?: string;
413
+ rowCount?: number;
414
+ changesPushed?: number;
415
+ changesAcked?: number;
416
+ }
417
+ /**
418
+ * Per-table sync statistics
419
+ */
420
+ interface SyncTableStats {
421
+ rowsPulled: number;
422
+ changesPushed: number;
423
+ changesSucceeded: number;
424
+ changesFailed: number;
425
+ }
426
+ /**
427
+ * Sync completion event with detailed statistics
428
+ */
429
+ interface SyncCompleteEvent {
430
+ streamId?: string;
431
+ schemaVersion: number;
432
+ tableSeqnums: Record<string, number>;
433
+ schemaUpgraded: boolean;
434
+ migrationsApplied: number;
435
+ totalRowsPulled: number;
436
+ totalChangesPushed: number;
437
+ totalChangesSucceeded: number;
438
+ totalChangesFailed: number;
439
+ pullStats: Record<string, SyncTableStats>;
440
+ pushStats: Record<string, SyncTableStats>;
441
+ }
442
+ /**
443
+ * Event emitted when Merkle verification completes
444
+ * Fires after background verification, especially useful when repairs occurred
445
+ */
446
+ interface MerkleVerificationEvent {
447
+ /** Tables that were repaired (empty if all matched) */
448
+ repairedTables: string[];
449
+ /** Whether any repairs were made */
450
+ hadMismatches: boolean;
451
+ }
452
+ /**
453
+ * Direction of repair when data differs between client and server.
454
+ * Applies to both seqnum-based sync and merkle tree verification.
455
+ */
456
+ declare enum RepairDirection {
457
+ /** Server is authoritative. Client overwrites local data with server data. */
458
+ PULL = "pull",
459
+ /** Client is authoritative. Client sends its rows to server. */
460
+ PUSH = "push",
461
+ /** Last-write-wins. Compare last_modified_ms per row. */
462
+ LWW = "lww"
463
+ }
464
+ /**
465
+ * Role of a channel in sync topology.
466
+ */
467
+ declare enum ChannelRole {
468
+ /** Client pushes its own data on this channel (e.g., user channel). */
469
+ PUSH = "push",
470
+ /** Client pulls shared data from this channel (e.g., tribe channel). */
471
+ PULL = "pull",
472
+ /** Channel supports both push and pull. */
473
+ BIDIRECTIONAL = "bidirectional"
474
+ }
475
+ /**
476
+ * A table associated with a sync channel.
477
+ * direction overrides the channel's default repair direction for this table.
478
+ * If omitted, defaults to the channel role's implied direction.
479
+ */
480
+ interface SyncTable {
481
+ name: string;
482
+ direction?: RepairDirection;
483
+ /** Columns to include in merkle hash (besides id). When set, only these columns
484
+ * are hashed, bypassing the precomputed row_hash fast path. */
485
+ hashColumns?: string[];
486
+ }
487
+ /**
488
+ * A sync channel: its topic, role, associated tables, and join params.
489
+ *
490
+ * Used for both seqnum-based sync (which tables to push/pull) and merkle
491
+ * tree verification (which tables to verify and how to repair).
492
+ */
493
+ interface SyncChannel {
494
+ /** The Phoenix channel topic (e.g., "sync:tribe:trainer123", "sync:user:user456") */
495
+ topic: string;
496
+ /** Role of this channel in the sync topology. */
497
+ role: ChannelRole;
498
+ /** Tables on this channel. Used for both seqnum sync and merkle verification. */
499
+ tables?: SyncTable[];
500
+ /** Optional params to send when joining this channel. */
501
+ params?: Record<string, string>;
502
+ }
503
+ /**
504
+ * Configuration for sync client
505
+ */
506
+ interface SyncClientConfig {
507
+ /** Synclib database instance */
508
+ db: SynclibDatabase;
509
+ /** WebSocket server URL */
510
+ serverUrl: string;
511
+ /** Unique client identifier */
512
+ clientId: string;
513
+ /** Channels to connect to and sync on. */
514
+ channels: SyncChannel[];
515
+ /** Batch size for pushing changes */
516
+ pushBatchSize?: number;
517
+ /** Optional metadata to send in hello message */
518
+ metadata?: Record<string, any>;
519
+ /** Debounce duration in ms for notifyLocalChange().
520
+ * Batches rapid writes together before pushing.
521
+ * Defaults to 100ms. */
522
+ syncOnWriteDebounce?: number;
523
+ /** Connection timeout in ms for waitForConnection().
524
+ * Defaults to 30000 (30 seconds). */
525
+ connectionTimeout?: number;
526
+ /** Sync operation timeout in ms.
527
+ * Defaults to 300000 (5 minutes). */
528
+ syncTimeout?: number;
529
+ /** Interval in ms for periodic background sync.
530
+ * When set, a timer calls syncUnified() at this interval.
531
+ * When undefined, sync is purely reactive (notifyLocalChange + manual calls).
532
+ * Defaults to undefined (disabled). */
533
+ periodicSyncInterval?: number;
534
+ /** Interval in ms for periodic Merkle integrity verification.
535
+ * When set, the client will periodically verify data integrity
536
+ * for the specified tables using Merkle tree comparison.
537
+ * Defaults to undefined (disabled). */
538
+ merkleVerifyInterval?: number;
539
+ }
540
+ /**
541
+ * Conflict resolver callback
542
+ */
543
+ type ConflictResolver = (local: ChangeMessage, remote: ChangeMessage) => Promise<ChangeMessage | null>;
544
+ /**
545
+ * Main sync client orchestrating bidirectional sync
546
+ */
547
+ declare class SyncClient {
548
+ private ws;
549
+ private db;
550
+ private config;
551
+ private serverMerkleBlockSize?;
552
+ private serverHashColumns?;
553
+ private isInitialized;
554
+ private hasConnectedOnce;
555
+ private pendingAcks;
556
+ private pendingChangeInfo;
557
+ private remoteChangeListeners;
558
+ private snapshotCompleteListeners;
559
+ private jobUpdateListeners;
560
+ private livestreamListeners;
561
+ private conversationListeners;
562
+ private syncReadyStateListeners;
563
+ private readyState;
564
+ private batchQueue;
565
+ private batchLock;
566
+ private messageQueue;
567
+ private processingMessages;
568
+ private conflictResolver?;
569
+ private helloHandshakeResolve?;
570
+ private syncOnWriteDebounceTimer?;
571
+ private periodicSyncTimer?;
572
+ private syncState;
573
+ private isSyncing;
574
+ private tableSeqnums;
575
+ private activeSyncCompleters;
576
+ private syncStateListeners;
577
+ private syncProgressListeners;
578
+ private syncCompleteListeners;
579
+ private merkleVerificationListeners;
580
+ constructor(config: SyncClientConfig);
581
+ /** Get channels that push (push or bidirectional role). */
582
+ private get pushChannels();
583
+ /** Get channels that pull (pull or bidirectional role). */
584
+ private get pullChannels();
585
+ /** Get all sync table names from channels config. */
586
+ private get allSyncTables();
587
+ /**
588
+ * One-time migration: switch to server-authoritative row_hash.
589
+ * Sets all local row_hash values to '' (sentinel) so merkle comparison
590
+ * detects mismatch and triggers repair from server.
591
+ */
592
+ private migrateToServerAuthoritativeRowHash;
593
+ /**
594
+ * Initialize the sync client
595
+ */
596
+ initialize(): Promise<void>;
597
+ /**
598
+ * Connect to sync server.
599
+ * `extra` is forwarded as additional Phoenix socket connect params —
600
+ * used for `slug` and `env` so the server can route the per-app
601
+ * Postgres schema ({slug}_dev vs {slug}_prod).
602
+ */
603
+ connect(token: string, extra?: Record<string, string>): Promise<void>;
604
+ /**
605
+ * Update the auth token used on subsequent (re)connects without tearing
606
+ * down the socket. Use this when the host app refreshes a JWT — typically
607
+ * either proactively (on a timer) or reactively (in response to
608
+ * setOnAuthFailure).
609
+ */
610
+ setToken(token: string): void;
611
+ /**
612
+ * Register a callback fired when the WebSocket is rejected due to bad/expired
613
+ * auth. The host app should refresh the token, call setToken(newToken), then
614
+ * call connect() again (or rely on the next scheduled reconnect, after we
615
+ * clear AUTH_FAILED state).
616
+ */
617
+ setOnAuthFailure(callback: () => void): void;
618
+ /**
619
+ * Join all configured channels
620
+ */
621
+ private joinChannels;
622
+ /**
623
+ * Join an additional channel after initial connection
624
+ */
625
+ joinChannel(channel: SyncChannel): Promise<void>;
626
+ /**
627
+ * Check if a channel is currently joined
628
+ */
629
+ isChannelJoined(channel: SyncChannel): boolean;
630
+ /**
631
+ * Get all currently joined channel topics
632
+ */
633
+ getJoinedChannels(): string[];
634
+ /**
635
+ * Update the channel configuration dynamically.
636
+ * Useful when new tables are discovered (e.g. after schema_update migration).
637
+ * The allSyncTables getter automatically uses config.channels.
638
+ */
639
+ updateChannels(channels: SyncChannel[]): void;
640
+ /**
641
+ * Leave a channel
642
+ */
643
+ leaveChannel(channel: SyncChannel): Promise<void>;
644
+ /**
645
+ * Leave a channel by its topic ID (e.g., "sync:user:123")
646
+ */
647
+ leaveChannelById(channelId: string): Promise<void>;
648
+ /**
649
+ * Disconnect from sync server
650
+ */
651
+ disconnect(): Promise<void>;
652
+ /**
653
+ * Manually trigger a sync cycle.
654
+ * Calls syncUnified() which handles push, pull, schema, and stripped content.
655
+ */
656
+ sync(): Promise<void>;
657
+ /**
658
+ * Unified sync - single operation that handles push, pull, and schema in one request.
659
+ *
660
+ * This is the recommended sync method that matches the Flutter implementation.
661
+ * It prevents concurrent syncs and waits for connection if disconnected.
662
+ */
663
+ syncUnified(options?: {
664
+ forceRefresh?: string[];
665
+ includeStripped?: boolean;
666
+ }): Promise<void>;
667
+ /**
668
+ * Wait for WebSocket connection with timeout
669
+ */
670
+ private waitForConnection;
671
+ /**
672
+ * Get per-table seqnums for incremental sync
673
+ */
674
+ private getPerTableSeqnums;
675
+ /**
676
+ * Wait for sync complete message
677
+ */
678
+ private waitForSyncComplete;
679
+ /**
680
+ * Update sync state and notify listeners
681
+ */
682
+ private updateSyncState;
683
+ /**
684
+ * Emit sync progress event
685
+ */
686
+ private emitSyncProgress;
687
+ /**
688
+ * Extract server-driven hash_columns from a channel join response.
689
+ */
690
+ private extractServerHashColumns;
691
+ /**
692
+ * Send hello message to server
693
+ */
694
+ private sendHello;
695
+ /**
696
+ * Enqueue a message for serialized processing.
697
+ * Prevents interleaving when multiple async messages arrive rapidly.
698
+ */
699
+ private enqueueMessage;
700
+ /**
701
+ * Process queued messages one at a time.
702
+ */
703
+ private processMessageQueue;
704
+ /**
705
+ * Handle incoming message from server
706
+ */
707
+ private handleMessage;
708
+ /**
709
+ * Apply a single remote change to local database
710
+ */
711
+ private applyRemoteChange;
712
+ /**
713
+ * Apply multiple remote changes
714
+ */
715
+ private applyRemoteChanges;
716
+ /**
717
+ * Handle acknowledgment from server
718
+ */
719
+ private handleAck;
720
+ /**
721
+ * Update the seqnum column on a local row after server assigns it
722
+ */
723
+ private updateLocalSeqnum;
724
+ /**
725
+ * Update the row_hash column on a local row with the server-computed value
726
+ */
727
+ private updateLocalRowHash;
728
+ /**
729
+ * Handle error message from server
730
+ */
731
+ private handleError;
732
+ /**
733
+ * Queue snapshot batch for processing
734
+ */
735
+ private queueSnapshotBatch;
736
+ /**
737
+ * Process all batches in the queue serially
738
+ */
739
+ private processBatchQueue;
740
+ /**
741
+ * Handle snapshot batch message
742
+ */
743
+ private handleSnapshotBatch;
744
+ /**
745
+ * Handle snapshot complete message
746
+ */
747
+ private handleSnapshotComplete;
748
+ /**
749
+ * Handle job update message
750
+ */
751
+ private handleJobUpdate;
752
+ /**
753
+ * Handle livestream message
754
+ */
755
+ private handleLivestream;
756
+ /**
757
+ * Handle conversation message
758
+ */
759
+ private handleConversation;
760
+ /**
761
+ * Handle schema update notification
762
+ */
763
+ private handleSchemaUpdate;
764
+ /**
765
+ * Handle batch of change acknowledgments (unified sync protocol)
766
+ */
767
+ private handleChangeAcks;
768
+ /**
769
+ * Handle sync data batch (unified sync protocol)
770
+ */
771
+ private handleSyncDataBatch;
772
+ /**
773
+ * Handle sync complete message (unified sync protocol)
774
+ */
775
+ private handleSyncComplete;
776
+ /**
777
+ * Handle Phoenix reply messages
778
+ */
779
+ private handlePhoenixReply;
780
+ /**
781
+ * Apply schema migrations from server
782
+ */
783
+ private applyMigrations;
784
+ /**
785
+ * Update sync ready state
786
+ */
787
+ private updateReadyState;
788
+ /**
789
+ * Handle connection state changes
790
+ */
791
+ private handleStateChange;
792
+ /**
793
+ * Generate SQL statement from change message
794
+ */
795
+ private generateSql;
796
+ /**
797
+ * Format a value for SQL
798
+ */
799
+ private formatSqlValue;
800
+ /**
801
+ * Escape single quotes in SQL strings
802
+ */
803
+ private escapeSql;
804
+ /**
805
+ * Quote a SQL identifier (table or column name) to prevent SQL injection.
806
+ */
807
+ private quoteId;
808
+ /**
809
+ * Convert operation string to SynclibOperation
810
+ */
811
+ private toSynclibOperation;
812
+ /**
813
+ * Execute function with batch lock (serial processing)
814
+ */
815
+ private withBatchLock;
816
+ /**
817
+ * Stream snapshot of tables from server
818
+ */
819
+ streamSnapshot(tables: string[], options?: {
820
+ incremental?: boolean;
821
+ channelTopic?: string;
822
+ }): Promise<void>;
823
+ /**
824
+ * Fetch a full row from the server
825
+ */
826
+ fetchRow(table: string, rowId: string): Promise<Record<string, any>>;
827
+ /**
828
+ * Send a custom message to the server and wait for reply
829
+ */
830
+ sendMessage(event: string, payload: Record<string, any>, channelTopic?: string): Promise<Record<string, any>>;
831
+ /**
832
+ * Send a conversation presence event
833
+ */
834
+ sendConversationPresence(options: {
835
+ conversationId: string;
836
+ userId: string;
837
+ event: 'conversation:user_joined' | 'conversation:user_left';
838
+ channelTopic?: string;
839
+ }): Promise<void>;
840
+ /**
841
+ * Set conflict resolver
842
+ */
843
+ setConflictResolver(resolver: ConflictResolver): void;
844
+ /**
845
+ * Notify the sync client that a local change occurred.
846
+ *
847
+ * Call this after writing to the database to trigger an immediate push.
848
+ * Uses debouncing to batch rapid writes together (configurable via syncOnWriteDebounce).
849
+ *
850
+ * Example:
851
+ * ```typescript
852
+ * // After writing to the database
853
+ * await db.write({ tableName: 'users', rowId: '123', ... });
854
+ * syncClient.notifyLocalChange();
855
+ * ```
856
+ */
857
+ notifyLocalChange(): void;
858
+ /** Start periodic background sync timer if configured. */
859
+ private startPeriodicSync;
860
+ /** Stop periodic background sync timer. */
861
+ private stopPeriodicSync;
862
+ /**
863
+ * Event listeners
864
+ */
865
+ onRemoteChange(listener: (change: ChangeMessage) => void): () => void;
866
+ onSnapshotComplete(listener: (streamId: string, channelId: string) => void): () => void;
867
+ onJobUpdate(listener: (update: JobUpdateMessage) => void): () => void;
868
+ onLivestream(listener: (message: LivestreamMessage) => void): () => void;
869
+ onConversation(listener: (message: ConversationMessage) => void): () => void;
870
+ onSyncReadyStateChange(listener: (state: SyncReadyState) => void): () => void;
871
+ onStateChange(listener: (state: ConnectionState) => void): () => void;
872
+ /**
873
+ * Listen to unified sync state changes
874
+ */
875
+ onSyncStateChange(listener: (state: SyncState) => void): () => void;
876
+ /**
877
+ * Listen to sync progress events
878
+ */
879
+ onSyncProgress(listener: (progress: SyncProgress) => void): () => void;
880
+ /**
881
+ * Listen to sync complete events
882
+ */
883
+ onSyncComplete(listener: (event: SyncCompleteEvent) => void): () => void;
884
+ /**
885
+ * Listen to Merkle verification events
886
+ * Fires after background verification completes, especially useful when repairs occurred.
887
+ * Subscribe to invalidate caches/refs when data was repaired.
888
+ */
889
+ onMerkleVerification(listener: (event: MerkleVerificationEvent) => void): () => void;
890
+ /**
891
+ * Getters
892
+ */
893
+ get connectionState(): ConnectionState;
894
+ get isReady(): boolean;
895
+ get currentReadyState(): SyncReadyState;
896
+ /**
897
+ * Get current unified sync state
898
+ */
899
+ get currentSyncState(): SyncState;
900
+ /**
901
+ * Check if Merkle verification is needed based on staleness.
902
+ * Called at the end of syncUnified to run verification if interval has elapsed.
903
+ */
904
+ private checkMerkleVerification;
905
+ /**
906
+ * Merkle verification using the new channels config.
907
+ * Verifies all tables per channel, then dispatches repair by direction.
908
+ */
909
+ private merkleVerifyFromChannels;
910
+ /**
911
+ * Legacy merkle verification using old config fields (backward compat).
912
+ */
913
+ /**
914
+ * Compare local Merkle roots against server and return mismatches.
915
+ * Shared root-verification step used by the channels config path.
916
+ */
917
+ private verifyRoots;
918
+ /**
919
+ * Verify data integrity using Merkle trees.
920
+ *
921
+ * Compares local Merkle roots against server and repairs any mismatched blocks.
922
+ * This is a consistency audit that catches:
923
+ * - Data corruption during development
924
+ * - Seqnum drift from manual database edits
925
+ * - Missed changes from network issues
926
+ * - Any state where seqnums match but data differs
927
+ *
928
+ * @param options - Verification options
929
+ * @returns List of tables that were repaired. Empty array means all matched.
930
+ *
931
+ * @example
932
+ * ```typescript
933
+ * const repairedTables = await syncClient.verifyIntegrity({
934
+ * tables: ['users', 'workouts'],
935
+ * blockSize: 100,
936
+ * });
937
+ * if (repairedTables.length > 0) {
938
+ * console.log('Repaired tables:', repairedTables);
939
+ * }
940
+ * ```
941
+ */
942
+ verifyIntegrity(options?: {
943
+ tables?: string[];
944
+ blockSize?: number;
945
+ channelTopic?: string;
946
+ }): Promise<string[]>;
947
+ /**
948
+ * Repair a table by fetching differing blocks from server (pull: server → client)
949
+ */
950
+ private repairTablePull;
951
+ /**
952
+ * Fetch a block from server and apply to local database
953
+ */
954
+ private fetchAndApplyBlock;
955
+ /**
956
+ * Repair a table by pushing local rows to server (push: client → server)
957
+ */
958
+ private repairTablePush;
959
+ /**
960
+ * Read local rows for a block and push them to the server.
961
+ */
962
+ private pushBlockToServer;
963
+ /**
964
+ * Repair a table using last-write-wins (LWW) resolution.
965
+ * Sends local rows to server which compares last_modified_ms timestamps.
966
+ */
967
+ private repairTableLww;
968
+ /**
969
+ * Resolve a single block using last-write-wins.
970
+ */
971
+ private lwwResolveBlock;
972
+ /**
973
+ * Dispose resources
974
+ */
975
+ dispose(): Promise<void>;
976
+ }
977
+
978
+ export { type AckMessage, type ChangeAck, type ChangeAcksMessage, type ChangeMessage, type ChangesBatchMessage, ChannelRole, type ConflictResolver, ConnectionState, type ConversationMessage, type ErrorMessage, type HelloMessage, type JobUpdateMessage, type LivestreamMessage, type MerkleLwwBlocksResponse, type MerklePushBlocksResponse, type MerkleVerificationEvent, MessageFactory, type PhoenixReplyMessage, RepairDirection, type RequestChangesMessage, type SchemaConfirmMessage, type SchemaUpdateMessage, type SnapshotBatchMessage, type SnapshotCompleteMessage, type SyncChannel, SyncClient, type SyncClientConfig, type SyncCompleteEvent, type SyncCompleteMessage, type SyncDataBatchMessage, type SyncMessage, type SyncProgress, SyncReadyState, SyncState, type SyncTable, type SyncTableStats, WebSocketManager };