fix(collab): continuous document text synchronization preserving local cursor
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
This commit is contained in:
1 parent
ee8d586e2f
commit
183199401d
1 file changed
+49
-15
@@ -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 {
|
||||||
|
// 1. Sync locks
|
||||||
|
const locksRes = await api.getDocLocks(docId);
|
||||||
|
if (isCancelled) return;
|
||||||
|
if (Array.isArray(locksRes.blockLocks)) {
|
||||||
const map: Record<string, BlockLockInfo> = {};
|
const map: Record<string, BlockLockInfo> = {};
|
||||||
for (const l of res.blockLocks) {
|
for (const l of locksRes.blockLocks) {
|
||||||
map[l.blockId] = l;
|
map[l.blockId] = l;
|
||||||
}
|
}
|
||||||
setActiveBlockLocks(map);
|
setActiveBlockLocks(map);
|
||||||
}
|
}
|
||||||
if (res.liveSession) {
|
if (locksRes.liveSession) {
|
||||||
setLiveSession(res.liveSession);
|
setLiveSession(locksRes.liveSession);
|
||||||
} else {
|
} else {
|
||||||
setLiveSession(null);
|
setLiveSession(null);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(() => {});
|
// 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 (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]
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user