AUDIT / src /hooks /useNetworkStatus.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
8.62 kB
/**
* useNetworkStatus.ts — S57: Network Intelligence Layer (bandwidth estimation)
*
* Monitora connettività reale + latenza + bandwidth stimata.
* Safari-safe: usa navigator.onLine + eventi window, nessun Worker.
*
* S57 rispetto a S55:
* + navigator.connection "change" events → react istantaneamente al cambio rete
* + downlink bandwidth estimation (navigator.connection.downlink, MB/s)
* + ping interval adattivo: 10s quando poor/slow, 30s quando good
* + campo bandwidthMbps esposto nell'API pubblica
* + classify usa downlink come segnale primario quando RTT non disponibile
*
* Stati qualità:
* offline — navigator.onLine=false o ping fallisce ×2
* poor — RTT >2500ms, downlink <0.5 Mbps, o effectiveType "2g"/"slow-2g"
* slow — RTT 800-2500ms, downlink 0.5-1 Mbps, o effectiveType "3g"
* good — RTT <800ms, downlink >2 Mbps (default se stima non disponibile)
*
* API:
* const { online, quality, rttMs, bandwidthMbps, since, justReconnected } = useNetworkStatus();
*/
import { useState, useEffect, useRef, useCallback } from "react";
export type NetworkQuality = "good" | "slow" | "poor" | "offline";
export interface NetworkState {
online: boolean;
quality: NetworkQuality;
rttMs: number | null;
bandwidthMbps: number | null; // S57: downlink stimato in Mbps (null se non disponibile)
since: number; // timestamp inizio stato attuale
justReconnected: boolean; // true per 4s dopo riconnessione
}
// ── Tipi Navigator Connection API (non standard, assente in @types/node) ──────
interface NetworkInformation extends EventTarget {
effectiveType?: "slow-2g" | "2g" | "3g" | "4g";
rtt?: number;
downlink?: number; // Mbps stimato
saveData?: boolean;
onchange?: ((ev: Event) => void) | null;
}
type NavigatorWithConn = Navigator & { connection?: NetworkInformation };
const PING_URL = "https://agente-ai.pages.dev/favicon.ico";
const PING_TIMEOUT_MS = 4000;
const PING_INTERVAL_GOOD_MS = 30_000;
const PING_INTERVAL_POOR_MS = 10_000; // S57: più frequente quando rete è scarsa
const RECONNECT_SHOW_MS = 4_000;
const DEFAULT: NetworkState = {
online: navigator.onLine,
quality: navigator.onLine ? "good" : "offline",
rttMs: null,
bandwidthMbps: null,
since: Date.now(),
justReconnected: false,
};
// ── Legge downlink da navigator.connection ─────────────────────────────────
function readDownlink(): number | null {
const conn = (navigator as NavigatorWithConn).connection;
if (!conn || conn.downlink === undefined) return null;
return conn.downlink > 0 ? conn.downlink : null;
}
// ── Legge effectiveType da navigator.connection ────────────────────────────
function readEffectiveType(): NetworkQuality | null {
const conn = (navigator as NavigatorWithConn).connection;
if (!conn) return null;
if (conn.effectiveType === "slow-2g" || conn.effectiveType === "2g") return "poor";
if (conn.effectiveType === "3g") return "slow";
return null;
}
// ── Classifica la qualità usando tutti i segnali disponibili ──────────────
function classify(
rttMs: number | null,
pingOk: boolean,
downlinkMbps: number | null
): NetworkQuality {
if (!pingOk) return "offline";
// effectiveType ha precedenza quando disponibile (dato dal browser)
const fromType = readEffectiveType();
if (fromType) return fromType;
// downlink come segnale primario S57
if (downlinkMbps !== null) {
if (downlinkMbps < 0.5) return "poor";
if (downlinkMbps < 1.0) return "slow"; // Bug#2: was 2.0 — banner showed at 1.4 Mbps (sufficient for app)
return "good";
}
// Fallback RTT
if (rttMs === null) return "good";
if (rttMs > 2500) return "poor";
if (rttMs > 800) return "slow";
return "good";
}
async function measureRtt(): Promise<{ ok: boolean; rttMs: number | null }> {
const t0 = performance.now();
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), PING_TIMEOUT_MS);
await fetch(`${PING_URL}?_=${Date.now()}`, {
method: "HEAD",
cache: "no-store",
signal: ctrl.signal,
});
clearTimeout(timer);
return { ok: true, rttMs: Math.round(performance.now() - t0) };
} catch {
return { ok: false, rttMs: null };
}
}
export function useNetworkStatus(): NetworkState {
const [state, setState] = useState<NetworkState>(DEFAULT);
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const pingCount = useRef<number>(0);
const currentQuality = useRef<NetworkQuality>(DEFAULT.quality);
// ── Ping + aggiornamento stato ───────────────────────────────────────────
const runPing = useCallback(async () => {
if (!navigator.onLine) {
setState(s => s.online === false ? s : {
online: false, quality: "offline",
rttMs: null, bandwidthMbps: null,
since: Date.now(), justReconnected: false,
});
currentQuality.current = "offline";
return;
}
const [{ ok, rttMs }] = await Promise.all([measureRtt()]);
const downlinkMbps = readDownlink();
if (!ok) {
pingCount.current++;
// Offline confermato solo dopo 2 ping falliti consecutivi (evita flap)
if (pingCount.current >= 2) {
setState(s => s.quality === "offline" ? s : {
online: false, quality: "offline",
rttMs: null, bandwidthMbps: null,
since: Date.now(), justReconnected: false,
});
currentQuality.current = "offline";
}
return;
}
pingCount.current = 0;
const quality = classify(rttMs, true, downlinkMbps);
setState(prev => {
const wasOffline = !prev.online;
const changed = prev.quality !== quality || !prev.online;
if (!changed && prev.bandwidthMbps === downlinkMbps) return prev;
if (wasOffline && reconnectTimer.current) clearTimeout(reconnectTimer.current);
currentQuality.current = quality;
return {
online: true,
quality,
rttMs,
bandwidthMbps: downlinkMbps,
since: changed ? Date.now() : prev.since,
justReconnected: wasOffline,
};
});
// Auto-dismiss justReconnected dopo 4s
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
reconnectTimer.current = setTimeout(() => {
setState(s => s.justReconnected ? { ...s, justReconnected: false } : s);
}, RECONNECT_SHOW_MS);
}, []);
// ── Intervallo adattivo: rischedula quando qualità cambia ────────────────
const rescheduleInterval = useCallback(() => {
if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
const ms = currentQuality.current === "good"
? PING_INTERVAL_GOOD_MS
: PING_INTERVAL_POOR_MS;
pingIntervalRef.current = setInterval(() => {
runPing().then(rescheduleInterval).catch(() => {});
}, ms);
}, [runPing]);
useEffect(() => {
runPing().then(rescheduleInterval).catch(() => {});
const onOnline = () => {
pingCount.current = 0;
runPing().then(rescheduleInterval).catch(() => {});
};
const onOffline = () => {
currentQuality.current = "offline";
setState({ online: false, quality: "offline", rttMs: null, bandwidthMbps: null, since: Date.now(), justReconnected: false });
};
window.addEventListener("online", onOnline);
window.addEventListener("offline", onOffline);
// S57: navigator.connection "change" event — react istantaneamente
const conn = (navigator as NavigatorWithConn).connection;
const onConnChange = () => { runPing().then(rescheduleInterval).catch(() => {}); };
conn?.addEventListener("change", onConnChange);
return () => {
if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
window.removeEventListener("online", onOnline);
window.removeEventListener("offline", onOffline);
conn?.removeEventListener("change", onConnChange);
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
};
}, [runPing, rescheduleInterval]);
return state;
}