import { useCallback, useEffect, useRef, useState } from 'react'; import { getChannelStates, messageFromError, type ChannelState } from './channels'; import { request } from './client'; import type { components } from './generated/v1'; export type ChannelGuide = components['schemas']['ChannelGuideResponseModel']; export type ChannelGuideChannel = components['schemas']['ChannelGuideChannelResponseModel']; export type ChannelGuideProgramme = components['schemas']['ChannelGuideProgrammeResponseModel']; export interface GuideScreenData { channelStates: ChannelState[]; guide: ChannelGuide; } export type GuideScreenQueryState = | { data: GuideScreenData; error: null; refresh: () => void; setWindowStart: (start: Date) => void; status: 'success'; windowStart: Date; windowEnd: Date; } | { data: null; error: string; refresh: () => void; status: 'error'; windowStart: Date; windowEnd: Date } | { data: null; error: null; refresh: () => void; status: 'loading'; windowStart: Date; windowEnd: Date }; type GuideScreenState = | { data: GuideScreenData; error: null; status: 'success' } | { data: null; error: string; status: 'error' } | { data: null; error: null; status: 'loading' }; export const GUIDE_PAST_MS = 60 * 60 * 1000; export const GUIDE_WINDOW_MS = 13 * 60 * 60 * 1000; export function defaultGuideWindowStart(now = new Date()): Date { return new Date(now.getTime() - GUIDE_PAST_MS); } export function getGuide(start: Date, end: Date): Promise { const query = new URLSearchParams({ start: start.toISOString(), end: end.toISOString() }); return request(`/api/v1/guide?${query.toString()}`); } export async function getGuideScreenData(start: Date, end: Date): Promise { const guide = await getGuide(start, end); const channelStates = await getChannelStates().catch(() => []); return { channelStates, guide }; } export function useGuideScreenQuery(channelStatePollMs = 30000): GuideScreenQueryState { const [windowStart, setWindowStartState] = useState(() => defaultGuideWindowStart()); const windowStartRef = useRef(windowStart); const [state, setState] = useState({ data: null, error: null, status: 'loading' }); const activeRef = useRef(true); const requestIdRef = useRef(0); useEffect(() => { activeRef.current = true; return () => { activeRef.current = false; }; }, []); const windowEnd = new Date(windowStart.getTime() + GUIDE_WINDOW_MS); const load = useCallback((start = windowStartRef.current) => { const end = new Date(start.getTime() + GUIDE_WINDOW_MS); const requestId = requestIdRef.current + 1; requestIdRef.current = requestId; getGuideScreenData(start, end) .then((data) => { if ( activeRef.current && requestIdRef.current === requestId && windowStartRef.current.getTime() === start.getTime() ) { setState({ data, error: null, status: 'success' }); } }) .catch((error: unknown) => { if ( activeRef.current && requestIdRef.current === requestId && windowStartRef.current.getTime() === start.getTime() ) { setState({ data: null, error: messageFromError(error), status: 'error' }); } }); }, []); const loadChannelStates = useCallback(() => { getChannelStates() .then((channelStates) => { if (!activeRef.current) { return; } setState((current) => { if (current.status !== 'success') { return current; } return { data: { ...current.data, channelStates }, error: null, status: 'success' }; }); }) .catch(() => { // Keep the guide visible when the optional live-state refresh fails. }); }, []); useEffect(() => { load(windowStart); }, [load, windowStart]); useEffect(() => { const intervalId = window.setInterval(loadChannelStates, Math.max(channelStatePollMs, 30000)); return () => { window.clearInterval(intervalId); }; }, [channelStatePollMs, loadChannelStates]); const setWindowStart = useCallback((start: Date) => { windowStartRef.current = start; setState({ data: null, error: null, status: 'loading' }); setWindowStartState(start); }, []); const refresh = useCallback(() => { setState({ data: null, error: null, status: 'loading' }); load(windowStartRef.current); }, [load]); if (state.status === 'success') { return { data: state.data, error: null, refresh, setWindowStart, status: 'success', windowEnd, windowStart }; } if (state.status === 'error') { return { data: null, error: state.error, refresh, status: 'error', windowEnd, windowStart }; } return { data: null, error: null, refresh, status: 'loading', windowEnd, windowStart }; }