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;
|
||||
const currentBlocksRef = useRef<DocumentBlock[]>(blocks);
|
||||
currentBlocksRef.current = blocks;
|
||||
const lastKnownUpdatedAtRef = useRef<string | null>(initialDoc?.updatedAt || null);
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
lastSavedSnapshotRef.current = snapshot;
|
||||
lastKnownUpdatedAtRef.current = updated.updatedAt || null;
|
||||
setSaveStatus('saved');
|
||||
setSaveErrorMessage(null);
|
||||
onSaveStatusChange?.('saved');
|
||||
@@ -646,6 +648,9 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
});
|
||||
lastSavedSnapshotRef.current = newSnapshot;
|
||||
currentSnapshotRef.current = newSnapshot;
|
||||
if (remoteDoc.updatedAt) {
|
||||
lastKnownUpdatedAtRef.current = remoteDoc.updatedAt;
|
||||
}
|
||||
|
||||
if (docRef.current) {
|
||||
docRef.current = { ...docRef.current, ...remoteDoc };
|
||||
@@ -715,32 +720,61 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
[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(() => {
|
||||
if (!docId) return;
|
||||
const syncLocks = () => {
|
||||
api.getDocLocks(docId)
|
||||
.then((res) => {
|
||||
if (Array.isArray(res.blockLocks)) {
|
||||
let isCancelled = false;
|
||||
|
||||
const syncLocksAndDoc = async () => {
|
||||
try {
|
||||
// 1. Sync locks
|
||||
const locksRes = await api.getDocLocks(docId);
|
||||
if (isCancelled) return;
|
||||
if (Array.isArray(locksRes.blockLocks)) {
|
||||
const map: Record<string, BlockLockInfo> = {};
|
||||
for (const l of res.blockLocks) {
|
||||
for (const l of locksRes.blockLocks) {
|
||||
map[l.blockId] = l;
|
||||
}
|
||||
setActiveBlockLocks(map);
|
||||
}
|
||||
if (res.liveSession) {
|
||||
setLiveSession(res.liveSession);
|
||||
if (locksRes.liveSession) {
|
||||
setLiveSession(locksRes.liveSession);
|
||||
} else {
|
||||
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();
|
||||
const interval = window.setInterval(syncLocks, 3000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [docId]);
|
||||
syncLocksAndDoc();
|
||||
const interval = window.setInterval(syncLocksAndDoc, 2000);
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
window.clearInterval(interval);
|
||||
};
|
||||
}, [docId, handleRemoteDocUpdated]);
|
||||
|
||||
// Subscribe to realtime lock & live session events
|
||||
useEffect(() => {
|
||||
@@ -842,7 +876,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
focusedBlockIdRef.current = null;
|
||||
}
|
||||
api.unlockBlock(docId, blockId).catch(() => {});
|
||||
executeAutoSave();
|
||||
executeAutoSave(true);
|
||||
},
|
||||
[docId, executeAutoSave]
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user