import type { QueryClient } from "@tanstack/react-query"; import { qk } from "./queryKeys"; import type { Video } from "./api"; // Every cached list that carries feed videos is one of two shapes: a single object with `items` // (a playlist detail) or an infinite query's `{ pages: [...] }` over those objects. Narrow // structurally rather than by type name so a new list surface is covered the day it is added. type ItemList = { items: Video[] }; type Cached = Partial & { pages?: unknown[] }; const isItemList = (v: unknown): v is ItemList => !!v && typeof v === "object" && Array.isArray((v as ItemList).items); /** * Write server-stored resume positions into every cached list holding those videos. * * The player checkpoints progress every few seconds, but until this ran, nothing wrote the new * position back into react-query — so the feed cache kept the value it was fetched with, a reopen * of the same card resumed from THAT stale value, and the next checkpoint then persisted the older * position over the newer one (measured: 830s → reopen → 751s). The Plex player already solved the * same problem for its detail query (`plexPlayerHooks`, "reopening the same item in the SAME * session read react-query's CACHED detail"); this is the feed-side twin. * * `positions` must hold what the SERVER stored (`api.saveProgress`'s answer), not what the player * read: the endpoint clears trivially-early and near-finished checkpoints to 0, and mirroring our * own reading would re-introduce that disagreement on the client. */ export function patchCachedProgress(qc: QueryClient, positions: Map): void { if (positions.size === 0) return; const patchList = (items: Video[]): Video[] => { let changed = false; const next = items.map((v) => { const pos = positions.get(v.id); if (pos === undefined || v.position_seconds === pos) return v; changed = true; return { ...v, position_seconds: pos }; }); // Keep the previous array identity when nothing moved, so an untouched list does not // re-render every card behind the open player. return changed ? next : items; }; const patch = (old: Cached | undefined): Cached | undefined => { if (!old) return old; if (Array.isArray(old.pages)) { let changed = false; const pages = old.pages.map((page) => { if (!isItemList(page)) return page; const items = patchList(page.items); if (items === page.items) return page; changed = true; return { ...page, items }; }); return changed ? { ...old, pages } : old; } if (isItemList(old)) { const items = patchList(old.items); return items === old.items ? old : { ...old, items }; } return old; }; // Prefix keys: every filter combination of the feed, and every playlist detail. qc.setQueriesData({ queryKey: qk.feed() }, patch); qc.setQueriesData({ queryKey: qk.playlist() }, patch); }