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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user