fix(flutter): cacheFirst no longer hangs on no-match cold fetches
Liked tab loaded into an infinite spinner when the user had likes in one category but not all three. Root cause: the three liked-tab providers share one _populateLikeIds function. When the populate writes track rows, drift watch fires for cached_likes (the table all three providers watch). The track provider's stream re-emits with rows.isNotEmpty → yields populated. The album and artist streams re-emit with rows.isEmpty (user has no album/artist likes), re-enter cacheFirst's rows-empty branch, fire populate AGAIN, drift fires again, repeat — never yielding, .isLoading stays true forever, UI spins. Generalises beyond the liked case: any cacheFirst with a populate that writes to a watched table but produces no rows matching this filter would loop. Fix tracks coldFetchAttempted per subscription so the first fetch is the only fetch via the rows-empty branch; subsequent empty emissions yield empty. Also yields current rows after a successful populate so a true no-op fetchAndPopulate (server genuinely empty, fresh-install with no library data) doesn't hang when drift doesn't re-emit for an empty batch. For populated cases, the order is: spinner → brief empty yield from the post-populate yield → drift watch re-emits with rows → populated. UI flashes empty for one frame. Acceptable trade-off for the no-spin guarantee. Also matches the timeout pattern: liked providers' isOnline gains the same 3-second timeout the home/library-list providers already had, so a stuck connectivity check can't extend the hang.
This commit is contained in:
+29
-2
@@ -48,9 +48,22 @@ Stream<T> cacheFirst<D, T>({
|
|||||||
// drift re-emission (otherwise the populate cycles forever).
|
// drift re-emission (otherwise the populate cycles forever).
|
||||||
var revalidated = false;
|
var revalidated = false;
|
||||||
|
|
||||||
|
// Tracks whether we've already tried a cold-cache fetch on this
|
||||||
|
// subscription. Without this, providers can spin forever in the
|
||||||
|
// rows-empty branch when fetchAndPopulate writes rows that don't
|
||||||
|
// match THIS filter — e.g. three liked-tab streams sharing one
|
||||||
|
// populate: the track populate writes track-typed rows, drift
|
||||||
|
// watch fires for the cached_likes table, the album stream
|
||||||
|
// re-emits with rows-empty (no album likes), the rows-empty
|
||||||
|
// branch re-fires populate, repeats forever. Setting this guard
|
||||||
|
// makes the first fetch attempt also the last for any given
|
||||||
|
// subscription.
|
||||||
|
var coldFetchAttempted = false;
|
||||||
|
|
||||||
await for (final rows in driftStream) {
|
await for (final rows in driftStream) {
|
||||||
if (rows.isNotEmpty) {
|
if (rows.isNotEmpty) {
|
||||||
yield toResult(rows);
|
yield toResult(rows);
|
||||||
|
coldFetchAttempted = true;
|
||||||
// Stale-while-revalidate: yield cache immediately, then kick off
|
// Stale-while-revalidate: yield cache immediately, then kick off
|
||||||
// a REST refresh in the background. Drift watch() picks up the
|
// a REST refresh in the background. Drift watch() picks up the
|
||||||
// resulting writes and re-emits via this same stream loop.
|
// resulting writes and re-emits via this same stream loop.
|
||||||
@@ -62,11 +75,25 @@ Stream<T> cacheFirst<D, T>({
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// rows is empty. If we've already attempted a cold fetch and
|
||||||
|
// drift is still empty for this filter, yield empty so the UI
|
||||||
|
// shows the no-content state instead of spinning forever.
|
||||||
|
if (coldFetchAttempted) {
|
||||||
|
yield toResult(rows);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
coldFetchAttempted = true;
|
||||||
if (await isOnline()) {
|
if (await isOnline()) {
|
||||||
try {
|
try {
|
||||||
await fetchAndPopulate();
|
await fetchAndPopulate();
|
||||||
// The drift watch() stream re-emits with the populated rows on
|
// Yield the current (still-empty) rows so the UI moves past
|
||||||
// the next loop iteration. Don't yield here.
|
// loading even if populate was a no-op for this filter
|
||||||
|
// (server returned nothing matching, or all rows were
|
||||||
|
// already in drift via sync). If populate DID write rows
|
||||||
|
// matching this filter, the drift watch's next emission
|
||||||
|
// yields them via the rows.isNotEmpty branch — UI briefly
|
||||||
|
// shows empty then populates, instead of spinning.
|
||||||
|
yield toResult(rows);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
debugPrint('cacheFirst[$tag]: fetchAndPopulate failed: $e\n$st');
|
debugPrint('cacheFirst[$tag]: fetchAndPopulate failed: $e\n$st');
|
||||||
|
|||||||
@@ -242,7 +242,9 @@ final _likedTrackIdsProvider = StreamProvider<List<String>>((ref) {
|
|||||||
driftStream: query,
|
driftStream: query,
|
||||||
fetchAndPopulate: () => _populateLikeIds(ref),
|
fetchAndPopulate: () => _populateLikeIds(ref),
|
||||||
toResult: _idsForEntity,
|
toResult: _idsForEntity,
|
||||||
isOnline: () async => (await ref.read(connectivityProvider.future)),
|
isOnline: () async => (await ref
|
||||||
|
.read(connectivityProvider.future)
|
||||||
|
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||||
alwaysRefresh: true,
|
alwaysRefresh: true,
|
||||||
tag: 'likedTrackIds',
|
tag: 'likedTrackIds',
|
||||||
);
|
);
|
||||||
@@ -258,7 +260,9 @@ final _likedAlbumIdsProvider = StreamProvider<List<String>>((ref) {
|
|||||||
driftStream: query,
|
driftStream: query,
|
||||||
fetchAndPopulate: () => _populateLikeIds(ref),
|
fetchAndPopulate: () => _populateLikeIds(ref),
|
||||||
toResult: _idsForEntity,
|
toResult: _idsForEntity,
|
||||||
isOnline: () async => (await ref.read(connectivityProvider.future)),
|
isOnline: () async => (await ref
|
||||||
|
.read(connectivityProvider.future)
|
||||||
|
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||||
alwaysRefresh: true,
|
alwaysRefresh: true,
|
||||||
tag: 'likedAlbumIds',
|
tag: 'likedAlbumIds',
|
||||||
);
|
);
|
||||||
@@ -274,7 +278,9 @@ final _likedArtistIdsProvider = StreamProvider<List<String>>((ref) {
|
|||||||
driftStream: query,
|
driftStream: query,
|
||||||
fetchAndPopulate: () => _populateLikeIds(ref),
|
fetchAndPopulate: () => _populateLikeIds(ref),
|
||||||
toResult: _idsForEntity,
|
toResult: _idsForEntity,
|
||||||
isOnline: () async => (await ref.read(connectivityProvider.future)),
|
isOnline: () async => (await ref
|
||||||
|
.read(connectivityProvider.future)
|
||||||
|
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||||
alwaysRefresh: true,
|
alwaysRefresh: true,
|
||||||
tag: 'likedArtistIds',
|
tag: 'likedArtistIds',
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user