"use client"; import { useEffect, useRef } from "react"; /** One stream per board/thread; open replies refresh through the board revision. */ export function useCommentEvents(scope: { target: string } | { admin: true }, refresh: (changed: boolean) => Promise) { const refreshRef = useRef(refresh); useEffect(() => { refreshRef.current = refresh; }, [refresh]); const query = "target" in scope ? `target=${encodeURIComponent(scope.target)}` : "scope=admin"; useEffect(() => { let source: EventSource | null = null; let timer: ReturnType | undefined; let refreshing = false; let dirty = false; let pendingChange = false; let connectedBefore = false; let disposed = false; async function flush() { timer = undefined; if (disposed) return; if (refreshing) { dirty = true; return; } refreshing = true; const changed = pendingChange; pendingChange = false; try { await refreshRef.current(changed); } finally { refreshing = false; if (dirty && !disposed) { dirty = false; schedule(); } } } function schedule(changed = false) { pendingChange ||= changed; if (!timer && !disposed) timer = setTimeout(() => { void flush(); }, 250); } function connect() { source?.close(); source = null; if (document.visibilityState !== "visible") return; source = new EventSource(`/api/comments/events?${query}`); source.addEventListener("changed", () => schedule(true)); // Catch updates between the initial page response and stream subscription, // and updates missed while disconnected. EventSource handles reconnects. source.addEventListener("open", () => { // A reconnect can have missed reply edits even if the root is unchanged. schedule(connectedBefore); connectedBefore = true; }); } connect(); document.addEventListener("visibilitychange", connect); return () => { disposed = true; source?.close(); if (timer) clearTimeout(timer); document.removeEventListener("visibilitychange", connect); }; }, [query]); }