import { useEffect, useRef } from 'react'; import { useFetchFeed } from './useFetchFeed'; type AdaptiveMatchFeedState = { data: T | undefined; loading: boolean; error: string | undefined; }; type AdaptiveMatchFeedOptions = { /** Polling interval while the match is live ('Playing'). Default 30000 (30s). */ liveInterval?: number; /** Polling interval pre-match (error/403 or 'Fixture'/'Suspended'). Default 60000 (60s). */ preMatchInterval?: number; /** Max consecutive errors before stopping polling. Default 10. */ maxRetries?: number; }; const TERMINAL_STATUSES = ['Played', 'Cancelled', 'Postponed', 'Abandoned']; /** * Adaptive polling hook for Opta match feeds. * * - Fetches immediately on mount. * - Polls at `preMatchInterval` when waiting for kick-off (error/403 or 'Fixture'/'Suspended'). * - Polls at `liveInterval` when match is live ('Playing'). * - Stops polling when match is terminal ('Played', 'Cancelled', 'Postponed', 'Abandoned'). * - Stops polling after `maxRetries` consecutive errors (prevents infinite polling for bad URLs). * * Expects the feed to have the shape: `{ liveData?: { matchDetails?: { matchStatus?: string } } }` */ export const useAdaptiveMatchFeed = < T extends { liveData?: { matchDetails?: { matchStatus?: string } }; } >( url: string | undefined, options?: AdaptiveMatchFeedOptions ): AdaptiveMatchFeedState => { const liveInterval = (options && options.liveInterval) || 30000; const preMatchInterval = (options && options.preMatchInterval) || 60000; const maxRetries = (options && options.maxRetries) || 10; // Track consecutive error count across renders. // We count by detecting loading transitions: loading went from true→false with an error // means one failed attempt completed. const errorCountRef = useRef(0); const wasLoadingRef = useRef(false); // Reset error count when URL changes so a new match gets fresh retries. useEffect( () => { errorCountRef.current = 0; wasLoadingRef.current = false; }, [url] ); // Initial fetch — always fires immediately, no polling. const { data: initialData, loading, error } = useFetchFeed(url); // Determine matchStatus from whatever data we have. const matchStatus = initialData && initialData.liveData && initialData.liveData.matchDetails && initialData.liveData.matchDetails.matchStatus; // Detect completed fetch attempts by tracking loading→not-loading transitions. useEffect( () => { if (wasLoadingRef.current && !loading) { if (error && !initialData) { errorCountRef.current += 1; } else { errorCountRef.current = 0; } } wasLoadingRef.current = loading; }, [loading, error, initialData] ); const isTerminal = matchStatus ? TERMINAL_STATUSES.indexOf(matchStatus) !== -1 : false; const maxRetriesExceeded = errorCountRef.current >= maxRetries; // Determine polling interval. let pollingInterval: number | undefined; if (isTerminal || maxRetriesExceeded) { pollingInterval = undefined; } else if (matchStatus === 'Playing') { pollingInterval = liveInterval; } else { // Covers: 'Fixture', 'Suspended', undefined (403 error), or any unknown status pollingInterval = preMatchInterval; } // Second hook — handles adaptive polling. When pollingInterval changes, // the hook re-subscribes with the new interval. const { data: polledData } = useFetchFeed( pollingInterval ? url : undefined, { pollingInterval } ); const data = polledData || initialData; return { data, loading, error }; };