Skip to content

fix(web): scope and throttle realtime media-surface invalidations - #797

Open
Quick104 wants to merge 1 commit into
mainfrom
t3code/fix-playback-metadata-requests
Open

fix(web): scope and throttle realtime media-surface invalidations#797
Quick104 wants to merge 1 commit into
mainfrom
t3code/fix-playback-metadata-requests

Conversation

@Quick104

Copy link
Copy Markdown
Contributor

Problem

During web playback, the network log fills with repeated identical batches of metadata requests (watch detail, seasons, two episodes fetches) and the video buffers. Root cause: RealtimeEventsProvider responded to every realtime catalog and user_state websocket event by calling invalidateMediaSurfaceQueries, which invalidates itemKeys.all, catalogKeys.all, sections, progress, history, favorites, watchlist, recs, and persons — unscoped. React Query immediately refetches invalidated active queries, so any background catalog churn (metadata worker publishing catalog.item.changed per enriched item, scans, another device's progress sync) turned into per-event refetch bursts on the watch page that compete with HLS segment downloads. The queries' 5-minute staleTime was defeated entirely.

Approach

Web frontend only; no API or server change.

  • Per-item catalog events (catalog.item.changed, metadata.updated, library.item_added) invalidate only the changed item's queries immediately (new invalidateItemScopedQueries, extracted from the existing per-item block so key literals aren't duplicated).
  • Broad surface refreshes (lists, sections, home) ride a per-provider throttle: at most one leading + one trailing invalidateCatalogState pass per 15s window, with coalesced options (differing library ids collapse to all-libraries; boolean flags OR together). The trailing timer is dropped while the tab is hidden (the catch-up-on-focus refetch covers it) and cleared on unmount.
  • user_state progress events — continuous while any device plays — now touch only progress/history/section surfaces, the item, and its series (invalidateSeriesScopedQueries, so an open series page's episode ticks still refresh), plus a trailing-only throttled broad pass so browse grids and watched-filtered collections catch up within 15s instead of going permanently stale. Threshold-based completion emits only change:"progress", so the trailing pass is what keeps those surfaces converging. Other user_state kinds (favorite/watchlist/watched) keep immediate behavior.
  • Job-terminal passes (catalog_import/delete_library completion) still run immediately but reset the throttle and swallow any pending trailing pass, avoiding a redundant back-to-back full sweep.
  • Direct user mutations (favorites, watchlist, history, dismissals, taste seed) are untouched and keep immediate unthrottled invalidation.

Net effect during playback with catalog churn: one 4-request metadata batch per 15s worst case, instead of one per event.

Evidence

Focused tests (3 files, 19 tests, includes new coverage for per-item scoping, throttle leading/trailing/recovery, hidden-tab suppression, job-terminal reset, and progress-event narrowing):

Test Files  3 passed (3)
     Tests  19 passed (19)

npx tsc -b clean; npx prettier --check clean on all touched files; ESLint on touched files: 0 errors, 7 pre-existing warnings untouched by this change. No entries added to WEBTEST_KNOWN_FAILURES.

Not run: full make test-go (no Go changes) and manual playback-under-scan reproduction on a live deployment — the fix is validated by unit tests asserting invalidation behavior.

Risks / follow-ups

  • Server publish sites currently pass an empty series_id in publishUserStateEvent (e.g. internal/api/handlers/progress.go), so the series-scoped immediate invalidation is inert until the server populates it; the trailing throttled pass covers the gap today. Populating series_id is a small server-side follow-up.
  • Progress events for the same series from another device invalidate series-scoped keys per event (unthrottled) — bounded to one series' queries, kept immediate so open series pages update promptly.
  • Surfaces refreshed by broad passes now update up to 15s later during event bursts; per-item and progress surfaces still update immediately.
  • Worth checking whether silo-apple/silo-android have an equivalent invalidate-everything pattern in their realtime handling; this PR is web-only.

Related issue: #796

AI Disclosure

  • Tool(s): Claude Code
  • Model(s): claude-fable-5[1m]
  • Involvement: Fully AI-generated, human verified
  • Adversarial review: three independent review passes over the full diff (freshness-regression lens tracing every surface that previously updated instantly; timing/lifecycle lens on the throttle state machine; test-adequacy lens). 5 findings, all resolved: permanent staleness of series/browse watch-state on progress-driven completion (fixed via series-scoped invalidation + trailing-only catch-up), trailing timer escaping the page-activity gate (fixed), ignored series_id (fixed), job-terminal pass doubling with a pending trailing pass (fixed), and a missing throttle-recovery test (added).

🤖 Generated with Claude Code

Every realtime catalog or user_state event invalidated all media-surface
query caches, so background catalog churn (metadata worker, scans) or
another device's progress sync forced the open player's watch-detail,
seasons, and episodes queries to refetch on every event — competing with
HLS segment downloads and causing buffering.

Per-item catalog events now invalidate only the changed item's queries;
broad surface refreshes coalesce behind a 15s leading+trailing throttle;
user_state progress events touch progress/history/section surfaces, the
item, and its series immediately, with the remaining watch-state
surfaces catching up through a trailing-only throttled pass.

Fixes #796

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 7 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 92 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2776224a-be20-49f9-9933-571822f5b5c3

📥 Commits

Reviewing files that changed from the base of the PR and between 8164fd5 and 4dbe8d7.

📒 Files selected for processing (4)
  • web/src/components/RealtimeEventsProvider.test.tsx
  • web/src/components/RealtimeEventsProvider.throttle.test.tsx
  • web/src/components/RealtimeEventsProvider.tsx
  • web/src/hooks/queries/mediaSurfaceRefresh.ts

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant