| import { useEffect, useState } from "react" |
| import { fetchLatestReport, getWebSocketUrl } from "../lib/api" |
|
|
| export default function useRealtimeInspection() { |
| const [latestInspection, setLatestInspection] = useState(null) |
| const [connectionState, setConnectionState] = useState("connecting") |
| const [lastEventAt, setLastEventAt] = useState(null) |
| const [streamError, setStreamError] = useState("") |
|
|
| useEffect(() => { |
| let socket |
| let reconnectTimer |
| let heartbeatTimer |
| let refreshTimer |
| let active = true |
| let retryCount = 0 |
|
|
| async function hydrateLatest() { |
| try { |
| const latest = await fetchLatestReport() |
| if (active && latest?.timestamp) { |
| setLatestInspection((current) => { |
| if (!current || current.timestamp !== latest.timestamp) { |
| return latest |
| } |
| return current |
| }) |
| setLastEventAt(latest.timestamp) |
| } |
| } catch { |
| if (active) { |
| setStreamError("Latest inspection snapshot is unavailable right now.") |
| } |
| } |
| } |
|
|
| function scheduleReconnect() { |
| if (!active) return |
|
|
| setConnectionState("reconnecting") |
| const delay = Math.min(8000, 1200 * 2 ** retryCount) |
| retryCount += 1 |
| reconnectTimer = window.setTimeout(connect, delay) |
| } |
|
|
| function connect() { |
| if (!active) return |
|
|
| setConnectionState("connecting") |
| socket = new WebSocket(getWebSocketUrl("/ws")) |
|
|
| socket.onopen = () => { |
| retryCount = 0 |
| setConnectionState("connected") |
| setStreamError("") |
|
|
| heartbeatTimer = window.setInterval(() => { |
| if (socket?.readyState === WebSocket.OPEN) { |
| socket.send(JSON.stringify({ type: "ping" })) |
| } |
| }, 15000) |
| } |
|
|
| socket.onmessage = (event) => { |
| const payload = JSON.parse(event.data) |
|
|
| if (payload.type === "inspection.update" || payload.type === "inspection.snapshot") { |
| setLatestInspection(payload) |
| setLastEventAt(payload.timestamp) |
| return |
| } |
|
|
| if (payload.type === "pong") { |
| setConnectionState("connected") |
| } |
| } |
|
|
| socket.onerror = () => { |
| setStreamError("Live stream interrupted. Reconnecting to the inspection server.") |
| } |
|
|
| socket.onclose = () => { |
| window.clearInterval(heartbeatTimer) |
| if (active) { |
| scheduleReconnect() |
| } |
| } |
| } |
|
|
| hydrateLatest() |
| connect() |
| refreshTimer = window.setInterval(hydrateLatest, 6000) |
|
|
| return () => { |
| active = false |
| window.clearTimeout(reconnectTimer) |
| window.clearInterval(heartbeatTimer) |
| window.clearInterval(refreshTimer) |
|
|
| if (socket) { |
| socket.close() |
| } |
| } |
| }, []) |
|
|
| return { |
| latestInspection, |
| connectionState, |
| lastEventAt, |
| streamError, |
| } |
| } |
|
|