Spaces:
Running
Running
| /** | |
| * 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; | |
| } | |