Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

fix: use server content echo for remote edits instead of delta application

+10 -30
+10 -30
templates/document_edit.html
··· 598 598 } 599 599 600 600 function applyRemoteEdit(msg) { 601 - // msg may be a full-content string (legacy sync path) or an object with deltas. 601 + // msg may be a full-content string (legacy sync path) or an object with content. 602 602 if (applyingRemote) return; 603 603 applyingRemote = true; 604 604 try { 605 - let content = typeof msg === 'string' ? msg : msg.content; 606 - const deltas = (typeof msg === 'object' && msg.deltas) ? msg.deltas : null; 605 + // Always use the server's authoritative content echo for applying remote edits. 606 + // Attempting to apply deltas directly is unreliable because the positions in 607 + // the delta batch are relative to the sender's pre-change document, not the 608 + // receiver's current state (which may have diverged). 609 + const content = typeof msg === 'string' ? msg : msg.content; 610 + if (!content) return; 607 611 608 612 if (currentMode === 'source' && cmView) { 609 - if (deltas && deltas.length > 0) { 610 - // Apply granular deltas as a single CodeMirror dispatch. 611 - // All from/to positions are relative to the original document 612 - // (before any delta in this batch), which is what CodeMirror 613 - // expects when changes are batched in a single dispatch call. 614 - const docLen = cmView.state.doc.length; 615 - const changes = deltas.map(d => ({ 616 - from: Math.min(d.from < 0 ? 0 : d.from, docLen), 617 - to: Math.min(d.to < 0 ? docLen : d.to, docLen), 618 - insert: d.insert || '', 619 - })); 620 - cmView.dispatch({ 621 - changes, 622 - annotations: [remoteEditAnnotation.of(true)], 623 - }); 624 - // Use the server's full-content echo as a divergence check. 625 - if (content && cmView.state.doc.toString() !== content) { 626 - // State diverged — fall back to full replacement. 627 - cmView.dispatch({ 628 - changes: { from: 0, to: cmView.state.doc.length, insert: content }, 629 - annotations: [remoteEditAnnotation.of(true)], 630 - }); 631 - } 632 - } else if (content && cmView.state.doc.toString() !== content) { 633 - // Legacy full-replacement path (sync message or old server). 613 + if (cmView.state.doc.toString() !== content) { 634 614 cmView.dispatch({ 635 615 changes: { from: 0, to: cmView.state.doc.length, insert: content }, 636 616 annotations: [remoteEditAnnotation.of(true)], 637 617 }); 638 618 } 639 - if (content) updatePreview(content); 640 - } else if (currentMode === 'rich' && milkdownEditor && content) { 619 + updatePreview(content); 620 + } else if (currentMode === 'rich' && milkdownEditor) { 641 621 createMilkdownEditor(content); 642 622 } 643 623 } finally {