160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
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<ChannelGuide> {
|
|
const query = new URLSearchParams({
|
|
start: start.toISOString(),
|
|
end: end.toISOString()
|
|
});
|
|
|
|
return request<ChannelGuide>(`/api/guide?${query.toString()}`);
|
|
}
|
|
|
|
export async function getGuideScreenData(start: Date, end: Date): Promise<GuideScreenData> {
|
|
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<GuideScreenState>({
|
|
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 };
|
|
}
|