Context
Structural operations (node create/move/delete, edge create/delete) currently use simple last-write-wins with no formal ordering. The server relays messages in arrival order without conflict detection. Sim uses client-generated timestamps with deterministic tiebreaking for structural conflict resolution.
Text content already has proper CRDT conflict resolution (RGA). This ticket is about the structural layer only.
What to build
Client-side: Timestamp every operation
- Every outbound structural operation includes a
timestamp field (high-resolution client clock, e.g., performance.now() + wall clock)
- Format: ISO 8601 with millisecond precision, or monotonic counter
Server-side: Timestamp-based ordering
- Position conflicts: When two users simultaneously move the same node, persist the one with the latest timestamp. Broadcast both in sequence — clients converge to the latest.
- Create/delete conflicts: If a delete arrives for an entity that was modified after the delete's timestamp, the modification wins (the entity survives). If the delete is newer, it wins.
Deterministic tiebreaking
- When timestamps are identical (within precision), use
userId lexicographic ordering as tiebreaker
- Ensures all clients converge to the same state given the same set of ops
Server-side coalescing for position
- Position updates within a ~25ms window for the same node: only persist the latest timestamp
- Reduces DB churn during collaborative dragging
Current code references
frontend/src/shared/events.ts — outbound event types (no timestamp field today)
services/collab/src/main/java/com/leetdoodle/collab/handler/CanvasWebSocketHandler.java — relay logic (no conflict detection)
Acceptance criteria
Context
Structural operations (node create/move/delete, edge create/delete) currently use simple last-write-wins with no formal ordering. The server relays messages in arrival order without conflict detection. Sim uses client-generated timestamps with deterministic tiebreaking for structural conflict resolution.
Text content already has proper CRDT conflict resolution (RGA). This ticket is about the structural layer only.
What to build
Client-side: Timestamp every operation
timestampfield (high-resolution client clock, e.g.,performance.now()+ wall clock)Server-side: Timestamp-based ordering
Deterministic tiebreaking
userIdlexicographic ordering as tiebreakerServer-side coalescing for position
Current code references
frontend/src/shared/events.ts— outbound event types (no timestamp field today)services/collab/src/main/java/com/leetdoodle/collab/handler/CanvasWebSocketHandler.java— relay logic (no conflict detection)Acceptance criteria