fix(player): persist watched at end-of-playback (atomic upsert)
The in-app player marks a video watched when it reaches the end, firing
POST /videos/{id}/state at the same instant as a progress checkpoint. The
progress endpoint deletes the 'new' video_states row near the end, so the
concurrent state UPDATE matched 0 rows and raised StaleDataError -> 500. The
watched status was never persisted (and Feed swallowed the error), so the
video stayed in the unwatched feed even after a refresh.
- set_video_state: atomic INSERT ... ON CONFLICT (uq_user_video) DO UPDATE for
watched/hidden, immune to a concurrent delete; tolerate StaleDataError on the
'new' branch and in the progress endpoint.
- PlayerModal: skip the redundant near-end progress checkpoint when we just
auto-marked watched, removing the self-inflicted race.
- Feed: drop the optimistic override if the server rejects the change, so a
failed request can't leave a card phantom-hidden.
This commit is contained in:
parent
64cf027286
commit
940b616804
3 changed files with 57 additions and 18 deletions
|
|
@ -339,12 +339,14 @@ export default function PlayerModal({
|
|||
|
||||
// Auto-watch only applies to the active item — not to other videos the player
|
||||
// navigated to via description links.
|
||||
const maybeAutoWatch = (current: number, duration: number) => {
|
||||
if (autoMarkedRef.current || duration <= 0 || currentIdRef.current !== id) return;
|
||||
const maybeAutoWatch = (current: number, duration: number): boolean => {
|
||||
if (autoMarkedRef.current || duration <= 0 || currentIdRef.current !== id) return false;
|
||||
if (current > duration - FINISH_MARGIN) {
|
||||
autoMarkedRef.current = true;
|
||||
setWatched(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const persist = (): Promise<unknown> | void => {
|
||||
const p = playerRef.current;
|
||||
|
|
@ -352,7 +354,10 @@ export default function PlayerModal({
|
|||
try {
|
||||
const cur = p.getCurrentTime();
|
||||
const dur = typeof p.getDuration === "function" ? p.getDuration() : 0;
|
||||
maybeAutoWatch(cur, dur);
|
||||
// If we just auto-marked watched, skip the near-end progress checkpoint: it would
|
||||
// only clear the resume position, and firing both at once needlessly races the
|
||||
// watched write against the progress row it deletes.
|
||||
if (maybeAutoWatch(cur, dur)) return;
|
||||
// Only checkpoint the feed video we opened with — a navigated-to video may not
|
||||
// be in this user's library, so the server would 404 on it.
|
||||
if (currentIdRef.current === id) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue