fix(collab): stop content duplication + fix false unreachable probe#186
Merged
Conversation
Two runtime bugs surfaced now that beta sets NEXT_PUBLIC_YJS_WS_URL.
BUG 1 — false "Live: unreachable" pill. The Phase-A connectivity probe
opened `new WebSocket(url)` against the BARE configured URL
(wss://<host>/<token>, no room). The worker reads the last path segment
as the room and the one before it as the auth token, so a single-segment
path has no token -> 403 -> the probe reported the server unreachable
even though the real document binding (which dials <url>/<room>)
authenticates fine. Fix: the probe now dials a dedicated `__probe__`
room (buildProbeUrl) so it hits the `/<token>/<room>` shape the worker
requires. Worker auth is untouched.
BUG 2 — content DOUBLING (data integrity). The editor was created with
value={initialContent} AND the collab binding seeded the same body into
the Y.Text. yCollab's ySync plugin only registers an observer; it never
reconciles the editor's initial doc against the (empty) Y.Text, so the
post-sync seed replayed the body on top of the existing text -> the body
rendered twice and the doubled text was saved back to the note (and would
sync to GitHub). Fix: when collab is enabled the editor is built EMPTY
and the Y.Text is the single content source. The seeder sees its body
once; a joiner receives the shared body over the wire once. With collab
off the editor is byte-for-byte unchanged.
Tests: probe builds a /<token>/<room> URL (not the bare url); a real
EditorView + yCollab binding holds the note body exactly once after
binding for both seeder and joiner (collabNoDoubling), with a regression
guard documenting that initializing the editor WITH the body doubles it.
Verified end-to-end with two Playwright browser contexts on the same
room (e2e/_verify_collab): pill "Live: on", body once in both clients,
live A->B sync, single saved content, no CSP/WS console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Two runtime bugs in the live-collaboration feature, exposed now that beta sets
NEXT_PUBLIC_YJS_WS_URL.BUG 1 — false "Live: unreachable" pill (
src/hooks/useCollaboration.ts)Root cause. The Phase-A connectivity probe opened
new WebSocket(url)against the bare configured URL (wss://<host>/<token>, no room). The Cloudflare worker (collab-server/src/index.ts) reads the last path segment as the room and the second-to-last as the auth token. A single-segment path therefore has no token -> the worker returns 403 -> the probe reported the server unreachable, even though the real document binding (which dials<url>/<room>viaWebsocketProvider) authenticates fine.Fix. The probe now dials a dedicated
__probe__room (buildProbeUrl), so it hits the/<token>/<room>shape the worker requires and the socket is accepted. The worker's auth-token check is untouched. The pill's exposedurlstays the bare configured value (used in the tooltip).BUG 2 — content DUPLICATION / data integrity (
src/components/editor/CodeMirrorEditor.tsx)Root cause. The CodeMirror editor was created with
value={initialContent}and the collab binding seeded the same note body into theY.Text.yCollab'sySyncplugin only registers an observer on construction — it never reconciles the editor's existing doc against the (empty)Y.Text. So when the seed-on-empty inserted the body intoY.Textafter the first sync, the observer replayed that insert on top of the body already in the editor -> the body rendered twice, and the doubled text was what got saved back to the note (and would sync to GitHub = corruption).Fix. When collab is enabled (
getConfiguredUrl() != null— the exact gate the collab effect uses), the editor is built empty and theY.Textbecomes the single source of content. The seeder sees its body exactly once; a joiner (empty local note) receives the shared body over the wire exactly once and never re-seeds. With collab off (NEXT_PUBLIC_YJS_WS_URLunset) the editor is byte-for-byte unchanged. The seed-on-empty logic incollabExtension.tswas already correct and is left as-is.Tests
useCollaboration.test.ts:buildProbeUrlproduces/<token>/<room>; the hook dials/__probe__, not the bare URL.collabNoDoubling.test.tsx(new): mounts a realEditorView+ the realyCollabbinding (only the websocket transport faked). After binding + sync the editor holds the body exactly once for the seeder (fresh room) and a joiner (content arriving over the wire), with a regression guard proving that initializing the editor with the body reproduces the doubling.Playwright verification (two contexts, same room)
Ran the dev server with the real worker (
NEXT_PUBLIC_YJS_WS_URL=wss://collab.noteser.app/<token>) and drove two browser contexts on the same room via the share link (?collab=<collabId>) — seee2e/_verify_collab.spec.ts.Gates
npm run lintclean ·npx tsc --noEmitzero errors · fulljest --cigreen (3008 passed, no flake).🤖 Generated with Claude Code