canvas: authoritative full-state snapshot sync#52
Merged
Conversation
Real-time collaboration was unreliable — users drifted out of sync and stayed that way, and the agent looked out of sync too. Root causes were all in the op-stream design: the client hydrated once then trusted a lossy op stream (sends drop while the socket is down, no re-sync on reconnect), and the remote-add playback queue silently dropped any update/delete targeting a shape still queued (so committed text never propagated, just the "Text" placeholder). Replace the op-broadcast model with an authoritative server that broadcasts the entire board as a coalesced `canvas.state` snapshot on every change and on join. Clients render the snapshot directly, so a missed message, reconnect, or race can never leave anyone diverged — the next snapshot is the truth and overwrites them. Server: - scheduleCanvasBroadcast: coalesced (40ms trailing) full-state snapshot to the project room; reads the latest doc at fire time. - applyCanvasOpAndBroadcast applies+persists then schedules a snapshot (agent path unchanged — same function). - join pushes the full snapshot immediately (self-healing reconnect). Client: - render = serverShapes (latest snapshot) + a thin local override layer for the shape you're actively manipulating or just committed. - overrides retire once a snapshot confirms them (field-equal), with an 800ms TTL backstop so a concurrent same-shape edit can't wedge one. - deletes the pending-add queue, applyOpToShapes-for-remote, echo-origin filtering, and the reveal/flash/entering animations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Canvas real-time collaboration was unreliable — users drifted out of sync and stayed that way, and the agent looked out of sync too. Root causes, all in the op-stream design:
OPEN, and nothing re-synced on reconnect (unlike chat,handleJoinnever pushed canvas state). Any disconnect blip left a client permanently diverged.update/deletetargeting a shape whoseaddwas still queued hitapplyOpToShapesas a no-op and was lost — so a committed text edit never propagated and the shape revealed with its stale"Text"placeholder. Same hole affected moving/recoloring/deleting just-created shapes, and the agent's create-then-set sequences.Fix — commands in, snapshots out
Replace the op-broadcast model with an authoritative server that broadcasts the entire board as a coalesced
canvas.statesnapshot on every change and on join. Clients render the snapshot directly, so a missed message, reconnect, or race can never leave anyone diverged — the next snapshot is the truth and overwrites them.Server (
ws.ts)scheduleCanvasBroadcast: coalesced (40 ms trailing) full-state snapshot to the project room; reads the latest doc at fire time, so a burst collapses into one broadcast.applyCanvasOpAndBroadcastapplies + persists, then schedules a snapshot. Agent path unchanged (same function).Client (
CanvasPage.tsx)serverShapes(latest snapshot) + a thin local override layer for the shape you're actively manipulating or just committed (keeps your own drag/type instant).applyOpToShapes-for-remote, echo-origin filtering, and the reveal/flash/entering animations.Tradeoffs / notes
projectRooms) unchanged — fine for one process; would need pub/sub to scale horizontally.Verification
tsc --noEmitclean andbun build.tsgreen. Not yet exercised with two live clients.