Skip to Content
AdvancedConflict Resolution

Conflict Resolution

Handle conflicts when optimistic updates clash with server state.

Default: Last-Write-Wins

Gluonic’s default strategy is simple:

// Client has optimistic update post.title = 'Client Version' // Server sends different value frame: { t: 'post', id: '123', p: { title: 'Server Version' } } // Result: Server wins post.title = 'Server Version' // Client's change discarded

When it works well:

  • Single-user scenarios
  • Non-collaborative editing
  • Server is authoritative

When it’s problematic:

  • Collaborative editing (Google Docs-style)
  • Offline editing by multiple users
  • Long-running offline sessions

Custom Strategies

Client-Wins

Client’s optimistic update always wins:

const { store } = createSyncClient({ conflictResolution: { strategy: 'client-wins' } }) // Client: title = 'Client Version' // Server: title = 'Server Version' // Result: title = 'Client Version' (client wins)

Use cases:

  • Draft/local-only mode
  • User preferences (client is source of truth)
  • Offline-first editing

Server-Wins (Default)

Server always wins:

conflictResolution: { strategy: 'server-wins' // Default }

Field-Level Merge

Merge non-conflicting fields:

conflictResolution: { strategy: 'merge', mergeFunction: (client, server, field) => { // Non-conflicting: use client (optimistic) if (!(field in server)) { return client[field] } // Conflicting: use server return server[field] } }

Custom Per-Model

Different strategies for different models:

conflictResolution: { strategy: 'custom', resolver: { post: (client, server, field) => { // Draft status: client wins if (field === 'draft') return client.draft // Title: server wins if (field === 'title') return server.title // Content: merge if possible if (field === 'content') { return mergeText(client.content, server.content) } // Default: server wins return server[field] }, comment: 'server-wins', // Simple string for default user: (client, server, field) => { // User preferences: client wins if (field === 'theme' || field === 'language') { return client[field] } // Everything else: server wins return server[field] } } }

Operational Transformation (Advanced)

For collaborative editing:

import { createOTResolver } from '@gluonic/client/conflict-resolution' conflictResolution: { strategy: 'custom', resolver: { document: createOTResolver({ field: 'content', algorithm: 'diff-match-patch' // or 'yjs', 'automerge' }) } }

How OT works:

// Client applies operation: insert "new " at position 0 clientOp: { type: 'insert', pos: 0, text: 'new ' } content: "new Hello world" // Client's view // Server applies operation: insert "!" at position 11 serverOp: { type: 'insert', pos: 11, text: '!' } content: "Hello world!" // Server's view // OT transforms operations: // - Client's op: insert "new " at 0 // - Server's op (transformed): insert "!" at 15 (adjusted for "new ") // Final merged result: content: "new Hello world!" // Both changes applied ✓

Conflict Notification

Notify users when conflicts occur:

const { store } = createSyncClient({ onConflict: (conflict) => { // conflict: { model, id, fields, clientValue, serverValue } toast.warning( `Your changes to ${conflict.fields.join(', ')} were overwritten` ) // Log for debugging console.log('Conflict detected:', conflict) } })

Timestamp-Based Resolution

Last-write-wins based on timestamps:

conflictResolution: { strategy: 'custom', resolver: (client, server, field) => { const clientTime = client._modifiedAt || 0 const serverTime = server._modifiedAt || 0 // Newest wins return clientTime > serverTime ? client[field] : server[field] } }

Requires: Add _modifiedAt timestamp to mutations.

Version Vectors (Multi-Device)

Track edits per device:

// Each device has unique ID deviceId: 'device-abc' // Version vector tracks last-seen version from each device versionVector: { 'device-abc': 5, // Seen up to version 5 from device-abc 'device-def': 3 // Seen up to version 3 from device-def } // Conflict resolution uses vector clocks // Can determine causal ordering // Resolves conflicts intelligently

Implementation: Coming in v0.3 (experimental).

Best Practices

DO ✅

  • Use server-wins for most cases (simple, safe)
  • Notify users when conflicts occur
  • Log conflicts for debugging
  • Use field-level strategies for user preferences
  • Consider OT for collaborative editing

DON’T ❌

  • Silently discard user changes (always notify)
  • Use client-wins for shared data (causes divergence)
  • Over-complicate (start simple)
  • Forget to test conflict scenarios

Testing Conflicts

// Simulate conflict in tests test('handles title conflict', async () => { // 1. Client makes optimistic update await store.save('post', '123', { title: 'Client Title' }) // 2. Simulate server frame with different value await store.applyFrames([{ sid: 100, t: 'post', id: '123', op: 'u', p: { title: 'Server Title' } }]) // 3. Verify resolution const post = bridge.getModel<Post>('post', '123') expect(post.title).toBe('Server Title') // Server wins })

Next Steps

Last updated on