From e6cb823c71da12863a9fab26c7d12acd1edf1417 Mon Sep 17 00:00:00 2001 From: satyakwok <119509589+satyakwok@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:21:23 +0200 Subject: [PATCH] feat(scan): refresh validators and tokens on new blocks via websocket Both pages waited a full poll interval (15s and 30s) to reflect chain changes. Added useRefetchOnNewBlock, which refetches shortly after each new block from the existing newHeads websocket, throttled to once per 10s so fast block times do not flood the per-IP request budget. Mirrors the home page live-refresh trigger. --- apps/scan/app/[locale]/tokens/page.tsx | 5 ++++- apps/scan/app/[locale]/validators/page.tsx | 5 ++++- apps/scan/lib/ws.ts | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/scan/app/[locale]/tokens/page.tsx b/apps/scan/app/[locale]/tokens/page.tsx index 6756372..5157972 100644 --- a/apps/scan/app/[locale]/tokens/page.tsx +++ b/apps/scan/app/[locale]/tokens/page.tsx @@ -14,6 +14,7 @@ import { EmptyState } from "@/components/common/EmptyState"; import { FetchError } from "@/components/common/FetchError"; import { useNetwork, useNetworkFromQuery } from "@/lib/network-context"; import { useTokens } from "@/lib/hooks"; +import { useRefetchOnNewBlock } from "@/lib/ws"; import { formatNumber, shortenAddress } from "@/lib/format"; type SortKey = "supply" | "holders" | "transfers" | "none"; @@ -26,7 +27,9 @@ export default function TokensPage() { // Deeplink network switch. useNetworkFromQuery(); const searchParams = useSearchParams(); - const { data: tokens, loading, error, retry } = useTokens(network); + const { data: tokens, loading, error, retry, refetch } = useTokens(network); + // Refresh shortly after each new block instead of waiting for the 30s poll. + useRefetchOnNewBlock(network, refetch); const [sortKey, setSortKey] = useState("none"); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); // ?standard=tokenop|evm pre-selects the filter so the Native rail's diff --git a/apps/scan/app/[locale]/validators/page.tsx b/apps/scan/app/[locale]/validators/page.tsx index ee36458..2f22d9c 100644 --- a/apps/scan/app/[locale]/validators/page.tsx +++ b/apps/scan/app/[locale]/validators/page.tsx @@ -13,6 +13,7 @@ import { StatCard } from "@/components/common/StatCard"; import { EmptyState } from "@/components/common/EmptyState"; import { useNetwork, useNetworkFromQuery } from "@/lib/network-context"; import { useValidators } from "@/lib/hooks"; +import { useRefetchOnNewBlock } from "@/lib/ws"; import { formatNumber } from "@/lib/format"; type SortKey = "blocks" | "stake" | "uptime" | "none"; @@ -32,7 +33,9 @@ export default function ValidatorsPage() { const { network } = useNetwork(); // Deeplink network switch. useNetworkFromQuery(); - const { data: validators, loading, error, retry } = useValidators(network); + const { data: validators, loading, error, retry, refetch } = useValidators(network); + // Refresh shortly after each new block instead of waiting for the poll cycle. + useRefetchOnNewBlock(network, refetch); const [sortKey, setSortKey] = useState("none"); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); const [statusFilter, setStatusFilter] = useState("all"); diff --git a/apps/scan/lib/ws.ts b/apps/scan/lib/ws.ts index 5314dc2..bcfd4c5 100644 --- a/apps/scan/lib/ws.ts +++ b/apps/scan/lib/ws.ts @@ -160,6 +160,28 @@ export function useLatestBlock(network: NetworkId): NewHeadEvent | null { return head; } +// Trigger `refetch` shortly after each new block so a page reflects chain +// changes without waiting for its full poll interval. Throttled to at most one +// call per `minIntervalMs`: fast block times (testnet ~0.5s) would otherwise +// turn this into a request flood for data that changes slowly (validator set, +// token list), blowing the per-IP rate budget the poll intervals are sized for. +export function useRefetchOnNewBlock( + network: NetworkId, + refetch: () => void, + minIntervalMs = 10_000, +): void { + const head = useLatestBlock(network); + const lastRun = useRef(0); + useEffect(() => { + if (!head) return; + const now = Date.now(); + if (now - lastRun.current < minIntervalMs) return; + lastRun.current = now; + refetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [head?.number, minIntervalMs]); +} + export function useLatestFinalized(network: NetworkId): number | null { const [height, setHeight] = useState(null); useEffect(() => {