import { useEffect, useRef } from 'react'; import { fetchAllFeeds } from '../../fetchers/feed'; import { isTuiShutdownError, useTuiLifecycle } from '../lifecycle'; import { resolveAutoFetchThresholdMs, shouldAutoFetch } from '../state/auto-fetch'; import { dispatchError } from '../state/context'; import type { Action, AppState } from '../state/types'; import { getArticleLoadState, isSameArticleLoadState, refreshVisibleData, runFeedFetchSession } from './useDataLoader'; /** * On TUI open, auto-fetch all feeds if the latest fetch is stale (or has * never happened). Threshold defaults to 1 hour, configurable via * `RSS_AUTO_FETCH_HOURS` (set `0` to disable). Skipped under tests. * * Fires once per session — even if the user keeps the TUI open past the * threshold, we don't auto-fetch again. Manual `f`/`F` still works. */ export function useAutoFetch(state: AppState, dispatch: React.Dispatch): void { const lifecycle = useTuiLifecycle(); const triggeredRef = useRef(false); const stateRef = useRef(state); const mountedRef = useRef(true); useEffect(() => { stateRef.current = state; }, [state]); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { if (triggeredRef.current) return; if (process.env.NODE_ENV === 'test' || process.env.RSS_SKIP_AUTO_FETCH === '1') return; if (state.loading) return; if (state.feeds.length === 0) return; const thresholdMs = resolveAutoFetchThresholdMs(process.env.RSS_AUTO_FETCH_HOURS); if (!shouldAutoFetch(state.lastFetchedAt, thresholdMs, new Date())) { triggeredRef.current = true; return; } triggeredRef.current = true; void lifecycle .run((signal) => runAutoFetch( () => stateRef.current, dispatch, () => mountedRef.current && lifecycle.isAcceptingWork(), signal, ), ) .catch((error) => { if (!isTuiShutdownError(error) && mountedRef.current) dispatchError(dispatch, error); }); }, [state, dispatch, lifecycle]); } export async function runAutoFetch( getState: () => AppState, dispatch: React.Dispatch, canDispatch: () => boolean = () => true, signal?: AbortSignal, ): Promise { try { await runFeedFetchSession( dispatch, async (reportProgress) => { await fetchAllFeeds({ signal }, (progress) => { reportProgress({ current: progress.current, total: progress.total, feedTitle: progress.feed?.title, }); }); if (!canDispatch()) return; const criteria = getArticleLoadState(getState()); await refreshVisibleData(criteria, dispatch, { canDispatch: () => canDispatch() && isSameArticleLoadState(criteria, getArticleLoadState(getState())), }); }, canDispatch, ); } catch (err) { if (!signal?.aborted && canDispatch()) dispatchError(dispatch, err); } }