| name | realtime-realtime-sync |
| description | Use real-time sync patterns when you need: |
Real-time Data Synchronization
Scope: Conflict resolution, CRDTs, operational transformation, and eventual consistency patterns
Lines: 398
Last Updated: 2025-10-18
When to Use This Skill
Use real-time sync patterns when you need:
- Collaborative editing: Multiple users editing the same document simultaneously
- Offline-first apps: Apps that work offline and sync when reconnected
- Conflict resolution: Merging concurrent updates without data loss
- Eventual consistency: Distributed data that converges to same state
- Real-time collaboration: Shared whiteboards, multiplayer games, collaborative design
Don't use when:
- Single user editing with simple save/load (use optimistic updates)
- No concurrent edits possible (use last-write-wins)
- Strong consistency required (use pessimistic locking)
- Data is immutable/append-only (use event sourcing)
Core Concepts
Conflict Resolution Strategies
1. Last-Write-Wins (LWW)
- Simplest approach
- Use timestamp or sequence number
- Data loss possible
2. Operational Transformation (OT)
- Transform operations based on concurrent ops
- Complex but preserves intent
- Used in Google Docs
3. Conflict-Free Replicated Data Types (CRDTs)
- Mathematical guarantees of convergence
- No conflict resolution needed
- Used in distributed systems
4. Three-Way Merge
- Compare: base, local, remote
- Identify conflicts explicitly
- User resolves ambiguities
Synchronization Patterns
Client A Server Client B
| | |
|-- Update A ---->| |
| |-- Broadcast -->|
| | |
|<-- Update B ----|<-- Update B ---|
| | |
|-- Resolve ----->| |
|<-- Merged ------|-- Merged ----->|
Patterns
1. Last-Write-Wins (LWW) with Vector Clocks
interface VersionedData<T> {
data: T;
version: number;
timestamp: number;
clientId: string;
}
class LWWStore<T> {
private data: VersionedData<T> | null = null;
update(newData: T, clientId: string): VersionedData<T> {
const version = (this.data?.version ?? 0) + 1;
const timestamp = Date.now();
this.data = {
data: newData,
version,
timestamp,
clientId,
};
return this.data;
}
merge(remote: VersionedData<T>): boolean {
if (!this.data) {
this.data = remote;
return true;
}
if (remote.timestamp > ..) {
. = remote;
;
} (remote. === ..) {
(remote. > ..) {
. = remote;
;
}
}
;
}
(): T | {
.?. ?? ;
}
}
store = <>();
store.(, );
remoteData = {
: ,
: ,
: .() + ,
: ,
};
updated = store.(remoteData);
.(updated);
.(store.());
2. CRDT: Last-Write-Wins Register
interface LWWRegister<T> {
value: T;
timestamp: number;
clientId: string;
}
class LWWRegisterCRDT<T> {
private register: LWWRegister<T>;
constructor(initialValue: T, clientId: string) {
this.register = {
value: initialValue,
timestamp: Date.now(),
clientId,
};
}
set(value: T, clientId: string): LWWRegister<T> {
this.register = {
value,
timestamp: Date.now(),
clientId,
};
return this.register;
}
merge(other: LWWRegister<T>): boolean {
if (other.timestamp > this.register.timestamp ||
(other.timestamp === this.register.timestamp &&
other. > ..)) {
. = other;
;
}
;
}
(): T {
..;
}
(): <T> {
{ .... };
}
}
3. CRDT: Grow-Only Set (G-Set)
class GSet<T> {
private elements: Set<T> = new Set();
add(element: T): boolean {
if (this.elements.has(element)) {
return false;
}
this.elements.add(element);
return true;
}
has(element: T): boolean {
return this.elements.has(element);
}
merge(other: GSet<T>): void {
other.elements.forEach((element) => {
this.elements.add(element);
});
}
toArray(): T[] {
return Array.from(this.elements);
}
getState(): T[] {
return this.toArray();
}
}
set1 = <>();
set1.();
set1.();
set2 = <>();
set2.();
set2.();
set1.(set2);
.(set1.());
4. CRDT: Two-Phase Set (2P-Set)
class TwoPhaseSet<T> {
private addSet: Set<T> = new Set();
private removeSet: Set<T> = new Set();
add(element: T): boolean {
if (this.removeSet.has(element)) {
return false;
}
this.addSet.add(element);
return true;
}
remove(element: T): boolean {
if (!this.addSet.has(element)) {
return false;
}
this.removeSet.add(element);
return true;
}
has(element: T): boolean {
return this.addSet.has(element) && !this..(element);
}
(: <T>): {
other..( ..(element));
other..( ..(element));
}
(): T[] {
.(.).( !..(element));
}
() {
{
: .(.),
: .(.),
};
}
}
5. CRDT: Counter (G-Counter and PN-Counter)
class GCounter {
private counts: Map<string, number> = new Map();
increment(clientId: string, amount: number = 1): void {
const current = this.counts.get(clientId) ?? 0;
this.counts.set(clientId, current + amount);
}
value(): number {
let total = 0;
this.counts.forEach((count) => {
total += count;
});
return total;
}
merge(other: GCounter): void {
other.counts.forEach((count, clientId) => {
const current = this.counts.get(clientId) ?? 0;
this.counts.set(clientId, .(current, count));
});
}
(): <, > {
(.);
}
}
{
: = ();
: = ();
(: , : = ): {
..(clientId, amount);
}
(: , : = ): {
..(clientId, amount);
}
(): {
..() - ..();
}
(: ): {
..(other.);
..(other.);
}
}
counter = ();
counter.(, );
counter.(, );
.(counter.());
6. Operational Transformation (Simple Text)
interface Operation {
type: 'insert' | 'delete';
position: number;
content?: string;
length?: number;
}
class OperationalTransform {
static transform(op1: Operation, op2: Operation): Operation {
if (op1.type === 'insert' && op2.type === 'insert') {
if (op1.position < op2.position) {
return {
...op2,
position: op2.position + (op1.content?.length ?? 0),
};
} else if (op1.position > op2.position) {
return op2;
} else {
return {
...op2,
position: op2.position + (op1.content?.length ?? ),
};
}
}
(op1. === && op2. === ) {
(op2. <= op1.) {
op2;
} (op2. >= op1. + (op1. ?? )) {
{
...op2,
: op2. - (op1. ?? ),
};
} {
{
...op2,
: op1.,
};
}
}
(op1. === && op2. === ) {
(op2. < op1.) {
op2;
} (op2. >= op1. + (op1.?. ?? )) {
{
...op2,
: op2. + (op1.?. ?? ),
};
} {
{
...op2,
: op1.,
: (op2. ?? ) + (op1.?. ?? ),
};
}
}
(op1. === && op2. === ) {
(op2. < op1.) {
op2;
} (op2. >= op1. + (op1. ?? )) {
{
...op2,
: op2. - (op1. ?? ),
};
} {
{
...op2,
: op1.,
: .(, (op2. ?? ) - (op1. ?? )),
};
}
}
op2;
}
(: , : ): {
(op. === ) {
text.(, op.) + op. + text.(op.);
} (op. === ) {
text.(, op.) + text.(op. + (op. ?? ));
}
text;
}
}
text = ;
: = { : , : , : };
: = { : , : , : };
textA = .(text, op1);
textB = .(text, op2);
op2Transformed = .(op1, op2);
textA = .(textA, op2Transformed);
op1Transformed = .(op2, op1);
textB = .(textB, op1Transformed);
.(textA === textB);
7. Sync Manager with WebSocket
interface SyncMessage {
type: 'update' | 'sync_request' | 'sync_response';
clientId: string;
data?: any;
version?: number;
}
class SyncManager<T> {
private ws: WebSocket;
private clientId: string;
private crdt: LWWRegisterCRDT<T>;
private onUpdate: (data: T) => void;
constructor(
url: string,
initialData: T,
onUpdate: (data: T) => void
) {
this.clientId = this.generateClientId();
this.crdt = new LWWRegisterCRDT(initialData, this.clientId);
this.onUpdate = onUpdate;
this.ws = new WebSocket(url);
this.();
}
() {
.. = {
.();
.();
};
.. = {
: = .(event.);
.(message);
};
}
() {
(message.) {
:
(message. && message. !== .) {
updated = ..(message.);
(updated) {
.(..());
}
}
;
:
(message.) {
..(message.);
.(..());
}
;
}
}
() {
..(data, .);
.();
.(..());
}
() {
: = {
: ,
: .,
: ..(),
};
..(.(message));
}
() {
: = {
: ,
: .,
};
..(.(message));
}
(): {
;
}
}
syncManager = (
,
,
{
.(, data);
}
);
syncManager.();
Quick Reference
CRDT Types
const register = new LWWRegisterCRDT('initial', 'client-1');
const gset = new GSet<string>();
const tpset = new TwoPhaseSet<string>();
const gcounter = new GCounter();
const pncounter = new PNCounter();
Conflict Resolution Strategies
if (remote.timestamp > local.timestamp) {
local = remote;
}
if (isGreaterThan(remote.vector, local.vector)) {
local = remote;
}
const merged = merge(base, local, remote);
State Synchronization
send({ type: 'full_sync', state: crdt.getState() });
send({ type: 'delta_sync', delta: crdt.getDelta() });
send({ type: 'operation', op: { type: 'insert', ... } });
Anti-Patterns
❌ Using Timestamps Without Tie-Breakers
if (remote.timestamp > local.timestamp) {
local = remote;
}
Why it's bad: Different clients may choose different values for concurrent updates
Better approach:
if (remote.timestamp > local.timestamp ||
(remote.timestamp === local.timestamp && remote.clientId > local.clientId)) {
local = remote;
}
❌ Sending Full State on Every Update
socket.send(JSON.stringify({ type: 'update', data: fullDocument }));
Why it's bad: Wastes bandwidth, especially for small changes
Better approach:
socket.send(JSON.stringify({ type: 'operation', op: { type: 'insert', position: 5, content: 'x' } }));
❌ No Conflict Detection
function update(data) {
this.data = data;
}
Why it's bad: Lost updates when concurrent modifications occur
Better approach:
function update(data, version) {
if (version <= this.version) {
throw new Error('Conflict detected');
}
this.data = data;
this.version = version;
}
❌ Synchronous Merge Operations
await merge(largeDocument);
Why it's bad: UI freezes during merge
Better approach:
worker.postMessage({ type: 'merge', data: largeDocument });
Related Skills
- websocket-implementation.md: WebSocket protocol for sync communication
- server-sent-events.md: SSE for server-to-client sync updates
- pubsub-patterns.md: Server-side message routing for collaborative features
- network-resilience-patterns.md: Handling network failures during sync
Last Updated: 2025-10-18
Format Version: 1.0 (Atomic)