Skip to Content
LearnCore ConceptsReal-Time Sync

Real-Time Sync

Concept: Changes propagate instantly to all connected clients via WebSocket push notifications.

What is Real-Time Sync?

Real-time sync enables instant updates when other users make changes. Instead of polling for changes, Gluonic uses WebSocket connections to push updates immediately.

Result: Changes appear in your UI within milliseconds of being made by another user.

How It Works

WebSocket Connection

Client A Server Client B │ │ │ ├─── Connect WS ──────>│ │ │ │<──── Connect WS ────┤ │ │ │ │ │ │ ├─── Make change ─────>│ │ │ (HTTP POST) │ │ │ │ │ │ ├─ Save to DB │ │ ├─ Write SyncAction │ │ │ │ │<──── Frame ──────────┤ │ │ (WebSocket) ├───── Frame ────────>│ │ │ (WebSocket) │ │ │ │ └─ UI updates │ UI updates ─┘

Both clients receive the update instantly via WebSocket.

Delta Sync + WebSocket

Gluonic uses BOTH mechanisms together:

WebSocket (Real-Time Push)

// Always-on connection ws.onmessage = (frames) => { store.applyFrames(frames) // Apply immediately // UI updates in real-time ✓ }

Purpose: Instant push notifications when connected

Delta Sync (Catch-Up)

// Periodic or on-reconnect const frames = await fetch(`/sync/v1/delta?since=${lastSyncId}`) store.applyFrames(frames)

Purpose: Catch up when WebSocket disconnected or missed frames

Together: Reliable + Real-time

  • WebSocket connected → instant updates
  • WebSocket disconnects → delta sync catches up
  • No data loss ✓

SyncFrame Format

Real-time updates are sent as SyncFrames:

