import { useRouter } from "@tanstack/react-router"; import { useEffect, useState } from "react"; const RECONNECT_MS = 3000; const PING_MS = 30000; type LiveMessage = { type: "changed"; actor: string } | { type: "presence"; viewers: string[] }; function parse(raw: string): LiveMessage | null { try { const value: unknown = JSON.parse(raw); if (typeof value !== "object" || value === null || !("type" in value)) return null; if (value.type === "changed" && "actor" in value && typeof value.actor === "string") return { type: "changed", actor: value.actor }; if (value.type === "presence" && "viewers" in value && Array.isArray(value.viewers)) return { type: "presence", viewers: value.viewers.filter((name): name is string => typeof name === "string") }; return null; } catch { return null; } } /** Subscribes to the project's Durable Object room: refreshes on remote edits and reports who is viewing. */ export function useProjectLive(owner: string, number: number) { const router = useRouter(); const [viewers, setViewers] = useState([]); const [connected, setConnected] = useState(false); useEffect(() => { let socket: WebSocket | null = null; let closed = false; let retry: ReturnType | undefined; let ping: ReturnType | undefined; const connect = () => { const scheme = window.location.protocol === "https:" ? "wss" : "ws"; socket = new WebSocket(`${scheme}://${window.location.host}/projects/${owner}/${number}/live`); socket.onopen = () => setConnected(true); socket.onmessage = (event) => { const message = typeof event.data === "string" ? parse(event.data) : null; if (message?.type === "presence") setViewers(message.viewers); if (message?.type === "changed") void router.invalidate(); }; socket.onclose = () => { setConnected(false); if (!closed) retry = setTimeout(connect, RECONNECT_MS); }; }; connect(); ping = setInterval(() => socket?.readyState === WebSocket.OPEN && socket.send("ping"), PING_MS); return () => { closed = true; clearTimeout(retry); clearInterval(ping); socket?.close(); }; }, [owner, number, router]); return { viewers, connected }; }