import { useEffect, useRef } from "react"; import type { QueryClient } from "@tanstack/react-query"; import { qk } from "./queryKeys"; import { type ChannelDetail } from "./api"; /** How often the channel page re-polls its detail (via useLiveQuery): every 4s while THIS user's * full-history backfill is running (`deep_requested && !backfill_done`) so the stored count + the * N/S/L breakdown climb on their own, and `false` (stop polling) once the back-catalogue is in or * the user never requested it. Extracted so the poll-or-stop decision is unit-testable. */ export const channelDetailPollMs = (d: ChannelDetail | undefined): number | false => d?.deep_requested && !d.backfill_done ? 4000 : false; /** Did the poll above just watch the running backfill change this channel's catalogue? * * Only the poll can tell: the backfill runs in a BackgroundTask, so the PATCH that STARTS it * returns before it has written anything, and it emits no event of its own. The window is the * same one the poll runs in (`deep_requested && !backfill_done` on the PREVIOUS reading), so a * detail refetch triggered by some other mutation — which invalidates the feed itself — can't * come through here twice. Completion counts even with an unchanged count: the last page can be * all-duplicates and still leave the grid's `hasNextPage` reading from a one-page catalogue. */ const backfillChangedCatalog = ( prev: ChannelDetail | undefined, next: ChannelDetail | undefined, ): boolean => !!prev && !!next && prev.id === next.id && prev.deep_requested && !prev.backfill_done && (next.stored_videos !== prev.stored_videos || next.backfill_done); /** Keep the channel page's video grid in step with the backfill its detail poll is watching. * * Without this the grid is frozen until an F5: the feed's infinite query has no interval, and the * app-wide defaults are `staleTime: 30s` + `refetchOnWindowFocus: false`, so nothing refetches it * on its own. "Get full history" invalidates the feed on the PATCH RESPONSE — before the * background job has written a row — which is why the list stayed on the ~19 cards the channel had * at subscribe time while the count above it climbed into the thousands. * * What it costs, honestly: an invalidation refetches EVERY loaded page of the grid's infinite * query — one request per page the viewer has scrolled through, on each 4 s tick where the count * moved, for as long as the backfill runs (the poll pauses while the tab is hidden, and stops when * the back-catalogue is in). That is the right price, not an accident: backfilled videos are OLDER * than what the grid opened with, so under the newest-first order they land in the pages BELOW the * head — refreshing only the first page would leave out exactly the rows the backfill wrote. The * head itself comes back structurally identical and re-renders nothing. */ export function useBackfillFeedRefresh(detail: ChannelDetail | undefined, qc: QueryClient) { const seen = useRef(undefined); useEffect(() => { const prev = seen.current; seen.current = detail; if (!backfillChangedCatalog(prev, detail)) return; qc.invalidateQueries({ queryKey: qk.feed() }); // …and the count pill beside it, which reads the same catalogue through its own query. qc.invalidateQueries({ queryKey: qk.feedSummary() }); }, [detail, qc]); }