/** * @file useLiveRefresh.ts * @description Keeps a page's data fresh without hammering the server: re-runs * `load` on matching WebSocket messages through a leading+trailing throttle (a * steady event stream can never freeze or flood the page), on a poll that skips * hidden tabs, when the tab becomes visible again, and after the socket * reconnects (messages sent while it was down are lost). The caller still does * its own initial load. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useRef } from "react"; import { eventBus } from "../lib/eventBus"; import { throttle } from "../lib/throttle"; import type { WSMessage } from "../lib/types"; export interface LiveRefreshOptions { /** Which WebSocket messages warrant a reload; none when omitted. */ match?: (msg: WSMessage) => boolean; /** Poll period while the tab is visible; no poll when omitted. */ intervalMs?: number; /** Minimum gap between event-driven reloads (default 3 s). */ throttleMs?: number; /** Off = no subscription, no poll (e.g. an inactive tab). Default true. */ enabled?: boolean; } export function useLiveRefresh(load: () => void, opts: LiveRefreshOptions = {}): void { const { intervalMs, throttleMs = 3000, enabled = true } = opts; const loadRef = useRef(load); const matchRef = useRef(opts.match); loadRef.current = load; matchRef.current = opts.match; useEffect(() => { if (!enabled) return; const live = throttle(() => loadRef.current(), throttleMs); const visible = () => document.visibilityState === "visible"; const offMsg = eventBus.subscribe((msg) => { if (matchRef.current?.(msg)) live(); }); let wasDown = !eventBus.connected; // (Guarded: test doubles of the bus often stub `subscribe` only.) const offConn = typeof eventBus.onConnection === "function" ? eventBus.onConnection((up) => { if (!up) wasDown = true; else if (wasDown) { wasDown = false; live(); } }) : () => undefined; const onVis = () => { if (visible()) live(); }; document.addEventListener("visibilitychange", onVis); const timer = intervalMs ? setInterval(() => { if (visible()) loadRef.current(); }, intervalMs) : undefined; return () => { live.cancel(); offMsg(); offConn(); document.removeEventListener("visibilitychange", onVis); clearInterval(timer); }; }, [enabled, intervalMs, throttleMs]); }