feat(ui): show a limited connection instead of offline behind proxies
When no realtime connection gets through (proxy buffering SSE, notes on long polling) but the server answers, the profile shows a yellow "eingeschränkt" instead of "offline". A full connection now shows a green "online" on the desktop too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7fcd5a258c
commit
282a9af557
6 files changed
+205
-129
No files matched your search
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { onRealtimeStatus } from './api';
|
||||
|
||||
// Grace period before a missing realtime connection counts as offline (app start, short reconnects)
|
||||
export const SERVER_OFFLINE_GRACE_MS = 4000;
|
||||
// How often the server is asked while the realtime connection is missing
|
||||
export const SERVER_PROBE_INTERVAL_MS = 15000;
|
||||
|
||||
// online: realtime connection (WebSocket/SSE) is up; limited: the server answers plain requests, but no
|
||||
// realtime connection gets through (e.g. a proxy that buffers event streams; notes fall back to long
|
||||
// polling); offline: no server at all
|
||||
export type ConnectionState = 'online' | 'limited' | 'offline';
|
||||
|
||||
async function serverReachable(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/v1/health', { cache: 'no-store' });
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether the server is reachable, judged by the app-wide realtime connection. Unlike navigator.onLine it
|
||||
// also notices a server that is down while the device still has network. Without device network it is
|
||||
// offline right away; otherwise, once the connection stayed away for a moment, a request to the server
|
||||
// decides between limited and offline (repeated while the connection is missing).
|
||||
export function useConnectionState(): ConnectionState {
|
||||
const [state, setState] = useState<ConnectionState>('online');
|
||||
|
||||
useEffect(() => {
|
||||
let connected = false;
|
||||
let disposed = false;
|
||||
let timer: number | undefined;
|
||||
const stopProbing = () => {
|
||||
window.clearTimeout(timer);
|
||||
timer = undefined;
|
||||
};
|
||||
const probe = async () => {
|
||||
const reachable = await serverReachable();
|
||||
if (disposed || connected) return;
|
||||
setState(reachable && navigator.onLine !== false ? 'limited' : 'offline');
|
||||
timer = window.setTimeout(probe, SERVER_PROBE_INTERVAL_MS);
|
||||
};
|
||||
// Reconnect attempts report "not connected" every few seconds; they must not restart the grace period
|
||||
const update = () => {
|
||||
if (connected) {
|
||||
stopProbing();
|
||||
setState('online');
|
||||
} else if (navigator.onLine === false) {
|
||||
stopProbing();
|
||||
setState('offline');
|
||||
} else if (timer === undefined) {
|
||||
timer = window.setTimeout(probe, SERVER_OFFLINE_GRACE_MS);
|
||||
}
|
||||
};
|
||||
const unsubscribe = onRealtimeStatus((isConnected) => {
|
||||
connected = isConnected;
|
||||
update();
|
||||
});
|
||||
window.addEventListener('online', update);
|
||||
window.addEventListener('offline', update);
|
||||
return () => {
|
||||
disposed = true;
|
||||
unsubscribe();
|
||||
stopProbing();
|
||||
window.removeEventListener('online', update);
|
||||
window.removeEventListener('offline', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
}
|
||||
Reference in new issue
Block a user