mirror of
https://github.com/Twigpine/node-explorer.git
synced 2026-11-10 13:11:36 -10:00
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:
1 parent
4837f99ef2
commit
c71dd9cbe0
9 files changed
+35
-68
No files matched your search
+11
-9
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user