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<string[]>([]);
const [connected, setConnected] = useState(false);
useEffect(() => {
let socket: WebSocket | null = null;
let closed = false;
let retry: ReturnType<typeof setTimeout> | undefined;
let ping: ReturnType<typeof setInterval> | 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);