Files
AgentHub/client/src/hooks/useLiveRefresh.ts
T
nntrivi2001 03f1673fce feat(dashboard): lazy routes, Needs-you strip, URL state, exact numbers
- Workspace, SettingsHub, SessionDetail and the Workflow sub-tab are lazy
  (main bundle 2,166 kB -> ~920 kB), wrapped in RouteErrorBoundary: a stale
  chunk after a self-update reloads once (vite:preloadError too), any other
  error shows a Reload panel instead of a blank page.
- Live refresh: lib/throttle.ts + hooks/useLiveRefresh.ts replace trailing
  debounces that froze under a steady event stream; refresh skips hidden
  tabs and refetches after a WebSocket reconnect. Monitor loads only while
  shown, with one batched GET /api/agents?session_ids= instead of N+1; a
  failing endpoint is an inline error, not a blank Dashboard.
- NeedsYouStrip on Monitor and above Analytics. Tabs, filters, page, sort
  and search live in the URL (Dashboard, Sessions, Activity, SessionDetail).
- Sessions: tool / workspace / date-range filters, CSV/JSON export
  (formula-neutralised), focusable row links, Open in workspace (the dead
  /run links are gone), stale responses dropped, error + Retry.
- Fixes: "NaNs" durations, 30/7-day charts skipping idle days, weekday
  labels west of UTC (now averages), cache hit rate denominator, billable
  tokens vs cache reads, exact tool-call counts, "To" date = end of day,
  invalid ?from= no longer throws, recent list no longer shrinks to 15,
  stable message keys, transcript switch races.
- Light-theme-unreadable bg-black code panels and dark-only DAG fills use
  tokens; status configs and chart colours use theme tokens; Health tab
  and other literals translated.
- Server (additive): GET /api/agents?session_ids=, GET /api/sessions
  awaiting=1&from=&to=, stats tool_call_count/distinct_tool_count, event
  rows carry session/agent labels.
2026-10-05 13:22:04 +07:00

72 lines
2.5 KiB
TypeScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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]);
}