fix(sync): eliminate zombie focus, flush debounced state on window blur, remove stale closures and buffer ws messages
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s

This commit is contained in:
sandrodach committed 2026-10-26 22:27:11 +01:00
1 parent 06f5f7043c
commit cedf5aebff
4 files changed
+221 -59

No files matched your search

+70 -19
View File
@@ -84,6 +84,13 @@ export function useDocumentSync(
const onRemoteEventRef = useRef(onRemoteEvent);
onRemoteEventRef.current = onRemoteEvent;
const docIdRef = useRef(docId);
docIdRef.current = docId;
const canEditRef = useRef(canEdit);
canEditRef.current = canEdit;
const currentUserRef = useRef(currentUser);
currentUserRef.current = currentUser;
const reconnectTimerRef = useRef<number | null>(null);
const fallbackTimerRef = useRef<number | null>(null);
@@ -93,6 +100,12 @@ export function useDocumentSync(
() => getUserColor(currentUser?.id || clientIdRef.current),
[currentUser?.id]
);
const localUserNameRef = useRef(localUserName);
localUserNameRef.current = localUserName;
const localUserColorRef = useRef(localUserColor);
localUserColorRef.current = localUserColor;
const pendingMessagesQueueRef = useRef<string[]>([]);
const lastCursorSendRef = useRef<number>(0);
const pendingCursorTimerRef = useRef<number | null>(null);
@@ -126,9 +139,11 @@ export function useDocumentSync(
if (!docId) {
setStatus('offline');
setRemoteCursors({});
pendingMessagesQueueRef.current = [];
return;
}
pendingMessagesQueueRef.current = [];
let isDisposed = false;
let retryCount = 0;
const currentClientId = `cli_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;
@@ -171,6 +186,19 @@ export function useDocumentSync(
}
retryCount = 0;
setStatus('connected');
// Flush any queued messages buffered while connecting or reconnecting
if (pendingMessagesQueueRef.current.length > 0) {
const queue = [...pendingMessagesQueueRef.current];
pendingMessagesQueueRef.current = [];
for (const msg of queue) {
try {
ws.send(msg);
} catch (err) {
console.warn('[DocSync] Failed to flush queued message:', err);
}
}
}
};
ws.onmessage = (event) => {
@@ -281,27 +309,32 @@ export function useDocumentSync(
const executeSendCursor = useCallback(
(blockId: string | null, selectionStart?: number, selectionEnd?: number) => {
const ws = wsRef.current;
if (!docId || !ws || ws.readyState !== WebSocket.OPEN) return;
const activeDocId = docIdRef.current;
if (!activeDocId || !ws || ws.readyState !== WebSocket.OPEN) return;
const payload: DocSyncEvent = {
type: 'cursor_moved',
docId,
docId: activeDocId,
clientId: clientIdRef.current,
userId: currentUser?.id,
userName: localUserName,
userColor: localUserColor,
userId: currentUserRef.current?.id,
userName: localUserNameRef.current,
userColor: localUserColorRef.current,
blockId: blockId || undefined,
selectionStart,
selectionEnd,
};
ws.send(JSON.stringify(payload));
try {
ws.send(JSON.stringify(payload));
} catch {
// Ephemeral cursor, ignore send failure
}
lastCursorSendRef.current = Date.now();
},
[docId, currentUser?.id, localUserName, localUserColor]
[]
);
const sendCursorMoved = useCallback(
(blockId: string | null, selectionStart?: number, selectionEnd?: number) => {
if (!docId) return;
if (!docIdRef.current) return;
const now = Date.now();
const elapsed = now - lastCursorSendRef.current;
@@ -340,30 +373,48 @@ export function useDocumentSync(
}
}
},
[docId, executeSendCursor]
[executeSendCursor]
);
const sendRawEvent = useCallback(
(evt: DocSyncEvent, fallbackFn?: () => void) => {
if (!docId || !canEdit) return;
const activeDocId = docIdRef.current;
const activeCanEdit = canEditRef.current;
if (!activeDocId || !activeCanEdit) return;
const ws = wsRef.current;
const payload: DocSyncEvent = {
...evt,
docId,
docId: activeDocId,
clientId: clientIdRef.current,
};
const jsonStr = JSON.stringify(payload);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
} else if (fallbackFn) {
if (fallbackTimerRef.current !== null) {
window.clearTimeout(fallbackTimerRef.current);
try {
ws.send(jsonStr);
} catch (err) {
console.warn('[DocSync] Failed to send WebSocket message, buffering:', err);
pendingMessagesQueueRef.current.push(jsonStr);
}
} else {
console.warn(
`[DocSync] WebSocket not OPEN (state=${ws ? ws.readyState : 'null'}). Buffering event:`,
evt.type
);
pendingMessagesQueueRef.current.push(jsonStr);
if (fallbackFn) {
if (fallbackTimerRef.current !== null) {
window.clearTimeout(fallbackTimerRef.current);
}
fallbackTimerRef.current = window.setTimeout(() => {
fallbackFn();
}, 600);
}
fallbackTimerRef.current = window.setTimeout(() => {
fallbackFn();
}, 600);
}
},
[docId, canEdit]
[]
);
const sendBlockCreated = useCallback(