fix(sync): mirror the server's watch position so resume crosses devices (DR-155)
The resume check reads the local user_data row and nothing else, but mirror_user_data -- the only path by which server UserData lands in that table -- mirrored is_favorite alone, and returned early whenever that field was absent, which is exactly the shape of an ordinary watched episode. playback_position_ticks was therefore write-only from this device's perspective: watch 40 minutes in a browser, open JellyTau, and it resumed from whatever this device last saw, or offered no resume at all. Same user-visible symptom as the Android bug fixed earlier on this branch, from an unrelated cause -- which is why resume read as broadly flaky rather than as one defect. The mirror now carries the position alongside the favourite flag under the same pending_sync = 0 conflict rule, so a local position still waiting to be pushed is never pulled backwards by a server that has not yet heard where we got to. COALESCE(excluded.x, user_data.x) keeps the stored value for a field the server omitted rather than nulling it, and a row with neither field is still skipped rather than fabricated as zeroes. Mirroring alone was not sufficient. get_item -- the call the player route makes -- returned the cached copy on a hit and never consulted the server, so for an already-cached item the mirror never ran. It now refreshes in the background on a cache hit via race_with_refresh, the reusable form of what get_items already did inline. That asymmetry is why browsing a season picked up other devices' state while opening the episode directly did not. The refreshed value lands for the next read; the cache-first race still answers immediately. The DR/total counts in extract-traces.test.ts are updated for DR-154 and DR-155 -- that edit is the test's intended signal that the CI gate's denominator is live rather than frozen. Verified red->green in the jellytau-builder image: both new tests failed before the fix. Full Rust suite passes (634), cargo fmt clean, clippy adds no new warnings; frontend suite (933) and svelte-check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1472,6 +1472,15 @@ async repositoryReportPlaybackProgress(handle: string, itemId: string, positionM
|
||||
},
|
||||
/**
|
||||
* Report playback stopped
|
||||
*
|
||||
* A stop-report that cannot reach the server is queued rather than dropped:
|
||||
* this is the position the resume point is built from, and losing it is
|
||||
* exactly the "it forgot where I was" the sync queue exists to prevent. The
|
||||
* drain (DR-131) pushes it on the next reconnect. Queueing is best-effort —
|
||||
* failing the command because the *queue* write failed would tell the caller
|
||||
* the report was lost when the local position was already saved.
|
||||
*
|
||||
* TRACES: UR-025 | DR-154 | UT-151
|
||||
*/
|
||||
async repositoryReportPlaybackStopped(handle: string, itemId: string, positionMs: number) : Promise<null> {
|
||||
return await TAURI_INVOKE("repository_report_playback_stopped", { handle, itemId, positionMs });
|
||||
|
||||
Reference in New Issue
Block a user