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 discardedWhen 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 intelligentlyImplementation: 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
- Middleware Hooks - Lifecycle events
- Optimistic Updates - How conflicts arise
- Testing - Test conflict scenarios
Last updated on