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
│ │ │ clientsAll 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 updatesNo manual reconnection logic needed.
Heartbeat
// Every 15 seconds
client → server: ping
server → client: pong
// If no pong after 30s:
// Client considers connection dead
// Triggers reconnectionEnsures 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 refreshPresence 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 instantlyPerformance
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 confirmedAll 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 refusedToken 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>API Reference: SyncServer · RedisBroadcaster · WebSocket Protocol
Next Steps
- Learn about Delta Sync - HTTP-based catch-up
- Server Real-Time Setup - Configure WebSocket
- Server Broadcasting - Multi-server setup
- Sync Protocol - Wire protocol details