Skip to Content

SyncFrame

Sync protocol frame format for delta sync and real-time updates.

Import

import type { SyncFrame } from '@gluonic/client'

Type Definition

Exact definition from implementation:

export type SyncFrame = { sid: number t: string id: string op: 'i' | 'u' | 'd' p?: Record<string, any> at?: number ctx?: { client_tx_id?: string } }

Fields

sid

sid: number

Description: Sync ID - monotonically increasing sequence number

Purpose: Track sync progress, enable delta sync

Example: 42, 1234

t

t: string

Description: Model type name (e.g., ‘task’, ‘user’, ‘post’)

Example: 'task', 'user'

id

id: string

Description: Unique model ID

Example: 'abc-123-def-456'

op

op: 'i' | 'u' | 'd'

Description: Operation type

Values:

  • 'i' - Insert (create)
  • 'u' - Update (modify)
  • 'd' - Delete (remove)

p

p?: Record<string, any>

Description: Payload data (properties)

Optional: Present for 'i' and 'u', absent for 'd'

Example: { title: 'Updated Task', done: true }

at

at?: number

Description: Timestamp (milliseconds since epoch)

Optional: Server-assigned operation time

Example: 1699564800000

ctx

ctx?: { client_tx_id?: string }

Description: Context metadata

Fields:

  • client_tx_id - Client transaction ID (for optimistic update matching)

Example: { client_tx_id: 'tx-123-456' }


Complete Examples

Insert Frame

const insertFrame: SyncFrame = { sid: 42, t: 'task', id: 'abc-123', op: 'i', p: { title: 'New Task', done: false, createdAt: 1699564800000 }, at: 1699564800000, ctx: { client_tx_id: 'tx-001' } }

Update Frame

const updateFrame: SyncFrame = { sid: 43, t: 'task', id: 'abc-123', op: 'u', p: { title: 'Updated Task', done: true }, at: 1699564900000 }

Delete Frame

const deleteFrame: SyncFrame = { sid: 44, t: 'task', id: 'abc-123', op: 'd', at: 1699565000000 }

Usage

Delta Sync

Client requests frames since last sync ID:

// Client has lastSyncId = 40 // Request delta GET /sync/v1/delta?since=40 // Server responds with frames 41, 42, 43... { sid: 43, frames: [ { sid: 41, t: 'task', id: '1', op: 'u', p: { done: true } }, { sid: 42, t: 'task', id: '2', op: 'i', p: { title: 'New' } }, { sid: 43, t: 'task', id: '3', op: 'd' } ] } // Client applies frames and updates lastSyncId to 43

Real-Time Updates

Frames pushed via WebSocket:

// Server broadcasts frame ws.send(JSON.stringify({ sid: 45, t: 'task', id: 'abc-123', op: 'u', p: { done: true } })) // Client receives and applies const frame: SyncFrame = JSON.parse(data) await store.applyFrames([frame])

Optimistic Updates

Client mutations include client_tx_id:

// Client creates task await store.create('task', 'abc-123', { title: 'New Task' }) // Queued with client_tx_id: 'tx-001' // Server confirms via frame { sid: 50, t: 'task', id: 'abc-123', op: 'i', p: { title: 'New Task', serverTimestamp: 1699564800000 }, ctx: { client_tx_id: 'tx-001' } } // Client matches client_tx_id, skips duplicate application

Frame Processing

Apply Frames

async applyFrames(frames: SyncFrame[]) { for (const frame of frames) { // Check if we originated this mutation const isOurs = frame.ctx?.client_tx_id && this.pendingTxIds.has(frame.ctx.client_tx_id) if (isOurs) { // Skip (already applied optimistically) // Just dequeue transaction await storage.dequeueTx(frame.ctx.client_tx_id) continue } // Apply frame to pool switch (frame.op) { case 'i': case 'u': pool.upsert({ t: frame.t, id: frame.id, p: frame.p }) break case 'd': pool.delete(frame.t, frame.id) break } // Update lastSyncId this.lastSyncId = Math.max(this.lastSyncId, frame.sid) } }

Why SyncFrame?

1. Delta Sync Efficiency

Only transmit changes since last sync:

// Instead of full snapshot (large) { rows: [/* all rows */] } // Send only changes (small) { frames: [ { sid: 41, t: 'task', id: '1', op: 'u', p: { done: true } } ]}

2. Operation Semantics

Clear intent for each change:

  • Insert: New object
  • Update: Modified fields
  • Delete: Removed object

3. Optimistic Update Matching

client_tx_id enables deduplication:

// Client applies optimistically with tx-001 // Server echoes tx-001 in frame // Client skips re-application (already done)

4. Real-Time Streaming

WebSocket can stream frames incrementally:

// Frame 1 { sid: 41, ... } // Frame 2 (immediately after) { sid: 42, ... }

Relationship to WireRow

SyncFrame is protocol format, WireRow is storage format:

// SyncFrame (from network) { sid: 42, t: 'task', id: 'abc-123', op: 'i', p: { title: 'New Task' } } // Convert to WireRow (for pool/storage) { t: 'task', id: 'abc-123', p: { title: 'New Task' } }

Differences:

  • SyncFrame has sid, op, at, ctx
  • WireRow has v (version)
  • SyncFrame transmits changes
  • WireRow stores state

See Also

Last updated on