fix(workspace): add http polling fallback for proxy/firewall environments and exponential ws reconnect backoff
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s

This commit is contained in:
sandrodach committed 2026-10-26 11:42:57 +01:00
1 parent e088ef6974
commit c2ed76c6a4
2 files changed
+65 -9

No files matched your search

@@ -132,6 +132,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
// Dedicated WebSocket connection & status for live indicator
const wsRef = useRef<WebSocket | null>(null);
const wsReconnectTimerRef = useRef<any>(null);
const wsReconnectDelayRef = useRef<number>(2000);
const [wsConnected, setWsConnected] = useState<boolean>(false);
// Folder state (synchronized with server)
@@ -970,6 +971,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
ws.onopen = () => {
if (!isMounted) return;
wsReconnectDelayRef.current = 2000;
setWsConnected(true);
api.getFolders().then(setCustomFolders).catch(() => {});
api.getSharedFolders().then(setSharedFolders).catch(() => {});
@@ -979,7 +981,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (!isMounted) return;
setWsConnected(false);
clearTimeout(wsReconnectTimerRef.current);
wsReconnectTimerRef.current = setTimeout(connect, 2000);
const delay = wsReconnectDelayRef.current;
wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5));
wsReconnectTimerRef.current = setTimeout(connect, delay);
};
ws.onerror = () => {
@@ -997,7 +1001,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (!isMounted) return;
setWsConnected(false);
clearTimeout(wsReconnectTimerRef.current);
wsReconnectTimerRef.current = setTimeout(connect, 3000);
const delay = wsReconnectDelayRef.current;
wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5));
wsReconnectTimerRef.current = setTimeout(connect, delay);
}
};
@@ -1024,6 +1030,44 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
};
}, [onStatsChange]);
// Fallback HTTP polling when WebSocket is disconnected (e.g. corporate proxy / firewall blocking WebSockets)
useEffect(() => {
if (wsConnected) return;
let isMounted = true;
const pollInterval = setInterval(async () => {
if (!isMounted || document.visibilityState !== 'visible') return;
// 1. Poll lock status of currently selected document
if (selectedDocIdRef.current) {
try {
const res = await api.getDocLock(selectedDocIdRef.current);
if (res.locked && res.lock && res.lock.userId !== currentUserIdRef.current) {
setActiveLock(res.lock);
} else {
setActiveLock(null);
}
} catch {}
}
// 2. Poll document content if not actively typing and document has changed on server
if (selectedDocIdRef.current && !isEditorFocusedRef.current && !pendingSaveDocRef.current) {
try {
const fresh = await api.getDocument(selectedDocIdRef.current);
if (fresh && (!selectedDoc || fresh.updatedAt !== selectedDoc.updatedAt)) {
setSelectedDoc(fresh);
setDocs((prev) => prev.map((d) => (d.id === fresh.id ? fresh : d)));
}
} catch {}
}
}, 4000);
return () => {
isMounted = false;
clearInterval(pollInterval);
};
}, [wsConnected, selectedDoc?.updatedAt]);
const openShareModal = async () => {
if (!selectedDoc) return;
setShowShareModal(true);
@@ -4001,11 +4045,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
className={`h-8 w-8 flex items-center justify-center rounded-btn border transition-colors ${
wsConnected
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
: 'bg-canvas border-border text-muted'
: 'bg-amber-500/10 border-amber-500/30 text-amber-500'
}`}
title={wsConnected ? 'Live synchronisiert (Online)' : 'Offline - Verbindung wird wiederhergestellt'}
title={
wsConnected
? 'Live synchronisiert (WebSocket online)'
: 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv'
}
>
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : 'opacity-80'}`} />
</div>
</div>
</div>
@@ -4037,11 +4085,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
className={`h-9 w-9 flex items-center justify-center rounded-btn border transition-colors ${
wsConnected
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
: 'bg-canvas border-border text-muted'
: 'bg-amber-500/10 border-amber-500/30 text-amber-500'
}`}
title={wsConnected ? 'Live synchronisiert (Online)' : 'Offline - Verbindung wird wiederhergestellt'}
title={
wsConnected
? 'Live synchronisiert (WebSocket online)'
: 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv'
}
>
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : 'opacity-80'}`} />
</div>
</div>
</div>