The player checkpointed every 5s and the server stored it, but nothing wrote the new position back into react-query: the close path only marked qk.feed() stale with refetchType "none", and nothing refetches the feed until it remounts (staleTime 30s, refetchOnWindowFocus off). So the resume read the position the cache had been FETCHED with, and the next checkpoint then persisted that older value over the newer one. Measured on localdev: played to 830s, closed (server had 830), reopened the same card without a reload -> the row was back to 751s. 79 seconds of progress destroyed by a reopen, and the card's progress bar never moved either. The Plex player already solved this for its detail query, with a comment describing the identical failure; this is the feed-side twin that was missed in that sweep. patchCachedProgress writes the position the SERVER stored (the endpoint clears trivially-early and near-finished checkpoints to 0, so mirroring our own reading would disagree with the row) into every cached feed page and playlist detail, keeping array identity where nothing changed. A session map covers the second half: the launch queue is frozen at mount, so stepping next->prev back to an item played earlier in the same session resumed from that snapshot. Verified in the browser: 811 -> close 816 -> reopen 825, and the card now shows Continue/Restart.
68 lines
2.9 KiB
TypeScript
68 lines
2.9 KiB
TypeScript
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<ItemList> & { 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<string, number>): 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<Cached>({ queryKey: qk.feed() }, patch);
|
|
qc.setQueriesData<Cached>({ queryKey: qk.playlist() }, patch);
|
|
}
|