interface SyncFrame { sid: number // Sync ID (monotonic sequence) t: string // Type name ('user', 'issue') id: string // Record ID op: 'i'|'u'|'d' // Operation (insert, update, delete) p?: any // Payload (for insert/update) who?: string // Actor ID (who made the change) at: number // Timestamp ctx?: { client_tx_id?: string // Original transaction ID } }

Example frame:

{ "sid": 1523, "t": "issue", "id": "issue-123", "op": "u", "p": { "title": "Updated Title" }, "who": "user-alice", "at": 1698765432000, "ctx": { "client_tx_id": "tx-789" } }

Automatic UI Updates

When a frame arrives, UI updates automatically:

// Component somewhere in your app const IssueRow = observer(({ issue }) => { return <div>{issue.title}</div> }) // Another user updates the issue // WebSocket frame arrives: { sid: 1523, t: 'issue', id: '123', op: 'u', p: { title: 'Updated by Alice' } } // Gluonic applies frame: store.pool.upsert({ t: 'issue', id: '123', p: { title: 'Updated by Alice' } }) // MobX detects pool change // IssueRow re-renders automatically // User sees: "Updated by Alice" ✓ // No code in your component needed!

Broadcasting with RedisBroadcaster

For multi-server deployments, use RedisBroadcaster:

import { SyncServer } from '@gluonic/server' import { RedisBroadcaster } from '@gluonic/broadcaster-redis' const server = SyncServer({ database, auth, broadcaster: RedisBroadcaster({ host: 'redis.example.com', port: 6379 }) })

How it works:

Pod 1 Redis Pod 2 │ │ │ ├─── Publish ────────>│ │ │ frame │ │ │ ├──── Broadcast ───>│ │ │ │ │ │ ├─ Send to │ │ │ connected │ │ │ clients

All clients receive updates regardless of which pod they’re connected to.

Connection Management

Auto-Reconnect

// WebSocket disconnects // Gluonic automatically: 1. Detects disconnection 2. Waits with exponential backoff 3. Reconnects WebSocket 4. Runs delta sync to catch up 5. Resumes real-time updates

No manual reconnection logic needed.

Heartbeat

// Every 15 seconds client → server: ping server → client: pong // If no pong after 30s: // Client considers connection dead // Triggers reconnection

Ensures stale connections are detected and reconnected.

Client-Side API

Connection State

const SyncIndicator = observer(() => { const { isOnline, isSyncing } = useConnectionState() if (!isOnline) return <Badge>Offline</Badge> if (isSyncing) return <Badge>Syncing...</Badge> return <Badge>Connected</Badge> })

Manual Sync

const SyncButton = () => { const store = useStore() const handleSync = async () => { await store.catchUp() // Delta sync + WebSocket reconnect } return <button onClick={handleSync}>Sync Now</button> }

Real-Time Features

Collaborative Editing

// Multiple users editing same issue const IssueEditor = observer(({ issue }) => { const store = useStore() const handleSave = async () => { await store.save('issue', issue.id, { title: issue.title }) } return ( <div> <input value={issue.title} onChange={e => issue.title = e.target.value} /> <button onClick={handleSave}>Save</button> {/* Shows who's editing */} {issue.lastEditedBy && ( <span className='text-sm opacity-70'> Last edited by {issue.lastEditedBy} </span> )} </div> ) }) // When another user saves, your UI updates automatically // No polling, no manual refresh

Presence Indicators

const IssueDetail = observer(({ issue }) => { const viewers = useCollectionModels<Viewer>('viewer', v => v.issueId === issue.id ) return ( <div> <h1>{issue.title}</h1> <div className='viewers'> {viewers.map(v => ( <Avatar key={v.userId} user={v.user.value} /> ))} </div> </div> ) }) // When another user opens the issue, their viewer record syncs instantly // Avatar appears in real-time ✓

Live Updates

const TaskBoard = observer(() => { const tasks = useCollectionModels<Task>('task') // Tasks update live as teammates work return ( <div className='grid'> {['todo', 'in-progress', 'done'].map(status => ( <Column key={status}> {tasks .filter(t => t.status === status) .map(task => <TaskCard key={task.id} task={task} />)} </Column> ))} </div> ) }) // As tasks move between columns (by any user), UI updates instantly

Performance

Efficient Frame Application

// Gluonic batches frames for efficient updates const frames = [ { sid: 1001, t: 'issue', id: '1', op: 'u', p: { title: 'A' } }, { sid: 1002, t: 'issue', id: '2', op: 'u', p: { title: 'B' } }, { sid: 1003, t: 'issue', id: '3', op: 'u', p: { title: 'C' } } ] // MobX batches pool updates // Components re-render ONCE (not 3 times) ✓

Backpressure Control

// Server configuration const server = SyncServer({ database, auth, deltaBackpressure: { minIntervalMs: 100, // Min 100ms between delta requests maxConcurrencyPerOrg: 5 // Max 5 concurrent per org } })

Prevents clients from overwhelming server with delta requests.

Offline → Online Transition

When coming back online, Gluonic seamlessly transitions:

User offline for 1 hour Edits 5 issues (queued) Connection returns 1. Reconnect WebSocket 2. Run delta sync to catch up (loads 100 changes from teammates) 3. Process transaction queue (send 5 edits) 4. Resume real-time updates User sees: All teammate changes + their edits confirmed

All automatic - no manual sync management.

Conflict Handling

Real-time updates can conflict with local optimistic changes:

// Local optimistic update store.save('issue', '123', { title: 'My Version' }) // Pool: { title: 'My Version' } (optimistic) // WebSocket frame arrives (from another user) { sid: 1523, t: 'issue', id: '123', op: 'u', p: { title: 'Their Version' } } // Gluonic behavior: // Server version wins (last-write-wins) // Pool: { title: 'Their Version' } (overwrites optimistic) // UI updates to server version // Optionally notify user: store.onConflict((type, id, fields) => { toast.warning(`Your changes to ${fields.join(', ')} were overwritten`) })

WebSocket Authentication

// WebSocket connects with token in query param const ws = new WebSocket(`wss://api.example.com/sync/v1/ws?token=${token}`) // Server verifies token before accepting connection // Invalid token → connection refused

Token changes trigger reconnection:

// User logs out <SyncProvider client={client} token={null}> {/* WebSocket disconnects, pool cleared */} </SyncProvider> // User logs in <SyncProvider client={client} token={newToken}> {/* WebSocket reconnects with new token, data reloads */} </SyncProvider>

Next Steps

Last updated on