- Remove localLamport and lastSyncedLamport from all interfaces and models - Simplify sync logic to use vector clocks exclusively for change tracking - Remove Lamport-based conflict detection and sync status logic - Update UI components to remove Lamport display elements - Clean up backwards compatibility utilities - Fix getVectorClock import error in sync.service.ts - Remove debug alert from sync-safety-backup.service.ts This simplifies the sync system by eliminating the dual tracking mechanism that was causing complications and false conflicts. Vector clocks provide better causality tracking for distributed sync.
7.1 KiB
Vector Clocks in Super Productivity Sync
Overview
Super Productivity uses vector clocks to provide accurate conflict detection and resolution in its synchronization system. This document explains how vector clocks work, why they're used, and how they integrate with the existing sync infrastructure.
Table of Contents
- What are Vector Clocks?
- Why Vector Clocks?
- Implementation Details
- Migration from Lamport Timestamps
- API Reference
- Examples
What are Vector Clocks?
A vector clock is a data structure used in distributed systems to determine the partial ordering of events and detect causality violations. Each client/device maintains its own component in the vector, incrementing it on local updates.
Structure
interface VectorClock {
[clientId: string]: number;
}
// Example:
{
"desktop_1234": 5,
"mobile_5678": 3,
"web_9012": 7
}
Comparison Results
Vector clocks can have four relationships:
- EQUAL: Same values for all components
- LESS_THAN: A happened before B (all components of A ≤ B)
- GREATER_THAN: B happened before A (all components of B ≤ A)
- CONCURRENT: Neither happened before the other (true conflict)
Why Vector Clocks?
Problem with Lamport Timestamps
Lamport timestamps provide a total ordering but can't distinguish between:
- Changes made after syncing (sequential)
- Changes made independently (concurrent)
This leads to false conflicts where user intervention is required even though one device is clearly ahead.
Benefits of Vector Clocks
- Accurate Conflict Detection: Only reports conflicts for truly concurrent changes
- Automatic Resolution: Can auto-merge when one vector dominates another
- Device Tracking: Maintains history of which device made which changes
- Reduced User Interruptions: Fewer false conflicts mean better UX
Implementation Details
File Structure
src/app/pfapi/api/
├── util/
│ ├── vector-clock.ts # Core vector clock operations
│ ├── backwards-compat.ts # Migration helpers
│ └── get-sync-status-from-meta-files.ts # Sync status detection
├── model-ctrl/
│ └── meta-model-ctrl.ts # Updates vector clocks on changes
└── sync/
└── sync.service.ts # Integrates vector clocks in sync flow
Core Operations
1. Increment on Local Change
// When user modifies data
const newVectorClock = incrementVectorClock(currentVectorClock, clientId);
2. Merge on Sync
// When downloading remote changes
const mergedClock = mergeVectorClocks(localVector, remoteVector);
3. Compare for Conflicts
const comparison = compareVectorClocks(localVector, remoteVector);
if (comparison === VectorClockComparison.CONCURRENT) {
// True conflict - user must resolve
}
Integration Points
- MetaModelCtrl: Increments vector clock on every local change
- SyncService: Merges vector clocks during download, includes in upload
- getSyncStatusFromMetaFiles: Uses vector clocks for conflict detection
Vector Clock Implementation
The system uses vector clocks exclusively for conflict detection:
How It Works
- Each client maintains its own counter in the vector clock
- Counters increment on local changes only
- Vector clocks are compared to detect concurrent changes
- No false conflicts from timestamp-based comparisons
Current Fields
| Field | Purpose |
|---|---|
vectorClock |
Track changes across all clients |
lastSyncedVectorClock |
Track last synced state |
API Reference
Core Functions
initializeVectorClock(clientId: string, initialValue?: number): VectorClock
Creates a new vector clock for a client.
compareVectorClocks(a: VectorClock, b: VectorClock): VectorClockComparison
Determines the relationship between two vector clocks.
incrementVectorClock(clock: VectorClock, clientId: string): VectorClock
Increments the client's component in the vector clock.
mergeVectorClocks(a: VectorClock, b: VectorClock): VectorClock
Merges two vector clocks by taking the maximum of each component.
hasVectorClockChanges(current: VectorClock, reference: VectorClock): boolean
Checks if current has any changes compared to reference.
Helper Functions
vectorClockToString(clock: VectorClock): string
Returns human-readable representation for debugging.
lamportToVectorClock(lamport: number, clientId: string): VectorClock
Converts Lamport timestamp to vector clock for migration.
Examples
Example 1: Simple Sequential Updates
// Device A makes a change
deviceA.vectorClock = { A: 1 };
// Device A syncs to cloud
cloud.vectorClock = { A: 1 };
// Device B downloads
deviceB.vectorClock = { A: 1 };
// Device B makes a change
deviceB.vectorClock = { A: 1, B: 1 };
// When A tries to sync, vector clock shows B is ahead
// Result: A downloads B's changes (no conflict)
Example 2: Concurrent Updates (True Conflict)
// Both devices start synced
deviceA.vectorClock = { A: 1, B: 1 };
deviceB.vectorClock = { A: 1, B: 1 };
// Both make changes before syncing
deviceA.vectorClock = { A: 2, B: 1 }; // A incremented
deviceB.vectorClock = { A: 1, B: 2 }; // B incremented
// Comparison shows CONCURRENT - neither dominates
// Result: User must resolve conflict
Example 3: Complex Multi-Device Scenario
// Three devices with different states
desktop.vectorClock = { desktop: 5, mobile: 3, web: 2 };
mobile.vectorClock = { desktop: 4, mobile: 3, web: 2 };
web.vectorClock = { desktop: 4, mobile: 3, web: 7 };
// Desktop vs Mobile: Desktop is ahead (5 > 4)
// Desktop vs Web: Concurrent (desktop has 5 vs 4, but web has 7 vs 2)
// Mobile vs Web: Web is ahead (7 > 2, everything else equal)
Debugging
Enable Verbose Logging
// In pfapi/api/util/log.ts, set log level to 2 or higher
pfLog(2, 'Vector clock comparison', {
localVector: vectorClockToString(localVector),
remoteVector: vectorClockToString(remoteVector),
result: comparison,
});
Common Issues
- Clock Drift: Ensure client IDs are stable and unique
- Migration Issues: Check both vector clock and Lamport fields during transition
- Overflow Protection: Clocks reset to 1 when approaching MAX_SAFE_INTEGER
Best Practices
- Always increment on local changes
- Always merge when receiving remote data
- Never modify vector clocks directly
- Use backwards-compat helpers during migration period
- Log vector states when debugging sync issues
Future Improvements
- Compression: Prune old client entries after inactivity
- Conflict Resolution: Add automatic resolution strategies
- Visualization: Add UI to show vector clock states
- Performance: Optimize comparison for many clients