refactor(realtime): remove manual refresh, poll every hook

- delete useRefreshKey (no more manual refresh key plumbing)
- useAutoRefresh polling now covers every data hook:
  - events/tasks: 30s (high-churn feeds), visibility-aware
  - repos/agents/peers/network/home: 60s
- useNodesStatus re-validates every 60s instead of one-shot/session cache
- useAgents gains a 60s visibility-aware tick (re-uses session cache
  on first load, bypasses it on each tick)
- hooks no longer accept refreshKey; render-phase reset keys tick instead
This commit is contained in:
Adarsh Gourab Mahalik committed 2026-08-10 12:38:19 +05:30
1 parent 4837f99ef2
commit c71dd9cbe0
9 files changed
+35 -68

No files matched your search

+11 -9
View File
@@ -2,9 +2,10 @@ import { useState, useEffect, useMemo } from 'react';
import { fetchAgents } from '../lib/api';
import type { ApiAgent } from '../lib/api';
import { useDebouncedValue } from './useDebouncedValue';
import { useAutoRefresh } from './useAutoRefresh';
// The agents endpoint is unpaginated, so the full list is fetched once per
// session and paged/filtered client-side.
// refetch cycle and paged/filtered client-side.
let agentsCache: ApiAgent[] | null = null;
let agentsPromise: Promise<ApiAgent[]> | null = null;
@@ -38,7 +39,6 @@ interface Options {
page: number;
perPage: number;
search?: string;
refreshKey?: number;
}
interface Result {
@@ -51,24 +51,26 @@ interface Result {
error: string | null;
}
export function useAgents({ page, perPage, search = '', refreshKey = 0 }: Options): Result {
export function useAgents({ page, perPage, search = '' }: Options): Result {
const tick = useAutoRefresh(60_000, true);
const [all, setAll] = useState<ApiAgent[] | null>(agentsCache);
const [loading, setLoading] = useState(agentsCache === null);
const [error, setError] = useState<string | null>(null);
const debouncedSearch = useDebouncedValue(search.trim().toLowerCase(), 300);
// Render-phase reset on refresh (react-hooks/set-state-in-effect)
const [prevRefreshKey, setPrevRefreshKey] = useState(refreshKey);
if (prevRefreshKey !== refreshKey) {
setPrevRefreshKey(refreshKey);
// Render-phase reset on tick or when the search query resolves to a new page.
const fetchKey = `${tick}|${debouncedSearch}`;
const [prevFetchKey, setPrevFetchKey] = useState(fetchKey);
if (prevFetchKey !== fetchKey) {
setPrevFetchKey(fetchKey);
setLoading(true);
setError(null);
}
useEffect(() => {
let cancelled = false;
loadAgents(refreshKey > 0)
loadAgents(tick > 0)
.then(agents => {
if (cancelled) return;
setAll(agents);
@@ -80,7 +82,7 @@ export function useAgents({ page, perPage, search = '', refreshKey = 0 }: Option
setLoading(false);
});
return () => { cancelled = true; };
}, [refreshKey]);
}, [tick, debouncedSearch, page, perPage]);
const derived = useMemo(() => {
if (!all) {
+2 -8
View File
@@ -16,17 +16,11 @@ interface Result {
* coverage from the nodes' ref-update feeds. Unreachable nodes stay in the
* result (rendered as offline) and are excluded from replication analysis.
*/
export function useNetwork(refreshKey = 0): Result {
export function useNetwork(): Result {
const tick = useAutoRefresh(60_000, true);
const [snapshots, setSnapshots] = useState<NodeSnapshot[] | null>(null);
const [loading, setLoading] = useState(true);
const [prevRefreshKey, setPrevRefreshKey] = useState(refreshKey);
if (prevRefreshKey !== refreshKey) {
setPrevRefreshKey(refreshKey);
setLoading(true);
}
useEffect(() => {
const controller = new AbortController();
Promise.all(FEDERATED_NODES.map(n => fetchNodeSnapshot(n, controller.signal)))
@@ -40,7 +34,7 @@ export function useNetwork(refreshKey = 0): Result {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [refreshKey, tick]);
}, [tick]);
const replication = useMemo(() => {
if (!snapshots) return null;
+2 -9
View File
@@ -31,7 +31,7 @@ const nullOnError = <T,>(p: Promise<T>): Promise<T | null> => p.catch(() => null
* independently (a failed corner renders as "—", never breaks the page) and
* the whole sweep re-runs on a visibility-aware 60s tick.
*/
export function useNodeOverview(refreshKey = 0): NodeOverview {
export function useNodeOverview(): NodeOverview {
const tick = useAutoRefresh(60_000, true);
const [state, setState] = useState<Omit<NodeOverview, 'loading' | 'unreachable'>>({
node: null,
@@ -44,13 +44,6 @@ export function useNodeOverview(refreshKey = 0): NodeOverview {
});
const [loaded, setLoaded] = useState(false);
// Render-phase reset on manual refresh only (ticks refresh in place).
const [prevRefreshKey, setPrevRefreshKey] = useState(refreshKey);
if (prevRefreshKey !== refreshKey) {
setPrevRefreshKey(refreshKey);
setLoaded(false);
}
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
@@ -70,7 +63,7 @@ export function useNodeOverview(refreshKey = 0): NodeOverview {
});
return () => controller.abort();
}, [refreshKey, tick]);
}, [tick]);
const unreachable =
loaded &&
+8 -6
View File
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { fetchNodeInfo, fetchStats } from '../lib/api';
import type { NodeInfo, NodeStats } from '../lib/api';
import { useAutoRefresh } from './useAutoRefresh';
interface NodeStatus {
node: NodeInfo | null;
@@ -9,13 +10,14 @@ interface NodeStatus {
}
// One fetch per session — the chrome must never break the page, so failures
// simply leave the fields null.
// simply leave the fields null. On each visibility-aware tick the cache is
// bypassed so the chrome stays current.
type CachedStatus = { node: NodeInfo | null; stats: NodeStats | null };
let cached: CachedStatus | null = null;
let inflight: Promise<CachedStatus> | null = null;
function load(): Promise<CachedStatus> {
if (cached) return Promise.resolve(cached);
function load(force = false): Promise<CachedStatus> {
if (!force && cached) return Promise.resolve(cached);
if (!inflight) {
inflight = Promise.all([
fetchNodeInfo().catch(() => null),
@@ -31,6 +33,7 @@ function load(): Promise<CachedStatus> {
}
export function useNodeStatus(): NodeStatus {
const tick = useAutoRefresh(60_000, true);
const [status, setStatus] = useState<NodeStatus>({
node: cached?.node ?? null,
stats: cached?.stats ?? null,
@@ -38,15 +41,14 @@ export function useNodeStatus(): NodeStatus {
});
useEffect(() => {
if (cached) return;
let cancelled = false;
load().then(result => {
load(tick > 0).then(result => {
if (!cancelled && result) {
setStatus({ node: result.node, stats: result.stats, loading: false });
}
});
return () => { cancelled = true; };
}, []);
}, [tick]);
return status;
}
+2 -10
View File
@@ -8,7 +8,6 @@ interface Options {
page: number;
perPage: number;
search?: string;
refreshKey?: number;
}
interface Result {
@@ -29,7 +28,7 @@ interface Result {
* filtered/paged client-side, re-fetched on a visibility-aware 60s tick.
* Reachable peers sort first, then by most recent last_seen.
*/
export function usePeers({ page, perPage, search = '', refreshKey = 0 }: Options): Result {
export function usePeers({ page, perPage, search = '' }: Options): Result {
const tick = useAutoRefresh(60_000, true);
const [all, setAll] = useState<ApiPeer[] | null>(null);
const [p2p, setP2p] = useState<P2PInfo | null>(null);
@@ -38,13 +37,6 @@ export function usePeers({ page, perPage, search = '', refreshKey = 0 }: Options
const debouncedSearch = useDebouncedValue(search.trim().toLowerCase(), 300);
const [prevRefreshKey, setPrevRefreshKey] = useState(refreshKey);
if (prevRefreshKey !== refreshKey) {
setPrevRefreshKey(refreshKey);
setLoading(true);
setError(null);
}
useEffect(() => {
const controller = new AbortController();
Promise.all([
@@ -63,7 +55,7 @@ export function usePeers({ page, perPage, search = '', refreshKey = 0 }: Options
setLoading(false);
});
return () => controller.abort();
}, [refreshKey, tick]);
}, [tick]);
const derived = useMemo(() => {
if (!all) {
+2 -10
View File
@@ -13,7 +13,6 @@ interface Options {
source?: EventSourceFilter;
/** Auto-refresh the feed (visibility-aware, 30s). */
live?: boolean;
refreshKey?: number;
}
interface Result {
@@ -30,7 +29,7 @@ interface Result {
}
export function useRefUpdates({
page, perPage, search = '', source = 'all', live = true, refreshKey = 0,
page, perPage, search = '', source = 'all', live = true,
}: Options): Result {
const tick = useAutoRefresh(30_000, live);
const [all, setAll] = useState<ApiRefUpdate[] | null>(null);
@@ -39,13 +38,6 @@ export function useRefUpdates({
const debouncedSearch = useDebouncedValue(search.trim().toLowerCase(), 300);
const [prevRefreshKey, setPrevRefreshKey] = useState(refreshKey);
if (prevRefreshKey !== refreshKey) {
setPrevRefreshKey(refreshKey);
setLoading(true);
setError(null);
}
useEffect(() => {
const controller = new AbortController();
fetchRefUpdates(MAX_EVENT_LIMIT, controller.signal)
@@ -61,7 +53,7 @@ export function useRefUpdates({
setLoading(false);
});
return () => controller.abort();
}, [refreshKey, tick]);
}, [tick]);
const derived = useMemo(() => {
if (!all) {
-7
View File
@@ -1,7 +0,0 @@
import { useState, useCallback } from 'react';
export function useRefreshKey() {
const [refreshKey, setRefreshKey] = useState(0);
const refresh = useCallback(() => setRefreshKey(k => k + 1), []);
return { refreshKey, refresh };
}
+5 -5
View File
@@ -4,6 +4,7 @@ import type { ApiRepo, RepoSort } from '../lib/api';
import type { Repository } from '../types/repo';
import { DEFAULT_PER_PAGE } from '../lib/constants';
import { useDebouncedValue } from './useDebouncedValue';
import { useAutoRefresh } from './useAutoRefresh';
interface Options {
page: number;
@@ -11,7 +12,6 @@ interface Options {
search?: string;
sort?: RepoSort;
owner?: string;
refreshKey?: number;
}
interface Result {
@@ -55,8 +55,8 @@ export function useRepositories({
search = '',
sort = 'updated',
owner = '',
refreshKey = 0,
}: Options): Result {
const tick = useAutoRefresh(60_000, true);
const [apiRepos, setApiRepos] = useState<ApiRepo[] | null>(null);
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(true);
@@ -66,9 +66,9 @@ export function useRepositories({
const serverQuery = SERVER_SEARCH_ENABLED ? debouncedSearch : '';
const serverSort: RepoSort = SERVER_SEARCH_ENABLED ? sort : 'updated';
// Render-phase reset: flip to loading as soon as the query inputs change,
// Render-phase reset: flip to loading as soon as the query inputs or tick change,
// without a cascading effect (react-hooks/set-state-in-effect).
const fetchKey = `${page}|${perPage}|${owner}|${serverQuery}|${serverSort}|${refreshKey}`;
const fetchKey = `${page}|${perPage}|${owner}|${serverQuery}|${serverSort}|${tick}`;
const [prevFetchKey, setPrevFetchKey] = useState(fetchKey);
if (prevFetchKey !== fetchKey) {
setPrevFetchKey(fetchKey);
@@ -97,7 +97,7 @@ export function useRepositories({
setLoading(false);
});
return () => controller.abort();
}, [page, perPage, owner, serverQuery, serverSort, refreshKey]);
}, [page, perPage, owner, serverQuery, serverSort, tick]);
const derived = useMemo(() => {
if (!apiRepos) {
+3 -4
View File
@@ -10,7 +10,6 @@ interface Options {
/** Server-side filter — refetches when it changes. */
status?: TaskStatus;
search?: string;
refreshKey?: number;
}
interface Result {
@@ -24,7 +23,7 @@ interface Result {
error: string | null;
}
export function useTasks({ page, perPage, status, search = '', refreshKey = 0 }: Options): Result {
export function useTasks({ page, perPage, status, search = '' }: Options): Result {
const tick = useAutoRefresh(30_000, true);
const [all, setAll] = useState<ApiTask[] | null>(null);
const [loading, setLoading] = useState(true);
@@ -33,7 +32,7 @@ export function useTasks({ page, perPage, status, search = '', refreshKey = 0 }:
const debouncedSearch = useDebouncedValue(search.trim().toLowerCase(), 300);
// Render-phase reset when the fetch key changes (manual refresh or filter).
const fetchKey = `${refreshKey}|${status ?? ''}`;
const fetchKey = `${tick}|${status ?? ''}`;
const [prevFetchKey, setPrevFetchKey] = useState(fetchKey);
if (prevFetchKey !== fetchKey) {
setPrevFetchKey(fetchKey);
@@ -56,7 +55,7 @@ export function useTasks({ page, perPage, status, search = '', refreshKey = 0 }:
setLoading(false);
});
return () => controller.abort();
}, [refreshKey, status, tick]);
}, [tick, status]);
const derived = useMemo(() => {
if (!all) {