fix(collab): continuous document text synchronization preserving local cursor
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s

This commit is contained in:
sandrodach committed 2026-10-27 15:48:17 +01:00
1 parent ee8d586e2f
commit 183199401d
1 file changed
+55 -21
@@ -197,6 +197,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
currentTitleRef.current = title; currentTitleRef.current = title;
const currentBlocksRef = useRef<DocumentBlock[]>(blocks); const currentBlocksRef = useRef<DocumentBlock[]>(blocks);
currentBlocksRef.current = blocks; currentBlocksRef.current = blocks;
const lastKnownUpdatedAtRef = useRef<string | null>(initialDoc?.updatedAt || null);
useEffect(() => { useEffect(() => {
if (!loading && (autoFocusTitle || (doc?.title === 'Neue Notiz' && (!doc?.blocks || doc?.blocks.length <= 1)))) { if (!loading && (autoFocusTitle || (doc?.title === 'Neue Notiz' && (!doc?.blocks || doc?.blocks.length <= 1)))) {
@@ -434,6 +435,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
}); });
isSavingRef.current = false; isSavingRef.current = false;
lastSavedSnapshotRef.current = snapshot; lastSavedSnapshotRef.current = snapshot;
lastKnownUpdatedAtRef.current = updated.updatedAt || null;
setSaveStatus('saved'); setSaveStatus('saved');
setSaveErrorMessage(null); setSaveErrorMessage(null);
onSaveStatusChange?.('saved'); onSaveStatusChange?.('saved');
@@ -646,6 +648,9 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
}); });
lastSavedSnapshotRef.current = newSnapshot; lastSavedSnapshotRef.current = newSnapshot;
currentSnapshotRef.current = newSnapshot; currentSnapshotRef.current = newSnapshot;
if (remoteDoc.updatedAt) {
lastKnownUpdatedAtRef.current = remoteDoc.updatedAt;
}
if (docRef.current) { if (docRef.current) {
docRef.current = { ...docRef.current, ...remoteDoc }; docRef.current = { ...docRef.current, ...remoteDoc };
@@ -715,32 +720,61 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
[docId] [docId]
); );
// Periodic poll for locks & updates (guarantees synchronization even across flaky networks) // Sync when initialDoc prop changes externally
useEffect(() => {
if (initialDoc && initialDoc.id === docId && initialDoc.updatedAt && initialDoc.updatedAt !== lastKnownUpdatedAtRef.current) {
lastKnownUpdatedAtRef.current = initialDoc.updatedAt;
handleRemoteDocUpdated(initialDoc);
}
}, [initialDoc, docId, handleRemoteDocUpdated]);
// Periodic poll for locks & document text (guarantees text synchronization even without WebSockets)
useEffect(() => { useEffect(() => {
if (!docId) return; if (!docId) return;
const syncLocks = () => { let isCancelled = false;
api.getDocLocks(docId)
.then((res) => { const syncLocksAndDoc = async () => {
if (Array.isArray(res.blockLocks)) { try {
const map: Record<string, BlockLockInfo> = {}; // 1. Sync locks
for (const l of res.blockLocks) { const locksRes = await api.getDocLocks(docId);
map[l.blockId] = l; if (isCancelled) return;
} if (Array.isArray(locksRes.blockLocks)) {
setActiveBlockLocks(map); const map: Record<string, BlockLockInfo> = {};
for (const l of locksRes.blockLocks) {
map[l.blockId] = l;
} }
if (res.liveSession) { setActiveBlockLocks(map);
setLiveSession(res.liveSession); }
} else { if (locksRes.liveSession) {
setLiveSession(null); setLiveSession(locksRes.liveSession);
} else {
setLiveSession(null);
}
// 2. Sync document content if not currently actively saving
if (!isSavingRef.current) {
const remoteDoc = await api.getDocument(docId);
if (isCancelled || !remoteDoc) return;
// Check if server has newer or different state
if (remoteDoc.updatedAt && remoteDoc.updatedAt !== lastKnownUpdatedAtRef.current) {
console.log(`[DocumentEditor] Polling: Neuer Text-Stand vom Server (${remoteDoc.updatedAt}). Aktualisiere nicht-fokussierte Blöcke...`);
lastKnownUpdatedAtRef.current = remoteDoc.updatedAt;
handleRemoteDocUpdated(remoteDoc);
} }
}) }
.catch(() => {}); } catch (err) {
// ignore poll errors
}
}; };
syncLocks(); syncLocksAndDoc();
const interval = window.setInterval(syncLocks, 3000); const interval = window.setInterval(syncLocksAndDoc, 2000);
return () => window.clearInterval(interval); return () => {
}, [docId]); isCancelled = true;
window.clearInterval(interval);
};
}, [docId, handleRemoteDocUpdated]);
// Subscribe to realtime lock & live session events // Subscribe to realtime lock & live session events
useEffect(() => { useEffect(() => {
@@ -842,7 +876,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
focusedBlockIdRef.current = null; focusedBlockIdRef.current = null;
} }
api.unlockBlock(docId, blockId).catch(() => {}); api.unlockBlock(docId, blockId).catch(() => {});
executeAutoSave(); executeAutoSave(true);
}, },
[docId, executeAutoSave] [docId, executeAutoSave]
); );