fix(flutter): stop the cache feedback loop — playback no longer competes

The prefetcher + alwaysRefresh combination was creating a feedback
loop visible in the logs as repeated `metadataPrefetcher: warming N
albums` cycles, each kicking N parallel getAlbum fetches that then
triggered drift writes that triggered re-emits that re-ran the
prefetcher. Tap-to-play was queueing behind 14+ in-flight cache
fetches.

Three structural fixes:

1. Prefetcher hard-dedupes per session via _warmedArtists Set.
   Re-rendering a screen no longer re-fires fetches for ids we've
   already seen.

2. Prefetcher only warms artistProvider, not albumProvider. Albums
   carry track lists; pre-warming N albums fans out N parallel
   "fetch tracks" round trips for content the user may never visit.
   Artist rows are single-row lookups — cheap. Album detail loads
   on tap (still fast: server-side perf work makes it ~one round
   trip).

3. Drop alwaysRefresh from albumProvider, artistProvider,
   artistAlbumsProvider, artistTracksProvider. Each was kicking one
   silent background refresh per first cache hit. With the prefetcher
   creating many subscriptions in parallel, that meant every
   prewarmed id triggered an extra fetch even when drift was already
   populated. playlistsListProvider keeps alwaysRefresh — system
   playlists genuinely rotate UUIDs and need the catch-up. Pull-to-
   refresh remains the explicit invalidation path everywhere else.

Removed the warmAlbums calls from the library Albums tab and artist
detail album grid (the storm sources).

Net effect: cold app boot warms ~12-15 artist rows once, period.
Tapping a tile still fetches its detail on demand (one round trip,
fast). User-initiated playback isn't queued behind cache work.
This commit is contained in:
2026-05-11 19:02:16 -04:00
parent 4bd069430b
commit 4ede37d9ad
5 changed files with 35 additions and 71 deletions
@@ -289,7 +289,6 @@ final playlistDetailProvider =
// Once-per-subscription guard so an empty server response doesn't
// cause repeated re-fetches.
var fetchAttempted = false;
var revalidated = false;
await for (final playlistRows in playlistQuery.watch()) {
if (playlistRows.isEmpty) {
@@ -348,13 +347,12 @@ final playlistDetailProvider =
yield PlaylistDetail(playlist: playlist, tracks: tracks);
// SWR: first complete cache hit kicks one background refresh.
if (!revalidated && trackRows.isNotEmpty) {
revalidated = true;
if (await isOnline()) {
unawaited(fetchAndPopulate());
}
}
// No SWR refresh here. The aggregate playlistsListProvider does
// alwaysRefresh (system playlists rotate UUIDs), but per-detail
// refresh on every visit was multiplying with the prefetcher's
// parallel fetches and starving user-initiated playback. Pull-
// to-refresh on the detail page invalidates the provider, which
// is the right path for an explicit refresh.
}
});