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 // Dedicated WebSocket connection & status for live indicator
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
const wsReconnectTimerRef = useRef<any>(null); const wsReconnectTimerRef = useRef<any>(null);
const wsReconnectDelayRef = useRef<number>(2000);
const [wsConnected, setWsConnected] = useState<boolean>(false); const [wsConnected, setWsConnected] = useState<boolean>(false);
// Folder state (synchronized with server) // Folder state (synchronized with server)
@@ -970,6 +971,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
ws.onopen = () => { ws.onopen = () => {
if (!isMounted) return; if (!isMounted) return;
wsReconnectDelayRef.current = 2000;
setWsConnected(true); setWsConnected(true);
api.getFolders().then(setCustomFolders).catch(() => {}); api.getFolders().then(setCustomFolders).catch(() => {});
api.getSharedFolders().then(setSharedFolders).catch(() => {}); api.getSharedFolders().then(setSharedFolders).catch(() => {});
@@ -979,7 +981,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (!isMounted) return; if (!isMounted) return;
setWsConnected(false); setWsConnected(false);
clearTimeout(wsReconnectTimerRef.current); 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 = () => { ws.onerror = () => {
@@ -997,7 +1001,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (!isMounted) return; if (!isMounted) return;
setWsConnected(false); setWsConnected(false);
clearTimeout(wsReconnectTimerRef.current); 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]); }, [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 () => { const openShareModal = async () => {
if (!selectedDoc) return; if (!selectedDoc) return;
setShowShareModal(true); 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 ${ className={`h-8 w-8 flex items-center justify-center rounded-btn border transition-colors ${
wsConnected wsConnected
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500' ? '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> </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 ${ className={`h-9 w-9 flex items-center justify-center rounded-btn border transition-colors ${
wsConnected wsConnected
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500' ? '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> </div>
</div> </div>
+5 -1
View File
@@ -1048,6 +1048,7 @@ let wsListeners: ((msg: WSMessage) => void)[] = [];
let wsStatusListeners: ((connected: boolean) => void)[] = []; let wsStatusListeners: ((connected: boolean) => void)[] = [];
let isWSConnected = false; let isWSConnected = false;
let manualDisconnect = false; let manualDisconnect = false;
let wsReconnectDelay = 2000;
export function buildWebSocketURL(listId?: string): string { export function buildWebSocketURL(listId?: string): string {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -1097,6 +1098,7 @@ export function connectWebSocket(): void {
activeGlobalWS = ws; activeGlobalWS = ws;
ws.onopen = () => { ws.onopen = () => {
wsReconnectDelay = 2000;
notifyStatusChange(true); notifyStatusChange(true);
}; };
@@ -1121,9 +1123,11 @@ export function connectWebSocket(): void {
notifyStatusChange(false); notifyStatusChange(false);
if (!manualDisconnect && getToken()) { if (!manualDisconnect && getToken()) {
clearTimeout(wsReconnectTimer); clearTimeout(wsReconnectTimer);
const delay = wsReconnectDelay;
wsReconnectDelay = Math.min(30000, Math.round(wsReconnectDelay * 1.5));
wsReconnectTimer = setTimeout(() => { wsReconnectTimer = setTimeout(() => {
connectWebSocket(); connectWebSocket();
}, 3000); }, delay);
} }
}; };