From b9fd396e28eaf4b65a8769b266cb8c023b98448a Mon Sep 17 00:00:00 2001 From: Naman Rusia Date: Thu, 24 Sep 2026 18:00:42 -0400 Subject: [PATCH 1/3] Let one client write the starter code into an empty editor When the owner picked a problem with a partner in the room, the partner heard about the problem before the owner's insert arrived, saw an empty document and inserted the starter too, so both saw it twice. Now only the owner writes it, or the partner when the owner is not in the room. --- apps/web/src/components/CollaborativeEditor.tsx | 12 ++++++++++-- apps/web/src/routes/rooms/RoomView.tsx | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/CollaborativeEditor.tsx b/apps/web/src/components/CollaborativeEditor.tsx index 198fb31..d1b71d6 100644 --- a/apps/web/src/components/CollaborativeEditor.tsx +++ b/apps/web/src/components/CollaborativeEditor.tsx @@ -17,6 +17,11 @@ interface Props { // the text; everyone else receives the swap through the document. Two // clients swapping at once would merge into two copies of the starter. replacesOnProblemChange: boolean; + // Whether this client may put the starter into an empty document. One + // writer per room, for the same reason: when the owner picks a problem, a + // partner hears about it before the owner's insert arrives, sees an empty + // document, and would insert a second copy. + writesStarter: boolean; onCodeChange: (code: string) => void; // The colour a user id is shown in everywhere else in the room. colorOf: (userId: string) => string; @@ -57,6 +62,7 @@ const CollaborativeEditor = ({ problemId, starterCode, replacesOnProblemChange, + writesStarter, onCodeChange, colorOf, }: Props) => { @@ -78,6 +84,8 @@ const CollaborativeEditor = ({ colorOfRef.current = colorOf; const starterCodeRef = useRef(starterCode); starterCodeRef.current = starterCode; + const writesStarterRef = useRef(writesStarter); + writesStarterRef.current = writesStarter; // The relay keeps no document, so a room's text lives only in its peers. The // starter code goes in when the shared text is empty after sync, which is @@ -91,7 +99,7 @@ const CollaborativeEditor = ({ const seedStarterCode = (ydoc: Y.Doc) => { const starter = starterCodeRef.current; const ytext = ydoc.getText("code"); - if (starter && !seededRef.current && ytext.length === 0) { + if (starter && writesStarterRef.current && !seededRef.current && ytext.length === 0) { seededRef.current = true; ytext.insert(0, starter); } @@ -197,7 +205,7 @@ const CollaborativeEditor = ({ return; } seedStarterCode(ydoc); - }, [problemId, starterCode, replacesOnProblemChange]); + }, [problemId, starterCode, replacesOnProblemChange, writesStarter]); // The roster decides colours, so a join or leave can recolour a cursor. useEffect(() => { diff --git a/apps/web/src/routes/rooms/RoomView.tsx b/apps/web/src/routes/rooms/RoomView.tsx index 06d626c..9756ec8 100644 --- a/apps/web/src/routes/rooms/RoomView.tsx +++ b/apps/web/src/routes/rooms/RoomView.tsx @@ -439,6 +439,7 @@ const Room = ({ roomId }: { roomId: string }) => { problemId={problem?.id} starterCode={problem?.starterCode} replacesOnProblemChange={isOwner} + writesStarter={isOwner || !members.some((member) => member.userId === room.createdBy)} onCodeChange={setCode} colorOf={colorOf} /> From eb76aecb84cd6eff4aa00641c0085d055a536126 Mon Sep 17 00:00:00 2001 From: Naman Rusia Date: Thu, 24 Sep 2026 18:14:39 -0400 Subject: [PATCH 2/3] With the owner away, let only the lowest user id write the starter code --- apps/web/src/routes/rooms/RoomView.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/web/src/routes/rooms/RoomView.tsx b/apps/web/src/routes/rooms/RoomView.tsx index 9756ec8..096b235 100644 --- a/apps/web/src/routes/rooms/RoomView.tsx +++ b/apps/web/src/routes/rooms/RoomView.tsx @@ -299,6 +299,11 @@ const Room = ({ roomId }: { roomId: string }) => { const { isConnected, connectionState, messages, members, submissions, problemChange, seedSubmissions, sendMessage } = useWebSocket(room?.id ?? ""); const isOwner = room?.createdBy === user?.id; + // One writer per room for the starter code: the owner, or with the owner + // away, the lowest user id, so every client agrees without talking. + const starterWriter = members.some((member) => member.userId === room?.createdBy) + ? room?.createdBy + : members.map((member) => member.userId).sort()[0]; const rosterKey = members.map((member) => member.userId).join(" "); const colorOf = useCallback( (userId: string) => personColor(userId, user?.id, rosterKey.split(" ")), @@ -439,7 +444,7 @@ const Room = ({ roomId }: { roomId: string }) => { problemId={problem?.id} starterCode={problem?.starterCode} replacesOnProblemChange={isOwner} - writesStarter={isOwner || !members.some((member) => member.userId === room.createdBy)} + writesStarter={isOwner || starterWriter === user.id} onCodeChange={setCode} colorOf={colorOf} /> From b4e978f7e3caf5a7f7ea6f8bbbb8ec94e5d962e0 Mon Sep 17 00:00:00 2001 From: Naman Rusia Date: Thu, 24 Sep 2026 18:16:23 -0400 Subject: [PATCH 3/3] Say who writes the starter code in the editor's comment --- apps/web/src/components/CollaborativeEditor.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/CollaborativeEditor.tsx b/apps/web/src/components/CollaborativeEditor.tsx index d1b71d6..72d5c2e 100644 --- a/apps/web/src/components/CollaborativeEditor.tsx +++ b/apps/web/src/components/CollaborativeEditor.tsx @@ -88,9 +88,9 @@ const CollaborativeEditor = ({ writesStarterRef.current = writesStarter; // The relay keeps no document, so a room's text lives only in its peers. The - // starter code goes in when the shared text is empty after sync, which is - // the first person to arrive with a problem assigned. Any later arrival - // syncs their text instead and leaves it alone. Once per document: a + // starter code goes in when the shared text is empty after sync, written by + // the one client allowed to (writesStarter). Any later arrival syncs their + // text instead and leaves it alone. Once per document: a // reconnect after someone cleared the editor must not put it back. const seededRef = useRef(false); // The problem the text was written for. Switching problems replaces the