import { useState, useEffect, useRef } from 'react'; type FetchState = { data: T | undefined; loading: boolean; error: string | undefined; }; type UseFetchFeedOptions = { pollingInterval?: number; }; // --------------------------------------------------------------------------- // Module-level request deduplication cache // // When multiple component instances call useFetchFeed with the same URL, they // share a single in-flight request and cached result. For polling, only one // interval runs per unique URL regardless of how many subscribers exist. // --------------------------------------------------------------------------- type CacheEntry = { /** Latest resolved data (any JSON payload). */ data: any; /** Latest error message, if any. */ error: string | undefined; /** Whether a fetch is currently in-flight. */ fetching: boolean; /** The in-flight fetch promise (so late subscribers can await it). */ promise: Promise | null; /** AbortController for the current in-flight request. */ controller: AbortController | null; /** Number of mounted hook instances subscribed to this URL. */ subscribers: number; /** Active polling interval id (one per URL). */ pollTimer: ReturnType | null; /** The polling interval (ms) requested by subscribers. Uses the shortest. */ pollInterval: number | undefined; /** Map of setState dispatchers → polling interval (undefined = no polling). */ listeners: Map<(s: FetchState) => void, number | undefined>; }; const cache = new Map(); function getOrCreateEntry(url: string): CacheEntry { let entry = cache.get(url); if (!entry) { entry = { data: undefined, error: undefined, fetching: false, promise: null, controller: null, subscribers: 0, pollTimer: null, pollInterval: undefined, listeners: new Map() }; cache.set(url, entry); } return entry; } /** Notify every subscriber about the current state of this cache entry. */ function notify(entry: CacheEntry): void { const snapshot: FetchState = { data: entry.data, loading: entry.fetching, error: entry.error }; entry.listeners.forEach((_, fn) => fn(snapshot)); } /** Execute a single fetch for `url`, de-duplicating in-flight requests. */ function fetchForEntry(url: string, isInitial: boolean): Promise { const entry = getOrCreateEntry(url); // If a request is already in-flight, piggy-back on it. if (entry.fetching && entry.promise) { return entry.promise; } entry.fetching = true; if (isInitial) { entry.error = undefined; } notify(entry); const controller = new AbortController(); entry.controller = controller; const promise = fetch(url, { signal: controller.signal }) .then(res => { if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return res.json(); }) .then(data => { if (!controller.signal.aborted) { entry.data = data; entry.error = undefined; } }) .catch(err => { if (!controller.signal.aborted) { entry.error = err.message || 'Fetch failed'; } }) .then(() => { if (!controller.signal.aborted) { entry.fetching = false; entry.promise = null; entry.controller = null; notify(entry); } }); entry.promise = promise; return promise; } /** Ensure a single polling interval is running for `url`. */ function ensurePolling(url: string): void { const entry = cache.get(url); if (!entry || !entry.pollInterval || entry.pollInterval <= 0) { return; } // If a timer is already running at the correct interval, do nothing. if (entry.pollTimer) { return; } entry.pollTimer = setInterval(() => { fetchForEntry(url, false); }, entry.pollInterval); } function stopPolling(entry: CacheEntry): void { if (entry.pollTimer) { clearInterval(entry.pollTimer); entry.pollTimer = null; } } // --------------------------------------------------------------------------- // Public hook — API is identical to the original implementation. // --------------------------------------------------------------------------- export const useFetchFeed = ( url: string | undefined, options?: UseFetchFeedOptions ): FetchState => { const pollingInterval = options && options.pollingInterval; const [state, setState] = useState>(() => { if (!url) { return { data: undefined, loading: false, error: undefined }; } const existing = cache.get(url); if (existing && existing.data !== undefined) { return { data: existing.data as T, loading: existing.fetching, error: existing.error }; } return { data: undefined, loading: true, error: undefined }; }); // Keep a ref to the latest setState so we can add/remove it from the // entry's listener set without re-running the effect on every render. const setStateRef = useRef(setState); setStateRef.current = setState; useEffect( () => { if (!url) { setState({ data: undefined, loading: false, error: undefined }); return; } const entry = getOrCreateEntry(url); entry.subscribers += 1; // Stable listener wrapper that always calls the latest setState. const listener = (s: FetchState) => setStateRef.current(s as FetchState); entry.listeners.set(listener, pollingInterval); // If another subscriber already has data, sync immediately. if (entry.data !== undefined) { setState({ data: entry.data as T, loading: entry.fetching, error: entry.error }); } // Recalculate polling from all listeners. { const intervals = Array.from(entry.listeners.values()).filter( (v): v is number => v != null && v > 0 ); const shortest = intervals.length > 0 ? Math.min(...intervals) : undefined; if (shortest !== entry.pollInterval) { stopPolling(entry); entry.pollInterval = shortest; } } // Trigger an initial fetch (deduped if already in-flight). fetchForEntry(url, true); ensurePolling(url); return () => { entry.listeners.delete(listener); entry.subscribers -= 1; if (entry.subscribers <= 0) { // Last subscriber unmounted — clean up everything. stopPolling(entry); entry.pollInterval = undefined; if (entry.controller) { entry.controller.abort(); } cache.delete(url); } else { // Recalculate polling from remaining subscribers. const remaining = Array.from(entry.listeners.values()).filter( (v): v is number => v != null && v > 0 ); const shortest = remaining.length > 0 ? Math.min(...remaining) : undefined; if (shortest !== entry.pollInterval) { stopPolling(entry); entry.pollInterval = shortest; if (shortest) { ensurePolling(url); } } } }; }, [url, pollingInterval] ); return state; };