| 1 | /** |
| 2 | * A project's live tail in the browser: the socket the site forwards to |
| 3 | * the project's store (routes/repo/observability-live.ts), opened while |
| 4 | * a page follows it, and opened again after it drops. |
| 5 | */ |
| 6 | import { useEffect, useRef, useState } from "react"; |
| 7 | |
| 8 | import type { LiveItem } from "@g1t/contracts"; |
| 9 | |
| 10 | export type LiveState = "off" | "connecting" | "live" | "dropped"; |
| 11 | |
| 12 | export function useTelemetryLive(path: string, on: boolean, onItems: (items: LiveItem[], dropped: number) => void): LiveState { |
| 13 | const [state, setState] = useState<LiveState>("off"); |
| 14 | const handler = useRef(onItems); |
| 15 | handler.current = onItems; |
| 16 | useEffect(() => { |
| 17 | if (!on) { |
| 18 | setState("off"); |
| 19 | return; |
| 20 | } |
| 21 | let ws: WebSocket | null = null; |
| 22 | let closed = false; |
| 23 | let retry: ReturnType<typeof setTimeout> | undefined; |
| 24 | let ping: ReturnType<typeof setInterval> | undefined; |
| 25 | let wait = 1000; |
| 26 | const open = () => { |
| 27 | setState("connecting"); |
| 28 | ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${path}`); |
| 29 | ws.onopen = () => { |
| 30 | wait = 1000; |
| 31 | setState("live"); |
| 32 | ping = setInterval(() => ws?.readyState === WebSocket.OPEN && ws.send("ping"), 30_000); |
| 33 | }; |
| 34 | ws.onmessage = (event) => { |
| 35 | if (typeof event.data !== "string" || event.data === "pong") return; |
| 36 | try { |
| 37 | const message = JSON.parse(event.data) as { type: string; items?: LiveItem[]; dropped?: number }; |
| 38 | if (message.type === "items" && message.items) handler.current(message.items, message.dropped ?? 0); |
| 39 | } catch {} |
| 40 | }; |
| 41 | ws.onclose = () => { |
| 42 | clearInterval(ping); |
| 43 | if (closed) return; |
| 44 | setState("dropped"); |
| 45 | retry = setTimeout(open, wait); |
| 46 | wait = Math.min(wait * 2, 30_000); |
| 47 | }; |
| 48 | }; |
| 49 | open(); |
| 50 | return () => { |
| 51 | closed = true; |
| 52 | clearTimeout(retry); |
| 53 | clearInterval(ping); |
| 54 | ws?.close(); |
| 55 | }; |
| 56 | }, [path, on]); |
| 57 | return state; |
| 58 | } |