@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.
- package/README.md +578 -0
- package/dist/index.d.mts +978 -0
- package/dist/index.d.ts +978 -0
- package/dist/index.js +2843 -0
- package/dist/index.mjs +2809 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
# synclib-sync
|
|
2
|
+
|
|
3
|
+
TypeScript/JavaScript sync client for coordinating database changes with Elixir Phoenix server. Works in tandem with [synclib](../synclib_js) to provide real-time bidirectional sync between your local SQLite database and a Phoenix server.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔄 **Bidirectional Sync** - Push local changes and receive remote updates
|
|
8
|
+
- 🔌 **Phoenix Channels** - Built on Phoenix WebSocket channels for real-time communication
|
|
9
|
+
- 🔁 **Automatic Reconnection** - Handles disconnections and reconnects automatically
|
|
10
|
+
- 📦 **Batch Operations** - Efficient batching of changes for better performance
|
|
11
|
+
- 🔀 **Conflict Resolution** - Pluggable conflict resolution strategies
|
|
12
|
+
- 📊 **Schema Migrations** - Automatic schema version management and migrations
|
|
13
|
+
- 📸 **Snapshot Streaming** - Efficient initial data loading with incremental updates
|
|
14
|
+
- 💬 **Real-time Events** - Support for livestream, conversation, and job update events
|
|
15
|
+
- 🎯 **TypeScript** - Full type safety and IntelliSense support
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install synclib-sync synclib
|
|
21
|
+
# or
|
|
22
|
+
pnpm add synclib-sync synclib
|
|
23
|
+
# or
|
|
24
|
+
yarn add synclib-sync synclib
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { SynclibDatabase } from 'synclib';
|
|
31
|
+
import { SyncClient } from 'synclib-sync';
|
|
32
|
+
|
|
33
|
+
// 1. Open your local database
|
|
34
|
+
const db = await SynclibDatabase.open({
|
|
35
|
+
databaseName: 'myapp.db'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 2. Create tables
|
|
39
|
+
db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
name TEXT NOT NULL,
|
|
43
|
+
email TEXT NOT NULL
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
// 3. Create sync client
|
|
48
|
+
const syncClient = new SyncClient({
|
|
49
|
+
db,
|
|
50
|
+
serverUrl: 'wss://api.example.com/socket',
|
|
51
|
+
clientId: 'user-123',
|
|
52
|
+
initialChannels: [
|
|
53
|
+
{
|
|
54
|
+
channelName: 'user',
|
|
55
|
+
channelId: 'user-123',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
pushInterval: 5000, // Push changes every 5 seconds
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 4. Initialize and connect
|
|
62
|
+
await syncClient.initialize();
|
|
63
|
+
await syncClient.connect('your-auth-token');
|
|
64
|
+
|
|
65
|
+
// 5. Stream initial data
|
|
66
|
+
await syncClient.streamSnapshot(['users']);
|
|
67
|
+
|
|
68
|
+
// 6. Listen for remote changes
|
|
69
|
+
syncClient.onRemoteChange((change) => {
|
|
70
|
+
console.log('Remote change applied:', change);
|
|
71
|
+
// Refresh your UI
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// 7. Make local changes (they'll be synced automatically)
|
|
75
|
+
await db.write({
|
|
76
|
+
tableName: 'users',
|
|
77
|
+
rowId: '456',
|
|
78
|
+
operation: SynclibOperation.INSERT,
|
|
79
|
+
sql: `INSERT INTO users (id, name, email) VALUES (?, ?, ?)`,
|
|
80
|
+
params: ['456', 'Jane Doe', 'jane@example.com'],
|
|
81
|
+
data: JSON.stringify({ id: '456', name: 'Jane Doe', email: 'jane@example.com' })
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage with Svelte
|
|
86
|
+
|
|
87
|
+
Here's a complete example of integrating synclib-sync in a Svelte application:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// src/lib/stores/sync.ts
|
|
91
|
+
import { writable } from 'svelte/store';
|
|
92
|
+
import { SynclibDatabase } from 'synclib';
|
|
93
|
+
import { SyncClient, type SyncClientConfig } from 'synclib-sync';
|
|
94
|
+
|
|
95
|
+
export const db = writable<SynclibDatabase | null>(null);
|
|
96
|
+
export const syncClient = writable<SyncClient | null>(null);
|
|
97
|
+
export const isConnected = writable(false);
|
|
98
|
+
export const isSyncing = writable(false);
|
|
99
|
+
|
|
100
|
+
export async function initializeSync(userId: string, token: string) {
|
|
101
|
+
// Open database
|
|
102
|
+
const database = await SynclibDatabase.open({
|
|
103
|
+
databaseName: 'myapp.db'
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Run migrations
|
|
107
|
+
const currentVersion = database.getSchemaVersion();
|
|
108
|
+
if (currentVersion < 1) {
|
|
109
|
+
database.exec(`
|
|
110
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
111
|
+
id TEXT PRIMARY KEY,
|
|
112
|
+
title TEXT NOT NULL,
|
|
113
|
+
completed INTEGER DEFAULT 0
|
|
114
|
+
)
|
|
115
|
+
`);
|
|
116
|
+
await database.setSchemaVersion(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create sync client
|
|
120
|
+
const client = new SyncClient({
|
|
121
|
+
db: database,
|
|
122
|
+
serverUrl: 'wss://api.example.com/socket',
|
|
123
|
+
clientId: userId,
|
|
124
|
+
initialChannels: [
|
|
125
|
+
{
|
|
126
|
+
channelName: 'user',
|
|
127
|
+
channelId: userId,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
pushInterval: 5000,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Listen for connection state changes
|
|
134
|
+
client.onStateChange((state) => {
|
|
135
|
+
isConnected.set(state === 'connected');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Listen for remote changes
|
|
139
|
+
client.onRemoteChange((change) => {
|
|
140
|
+
console.log('Remote change:', change);
|
|
141
|
+
// Trigger UI refresh
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Initialize and connect
|
|
145
|
+
await client.initialize();
|
|
146
|
+
await client.connect(token);
|
|
147
|
+
|
|
148
|
+
// Stream initial data
|
|
149
|
+
isSyncing.set(true);
|
|
150
|
+
await client.streamSnapshot(['todos']);
|
|
151
|
+
|
|
152
|
+
// Wait for snapshot to complete
|
|
153
|
+
await new Promise<void>((resolve) => {
|
|
154
|
+
const unsubscribe = client.onSnapshotComplete(() => {
|
|
155
|
+
unsubscribe();
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
isSyncing.set(false);
|
|
160
|
+
|
|
161
|
+
db.set(database);
|
|
162
|
+
syncClient.set(client);
|
|
163
|
+
|
|
164
|
+
return { database, client };
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```svelte
|
|
169
|
+
<!-- src/routes/+layout.svelte -->
|
|
170
|
+
<script lang="ts">
|
|
171
|
+
import { onMount } from 'svelte';
|
|
172
|
+
import { initializeSync, isConnected, isSyncing } from '$lib/stores/sync';
|
|
173
|
+
|
|
174
|
+
let userId = 'user-123';
|
|
175
|
+
let token = 'your-auth-token';
|
|
176
|
+
|
|
177
|
+
onMount(async () => {
|
|
178
|
+
await initializeSync(userId, token);
|
|
179
|
+
});
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
{#if $isSyncing}
|
|
183
|
+
<div class="loading">Syncing data...</div>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
{#if !$isConnected}
|
|
187
|
+
<div class="offline">Offline - changes will sync when reconnected</div>
|
|
188
|
+
{/if}
|
|
189
|
+
|
|
190
|
+
<slot />
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```svelte
|
|
194
|
+
<!-- src/routes/+page.svelte -->
|
|
195
|
+
<script lang="ts">
|
|
196
|
+
import { db } from '$lib/stores/sync';
|
|
197
|
+
import { SynclibOperation } from 'synclib';
|
|
198
|
+
|
|
199
|
+
let todos: Array<{ id: string; title: string; completed: number }> = [];
|
|
200
|
+
let newTodoTitle = '';
|
|
201
|
+
|
|
202
|
+
$: if ($db) {
|
|
203
|
+
loadTodos();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function loadTodos() {
|
|
207
|
+
if (!$db) return;
|
|
208
|
+
todos = $db.read('SELECT * FROM todos ORDER BY id');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function addTodo() {
|
|
212
|
+
if (!$db || !newTodoTitle.trim()) return;
|
|
213
|
+
|
|
214
|
+
const id = crypto.randomUUID();
|
|
215
|
+
|
|
216
|
+
await $db.write({
|
|
217
|
+
tableName: 'todos',
|
|
218
|
+
rowId: id,
|
|
219
|
+
operation: SynclibOperation.INSERT,
|
|
220
|
+
sql: `INSERT INTO todos (id, title, completed) VALUES (?, ?, ?)`,
|
|
221
|
+
params: [id, newTodoTitle, 0],
|
|
222
|
+
data: JSON.stringify({ id, title: newTodoTitle, completed: 0 })
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
newTodoTitle = '';
|
|
226
|
+
loadTodos();
|
|
227
|
+
}
|
|
228
|
+
</script>
|
|
229
|
+
|
|
230
|
+
<div>
|
|
231
|
+
<h1>Todos</h1>
|
|
232
|
+
|
|
233
|
+
<input
|
|
234
|
+
bind:value={newTodoTitle}
|
|
235
|
+
on:keypress={(e) => e.key === 'Enter' && addTodo()}
|
|
236
|
+
placeholder="Add a todo"
|
|
237
|
+
/>
|
|
238
|
+
<button on:click={addTodo}>Add</button>
|
|
239
|
+
|
|
240
|
+
{#each todos as todo}
|
|
241
|
+
<div>
|
|
242
|
+
<input
|
|
243
|
+
type="checkbox"
|
|
244
|
+
checked={todo.completed === 1}
|
|
245
|
+
on:change={() => toggleTodo(todo.id, todo.completed)}
|
|
246
|
+
/>
|
|
247
|
+
<span>{todo.title}</span>
|
|
248
|
+
</div>
|
|
249
|
+
{/each}
|
|
250
|
+
</div>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## API Reference
|
|
254
|
+
|
|
255
|
+
### SyncClient
|
|
256
|
+
|
|
257
|
+
Main class for managing sync with the server.
|
|
258
|
+
|
|
259
|
+
#### Constructor
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
new SyncClient(config: SyncClientConfig)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Config Options:**
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
interface SyncClientConfig {
|
|
269
|
+
// Required
|
|
270
|
+
db: SynclibDatabase; // Synclib database instance
|
|
271
|
+
serverUrl: string; // WebSocket server URL
|
|
272
|
+
clientId: string; // Unique client identifier
|
|
273
|
+
initialChannels: SyncClientChannel[]; // Channels to join on connect
|
|
274
|
+
|
|
275
|
+
// Optional
|
|
276
|
+
pushInterval?: number; // How often to push changes (ms), null = manual
|
|
277
|
+
pushBatchSize?: number; // Max changes per push (default: 100)
|
|
278
|
+
pullInterval?: number | null; // How often to pull changes (ms), null = reactive
|
|
279
|
+
metadata?: Record<string, any>; // Custom metadata for hello message
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface SyncClientChannel {
|
|
283
|
+
channelName: string; // Channel type (e.g., 'user', 'tribe', 'guild')
|
|
284
|
+
channelId: string; // Specific channel ID
|
|
285
|
+
params?: Record<string, string>; // Additional channel params
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### Methods
|
|
290
|
+
|
|
291
|
+
**`async initialize(): Promise<void>`**
|
|
292
|
+
|
|
293
|
+
Initialize the sync client. Must be called before connect().
|
|
294
|
+
|
|
295
|
+
**`async connect(token: string): Promise<void>`**
|
|
296
|
+
|
|
297
|
+
Connect to the sync server with authentication token.
|
|
298
|
+
|
|
299
|
+
**`async disconnect(): Promise<void>`**
|
|
300
|
+
|
|
301
|
+
Disconnect from the sync server.
|
|
302
|
+
|
|
303
|
+
**`async sync(): Promise<void>`**
|
|
304
|
+
|
|
305
|
+
Manually trigger a sync cycle (push local changes, pull remote changes).
|
|
306
|
+
|
|
307
|
+
**`async joinChannel(channel: SyncClientChannel): Promise<void>`**
|
|
308
|
+
|
|
309
|
+
Join an additional channel after initial connection.
|
|
310
|
+
|
|
311
|
+
**`async leaveChannel(channel: SyncClientChannel): Promise<void>`**
|
|
312
|
+
|
|
313
|
+
Leave a channel.
|
|
314
|
+
|
|
315
|
+
**`isChannelJoined(channel: SyncClientChannel): boolean`**
|
|
316
|
+
|
|
317
|
+
Check if a channel is currently joined.
|
|
318
|
+
|
|
319
|
+
**`getJoinedChannels(): string[]`**
|
|
320
|
+
|
|
321
|
+
Get all currently joined channel topics.
|
|
322
|
+
|
|
323
|
+
**`async streamSnapshot(tables: string[], options?: { incremental?: boolean; channelTopic?: string }): Promise<void>`**
|
|
324
|
+
|
|
325
|
+
Stream a snapshot of tables from the server.
|
|
326
|
+
|
|
327
|
+
- `tables`: Array of table names to stream
|
|
328
|
+
- `options.incremental`: If true, only stream changes since last sync
|
|
329
|
+
- `options.channelTopic`: Specific channel to use for the request
|
|
330
|
+
|
|
331
|
+
**`async fetchRow(table: string, rowId: string): Promise<Record<string, any>>`**
|
|
332
|
+
|
|
333
|
+
Fetch a single row from the server (including JSONB fields).
|
|
334
|
+
|
|
335
|
+
**`async sendMessage(event: string, payload: Record<string, any>, channelTopic?: string): Promise<Record<string, any>>`**
|
|
336
|
+
|
|
337
|
+
Send a custom message to the server and wait for reply.
|
|
338
|
+
|
|
339
|
+
**`async sendConversationPresence(options): Promise<void>`**
|
|
340
|
+
|
|
341
|
+
Send a conversation presence event (user_joined or user_left).
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
await syncClient.sendConversationPresence({
|
|
345
|
+
conversationId: 'tribe_123',
|
|
346
|
+
userId: 'user_456',
|
|
347
|
+
event: 'conversation:user_joined',
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**`setConflictResolver(resolver: ConflictResolver): void`**
|
|
352
|
+
|
|
353
|
+
Set a custom conflict resolution function.
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
syncClient.setConflictResolver(async (local, remote) => {
|
|
357
|
+
// Return the change to apply, or null to skip
|
|
358
|
+
// Example: last-write-wins
|
|
359
|
+
return remote.timestamp! > local.timestamp! ? remote : local;
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### Event Listeners
|
|
364
|
+
|
|
365
|
+
All event listeners return an unsubscribe function.
|
|
366
|
+
|
|
367
|
+
**`onRemoteChange(listener: (change: ChangeMessage) => void): () => void`**
|
|
368
|
+
|
|
369
|
+
Called when a remote change is applied to the local database.
|
|
370
|
+
|
|
371
|
+
**`onSnapshotComplete(listener: (streamId: string) => void): () => void`**
|
|
372
|
+
|
|
373
|
+
Called when a snapshot stream completes.
|
|
374
|
+
|
|
375
|
+
**`onJobUpdate(listener: (update: JobUpdateMessage) => void): () => void`**
|
|
376
|
+
|
|
377
|
+
Called when a job update is received (e.g., from ECS tasks).
|
|
378
|
+
|
|
379
|
+
**`onLivestream(listener: (message: LivestreamMessage) => void): () => void`**
|
|
380
|
+
|
|
381
|
+
Called when a livestream event is received (started/stopped).
|
|
382
|
+
|
|
383
|
+
**`onConversation(listener: (message: ConversationMessage) => void): () => void`**
|
|
384
|
+
|
|
385
|
+
Called when a conversation event is received (user presence, messages).
|
|
386
|
+
|
|
387
|
+
**`onSyncReadyStateChange(listener: (state: SyncReadyState) => void): () => void`**
|
|
388
|
+
|
|
389
|
+
Called when the sync ready state changes.
|
|
390
|
+
|
|
391
|
+
States:
|
|
392
|
+
- `WAITING_FOR_HELLO`: Waiting for initial hello reply
|
|
393
|
+
- `APPLYING_MIGRATIONS`: Applying schema migrations
|
|
394
|
+
- `READY`: Ready to stream snapshots and sync data
|
|
395
|
+
|
|
396
|
+
**`onStateChange(listener: (state: ConnectionState) => void): () => void`**
|
|
397
|
+
|
|
398
|
+
Called when the WebSocket connection state changes.
|
|
399
|
+
|
|
400
|
+
States:
|
|
401
|
+
- `DISCONNECTED`: Not connected
|
|
402
|
+
- `CONNECTING`: Connecting to server
|
|
403
|
+
- `CONNECTED`: Connected and ready
|
|
404
|
+
- `RECONNECTING`: Attempting to reconnect
|
|
405
|
+
- `FAILED`: Connection failed
|
|
406
|
+
|
|
407
|
+
#### Properties
|
|
408
|
+
|
|
409
|
+
**`connectionState: ConnectionState`** (readonly)
|
|
410
|
+
|
|
411
|
+
Current connection state.
|
|
412
|
+
|
|
413
|
+
**`isReady: boolean`** (readonly)
|
|
414
|
+
|
|
415
|
+
Whether the client is ready to stream snapshots (true when ready state is READY).
|
|
416
|
+
|
|
417
|
+
**`currentReadyState: SyncReadyState`** (readonly)
|
|
418
|
+
|
|
419
|
+
Current sync ready state.
|
|
420
|
+
|
|
421
|
+
#### Disposal
|
|
422
|
+
|
|
423
|
+
**`async dispose(): Promise<void>`**
|
|
424
|
+
|
|
425
|
+
Clean up resources and disconnect.
|
|
426
|
+
|
|
427
|
+
## Advanced Usage
|
|
428
|
+
|
|
429
|
+
### Conflict Resolution
|
|
430
|
+
|
|
431
|
+
Handle conflicts when local and remote changes affect the same row:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
syncClient.setConflictResolver(async (local, remote) => {
|
|
435
|
+
// Last-write-wins strategy
|
|
436
|
+
if (remote.timestamp! > local.timestamp!) {
|
|
437
|
+
return remote;
|
|
438
|
+
}
|
|
439
|
+
return local;
|
|
440
|
+
|
|
441
|
+
// Or custom merge logic
|
|
442
|
+
// return {
|
|
443
|
+
// ...remote,
|
|
444
|
+
// data: {
|
|
445
|
+
// ...local.data,
|
|
446
|
+
// ...remote.data,
|
|
447
|
+
// }
|
|
448
|
+
// };
|
|
449
|
+
|
|
450
|
+
// Or skip the change
|
|
451
|
+
// return null;
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Incremental Snapshots
|
|
456
|
+
|
|
457
|
+
For large datasets, use incremental snapshots to only fetch new data:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// First sync - get all data
|
|
461
|
+
await syncClient.streamSnapshot(['users', 'posts']);
|
|
462
|
+
|
|
463
|
+
// Later syncs - only get changes
|
|
464
|
+
await syncClient.streamSnapshot(['users', 'posts'], {
|
|
465
|
+
incremental: true
|
|
466
|
+
});
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Custom Channel Messages
|
|
470
|
+
|
|
471
|
+
Send custom messages to your Phoenix channels:
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
const response = await syncClient.sendMessage('custom_event', {
|
|
475
|
+
key: 'value',
|
|
476
|
+
data: { foo: 'bar' }
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
console.log('Server response:', response);
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Multiple Channels
|
|
483
|
+
|
|
484
|
+
Join multiple channels for different data scopes:
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
const syncClient = new SyncClient({
|
|
488
|
+
// ...
|
|
489
|
+
initialChannels: [
|
|
490
|
+
{ channelName: 'user', channelId: userId },
|
|
491
|
+
{ channelName: 'tribe', channelId: tribeId },
|
|
492
|
+
{ channelName: 'guild', channelId: guildId },
|
|
493
|
+
],
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Or join dynamically
|
|
497
|
+
await syncClient.joinChannel({
|
|
498
|
+
channelName: 'party',
|
|
499
|
+
channelId: partyId,
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Server-Side Integration
|
|
504
|
+
|
|
505
|
+
This library is designed to work with an Elixir Phoenix server using Phoenix Channels. Your server should implement:
|
|
506
|
+
|
|
507
|
+
1. **Channel authentication** - Verify tokens on channel join
|
|
508
|
+
2. **Change broadcasting** - Broadcast changes to relevant channels
|
|
509
|
+
3. **Snapshot streaming** - Stream table data in batches
|
|
510
|
+
4. **Schema migrations** - Manage and distribute schema versions
|
|
511
|
+
|
|
512
|
+
Example Phoenix channel (Elixir):
|
|
513
|
+
|
|
514
|
+
```elixir
|
|
515
|
+
defmodule MyAppWeb.SyncChannel do
|
|
516
|
+
use Phoenix.Channel
|
|
517
|
+
|
|
518
|
+
def join("sync:user:" <> user_id, _params, socket) do
|
|
519
|
+
# Authenticate and authorize
|
|
520
|
+
{:ok, socket}
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def handle_in("hello", %{"schema_version" => version}, socket) do
|
|
524
|
+
# Check schema version and send migrations if needed
|
|
525
|
+
{:reply, {:ok, %{status: "up_to_date"}}, socket}
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def handle_in("changes_batch", %{"changes" => changes}, socket) do
|
|
529
|
+
# Process and broadcast changes
|
|
530
|
+
Enum.each(changes, fn change ->
|
|
531
|
+
broadcast!(socket, "change", change)
|
|
532
|
+
end)
|
|
533
|
+
{:reply, {:ok, %{}}, socket}
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def handle_in("stream_snapshot", %{"tables" => tables}, socket) do
|
|
537
|
+
# Stream table data in batches
|
|
538
|
+
Task.start(fn ->
|
|
539
|
+
Enum.each(tables, fn table ->
|
|
540
|
+
stream_table_data(socket, table)
|
|
541
|
+
end)
|
|
542
|
+
end)
|
|
543
|
+
{:reply, {:ok, %{}}, socket}
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## Troubleshooting
|
|
549
|
+
|
|
550
|
+
### Connection Issues
|
|
551
|
+
|
|
552
|
+
If you're having trouble connecting:
|
|
553
|
+
|
|
554
|
+
1. Check WebSocket URL is correct (should start with `wss://` or `ws://`)
|
|
555
|
+
2. Verify authentication token is valid
|
|
556
|
+
3. Check server logs for connection errors
|
|
557
|
+
4. Ensure CORS is properly configured on your server
|
|
558
|
+
|
|
559
|
+
### Changes Not Syncing
|
|
560
|
+
|
|
561
|
+
If changes aren't syncing:
|
|
562
|
+
|
|
563
|
+
1. Check connection state: `syncClient.connectionState`
|
|
564
|
+
2. Verify channels are joined: `syncClient.getJoinedChannels()`
|
|
565
|
+
3. Check for errors in browser console
|
|
566
|
+
4. Verify `pushInterval` is set (or call `sync()` manually)
|
|
567
|
+
|
|
568
|
+
### Schema Mismatch
|
|
569
|
+
|
|
570
|
+
If you get schema version errors:
|
|
571
|
+
|
|
572
|
+
1. The client will automatically apply migrations from the server
|
|
573
|
+
2. Listen to `onSyncReadyStateChange` to track migration progress
|
|
574
|
+
3. Ensure your server is sending proper migration SQL
|
|
575
|
+
|
|
576
|
+
## License
|
|
577
|
+
|
|
578
|
+
MIT
|