A local-first note taking app
0

Configure Feed

Select the types of activity you want to include in your feed.

add lock to automerge editor to prevent race on debounce

Ethan Graf (Jun 14, 2026, 10:57 PM EDT) c063ef29 a624a8a9

+18 -21
+18 -21
src/editors/automerge/automergeDocumentEditor.tsx
··· 31 31 32 32 const readOnlyCompartment = useRef(new Compartment()); 33 33 34 - // Echo suppression: when a local write is in flight, skip dispatching 35 - // the resulting change back into the editor. 36 - const isInFlightRef = useRef(false); 37 - 38 34 // Current content as a string, used for diff computation. 39 35 const contentRef = useRef(''); 40 36 41 37 // Debounce timer for the save-based writer. 42 38 const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); 39 + 40 + // Lock: set while we are writing to the doc so the sync useEffect 41 + // can skip the round-trip back into the editor. 42 + const isWriting = useRef(false); 43 43 44 44 // Version counter — bumped when the session notifies us of a doc change. 45 45 const [docVersion, setDocVersion] = useState(0); ··· 57 57 // When the session loads/updates the doc, push the new content into CM6. 58 58 useEffect(() => { 59 59 const newContent = doc.content ?? ''; 60 - contentRef.current = newContent; 61 60 const view = viewRef.current; 62 61 if (!view) return; 63 62 63 + if (isWriting.current) { 64 + isWriting.current = false; 65 + contentRef.current = newContent; 66 + return; 67 + } 68 + 64 69 const currentText = view.state.doc.toString(); 65 70 if (currentText !== newContent) { 66 71 view.dispatch({ 67 72 changes: { from: 0, to: currentText.length, insert: newContent }, 68 73 }); 69 74 } 75 + contentRef.current = newContent; 70 76 }, [docVersion, doc.content]); 71 77 72 78 // Create EditorView on mount. ··· 85 91 if (saveTimerRef.current) clearTimeout(saveTimerRef.current); 86 92 saveTimerRef.current = setTimeout(() => { 87 93 saveTimerRef.current = null; 88 - isInFlightRef.current = true; 89 - try { 90 - applyChangeRef.current((d) => { 91 - d.content = newContent; 92 - }); 93 - } finally { 94 - isInFlightRef.current = false; 95 - } 94 + isWriting.current = true; 95 + applyChangeRef.current((d) => { 96 + d.content = newContent; 97 + }); 96 98 }, 50); 97 99 } 98 100 }); ··· 142 144 clearTimeout(saveTimerRef.current); 143 145 saveTimerRef.current = null; 144 146 const finalContent = view.state.doc.toString(); 145 - isInFlightRef.current = true; 146 - try { 147 - applyChangeRef.current((d) => { 148 - d.content = finalContent; 149 - }); 150 - } finally { 151 - isInFlightRef.current = false; 152 - } 147 + applyChangeRef.current((d) => { 148 + d.content = finalContent; 149 + }); 153 150 } 154 151 view.destroy(); 155 152 viewRef.current = null;