import { useEffect, useRef } from "react"; import { useStore } from "../state/store.tsx"; import { batchFetch as defaultBatchFetch } from "../operations/batch.ts"; import { bunGitService } from "../services/git.ts"; import { updateProjectRemoteStatus } from "../state/actions.ts"; import type { GitforestConfig, Project, BatchResult, RemoteStatus } from "../types/index.ts"; /** * Dependencies that can be injected for testing */ export interface BackgroundFetchDeps { batchFetch: (projects: Project[], options?: { concurrency?: number }) => Promise; getRemoteStatus: (path: string) => Promise; } export function useBackgroundFetch( config: GitforestConfig, deps?: Partial, ) { const { state, dispatch } = useStore(); const batchFetch = deps?.batchFetch ?? defaultBatchFetch; const getRemoteStatus = deps?.getRemoteStatus ?? bunGitService.getRemoteStatus.bind(bunGitService); const timerRef = useRef | null>(null); const isFetchingRef = useRef(false); // Use config value or fallback to default const intervalMs = (config.cache.backgroundRefreshIntervalSeconds ?? 300) * 1000; const isEnabled = config.cache.enableBackgroundRefresh !== false; useEffect(() => { // Don't run if disabled or while loading or during an action if (!isEnabled || state.isLoading || state.actionInProgress) { return; } const runBackgroundFetch = async () => { // Prevent concurrent fetches if (isFetchingRef.current || state.actionInProgress) return; isFetchingRef.current = true; const gitProjects = state.projects.filter( (p) => p.type === "git" && p.status?.hasRemote ); if (gitProjects.length === 0) { isFetchingRef.current = false; return; } try { // Silently fetch in background await batchFetch(gitProjects, { concurrency: config.scan.concurrency }); // Dispatch per-project remote-status updates instead of triggering a // full re-scan. Per ADR-0005, only the tracking-ref-derived fields // can have changed during a fetch — local fields are untouched. await Promise.all( gitProjects.map(async (p) => { try { const remote = await getRemoteStatus(p.path); dispatch(updateProjectRemoteStatus(p.path, remote)); } catch { // Best-effort: skip the project on failure } }) ); } finally { isFetchingRef.current = false; } }; // Set up interval timerRef.current = setInterval(runBackgroundFetch, intervalMs); // Run initial fetch after a short delay const initialTimeout = setTimeout(runBackgroundFetch, 10000); // 10 seconds after start return () => { if (timerRef.current) { clearInterval(timerRef.current); } clearTimeout(initialTimeout); }; }, [state.isLoading, state.actionInProgress, state.projects, config.scan.concurrency, intervalMs, isEnabled, dispatch, batchFetch, getRemoteStatus]); return null; }