55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||
|
|
import { getChainHealth, getLatestBlock, type ChainHealth, type LatestBlock } from '../services/chain138';
|
||
|
|
import { getExplorerStats, type ExplorerStats } from '../services/explorer';
|
||
|
|
|
||
|
|
export interface LiveChainState {
|
||
|
|
health: ChainHealth | null;
|
||
|
|
latestBlock: LatestBlock | null;
|
||
|
|
stats: ExplorerStats | null;
|
||
|
|
loading: boolean;
|
||
|
|
error: string | null;
|
||
|
|
lastUpdated: Date | null;
|
||
|
|
refresh: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Polls chain-138 RPC + SolaceScan explorer every `pollMs` (default 12s).
|
||
|
|
* Returns `null` values while loading the first time; never throws.
|
||
|
|
*/
|
||
|
|
export function useLiveChain(pollMs = 12_000): LiveChainState {
|
||
|
|
const [health, setHealth] = useState<ChainHealth | null>(null);
|
||
|
|
const [latestBlock, setLatestBlock] = useState<LatestBlock | null>(null);
|
||
|
|
const [stats, setStats] = useState<ExplorerStats | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||
|
|
const mounted = useRef(true);
|
||
|
|
|
||
|
|
const tick = useCallback(async () => {
|
||
|
|
try {
|
||
|
|
const [h, b, s] = await Promise.allSettled([getChainHealth(), getLatestBlock(), getExplorerStats()]);
|
||
|
|
if (!mounted.current) return;
|
||
|
|
if (h.status === 'fulfilled') setHealth(h.value);
|
||
|
|
if (b.status === 'fulfilled') setLatestBlock(b.value);
|
||
|
|
if (s.status === 'fulfilled') setStats(s.value);
|
||
|
|
const anyError = [h, b, s].find(r => r.status === 'rejected') as PromiseRejectedResult | undefined;
|
||
|
|
setError(anyError ? String(anyError.reason?.message ?? anyError.reason) : null);
|
||
|
|
setLastUpdated(new Date());
|
||
|
|
} catch (e) {
|
||
|
|
if (!mounted.current) return;
|
||
|
|
setError(e instanceof Error ? e.message : String(e));
|
||
|
|
} finally {
|
||
|
|
if (mounted.current) setLoading(false);
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
mounted.current = true;
|
||
|
|
void tick();
|
||
|
|
const id = setInterval(tick, pollMs);
|
||
|
|
return () => { mounted.current = false; clearInterval(id); };
|
||
|
|
}, [tick, pollMs]);
|
||
|
|
|
||
|
|
return { health, latestBlock, stats, loading, error, lastUpdated, refresh: () => { void tick(); } };
|
||
|
|
}
|