Files
pinkudex/components/video/VideoIndexProvider.tsx
2026-05-26 22:46:00 +02:00

75 lines
2.6 KiB
TypeScript

"use client";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { subscribeVideoStatusRefresh } from "./videoStatusEvents";
interface Status {
codes: Set<string>;
subtitleCodes: Set<string>;
count: number;
lastScannedAt: number;
rootsScanned: string[];
}
interface Ctx extends Status {
hasVideo: (code: string | null | undefined) => boolean;
hasSubtitle: (code: string | null | undefined) => boolean;
refresh: () => Promise<void>;
}
const empty: Status = { codes: new Set(), subtitleCodes: new Set(), count: 0, lastScannedAt: 0, rootsScanned: [] };
const VideoIdxCtx = createContext<Ctx | null>(null);
export function VideoIndexProvider({ children }: { children: React.ReactNode }) {
const [status, setStatus] = useState<Status>(empty);
const inflightRef = useRef<AbortController | null>(null);
const refresh = useCallback(async () => {
// Abort any prior fetch so a slow first request can't clobber a
// newer second request's result on settle order.
inflightRef.current?.abort();
const ctrl = new AbortController();
inflightRef.current = ctrl;
try {
const r = await fetch("/api/video-status", { cache: "no-store", signal: ctrl.signal });
if (!r.ok) return;
const j = await r.json();
if (ctrl.signal.aborted) return;
setStatus({
codes: new Set<string>(Array.isArray(j.codes) ? j.codes : []),
subtitleCodes: new Set<string>(Array.isArray(j.subtitleCodes) ? j.subtitleCodes : []),
count: j.count ?? 0,
lastScannedAt: j.lastScannedAt ?? 0,
rootsScanned: Array.isArray(j.rootsScanned) ? j.rootsScanned : [],
});
} catch {
// Silent — if the endpoint fails or aborts, no badges. No user-facing error.
} finally {
if (inflightRef.current === ctrl) inflightRef.current = null;
}
}, []);
useEffect(() => { refresh(); return () => { inflightRef.current?.abort(); }; }, [refresh]);
useEffect(() => subscribeVideoStatusRefresh(() => { refresh(); }), [refresh]);
const value = useMemo<Ctx>(() => ({
...status,
hasVideo: (code) => {
if (!code) return false;
return status.codes.has(code.toUpperCase());
},
hasSubtitle: (code) => {
if (!code) return false;
return status.subtitleCodes.has(code.toUpperCase());
},
refresh,
}), [status, refresh]);
return <VideoIdxCtx.Provider value={value}>{children}</VideoIdxCtx.Provider>;
}
export function useVideoIndex(): Ctx {
const ctx = useContext(VideoIdxCtx);
if (!ctx) throw new Error("useVideoIndex must be used within VideoIndexProvider");
return ctx;
}