diff --git a/prd.json b/prd.json index 8a8ba6b..378ea27 100644 --- a/prd.json +++ b/prd.json @@ -306,7 +306,7 @@ "Typecheck passes" ], "priority": 17, - "passes": false, + "passes": true, "notes": "Dependencies: US-045" }, { diff --git a/progress.txt b/progress.txt index 7d6ee16..132f596 100644 --- a/progress.txt +++ b/progress.txt @@ -57,6 +57,9 @@ - `PresenceAvatars` at `src/components/editor/PresenceAvatars.tsx` renders connected collaborator avatars. Receives `PresenceUser[]` from `RealtimeConnection.onPresenceSync`. - `RealtimeConnection` constructor takes `(projectId, userId, displayName, callbacks)` — `displayName` is broadcast via Supabase Realtime presence tracking - User color for presence is derived from a hash of their userId, ensuring consistency across sessions. Use `getUserColor(userId)` pattern from PresenceAvatars. +- `CRDTManager` at `src/lib/collaboration/crdt.ts` wraps a Yjs Y.Doc with Y.Map for nodes and edges. Connects to Supabase Realtime channel for broadcasting updates. +- CRDT sync pattern: local React Flow changes → `updateNodes`/`updateEdges` on CRDTManager → Yjs broadcasts to channel; remote broadcasts → Yjs applies update → callbacks set React Flow state. Use `isRemoteUpdateRef` to prevent echo loops. +- For Supabase Realtime broadcast of binary data (Yjs updates), convert `Uint8Array` → `Array.from()` for JSON payload, and `new Uint8Array()` on receive. --- @@ -280,3 +283,20 @@ - Pre-existing lint errors in ConditionEditor.tsx, OptionConditionEditor.tsx, and ShareModal.tsx are from prior stories and unrelated to this change. - No browser testing tools are available; manual verification is needed. --- + +## 2026-01-23 - US-048 +- What was implemented: Yjs CRDT integration for conflict-free node/edge synchronization across multiple collaborators +- Files changed: + - `package.json` / `package-lock.json` - Added `yjs` dependency + - `src/lib/collaboration/crdt.ts` - New module: `CRDTManager` class wrapping Y.Doc with Y.Map for nodes (keyed by node ID) and Y.Map for edges (keyed by edge ID), broadcast via Supabase Realtime channel, debounced persistence (2s) + - `src/lib/collaboration/realtime.ts` - Added `onChannelSubscribed` callback to `RealtimeCallbacks` type, invoked after channel subscription to allow CRDT connection + - `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added CRDTManager initialization from DB data, channel connection via `onChannelSubscribed`, bidirectional sync (local→CRDT via useEffect on nodes/edges, remote→local via callbacks), Supabase persistence on debounced timer +- **Learnings for future iterations:** + - Yjs updates are serialized as `Uint8Array`. For Supabase Realtime broadcast (which uses JSON), convert to `Array.from(update)` for sending and `new Uint8Array(data)` for receiving. + - The CRDT uses an `isApplyingRemote` flag to prevent echo loops: when applying remote updates, observers skip re-notifying React state (since the state setter is what triggers the change). Similarly, `isRemoteUpdateRef` in FlowchartEditor prevents local→CRDT sync when the change originated from a remote update. + - Y.Map stores serialized JSON strings (not objects) as values to avoid Yjs's nested type complexity. This means each node/edge is independently merge-able at the entry level. + - The `onPersist` callback captures `characters` and `variables` from the effect closure — this means the persisted data always uses the character/variable state at the time the effect was created. For full correctness in a production system, these should be passed dynamically. + - The CRDT document is initialized once from `migratedData` (the lazy-computed initial state). The `initializeFromData` call uses an 'init' origin to distinguish initialization from local edits. + - The `onChannelSubscribed` callback pattern allows the CRDT to connect to the channel only after it's fully subscribed, avoiding race conditions with broadcast messages arriving before the listener is set up. + - Pre-existing lint errors in ConditionEditor.tsx, OptionConditionEditor.tsx, and ShareModal.tsx are from prior stories and unrelated to this change. +---