e856172d60
Cleanup pass on the noisy debugPrints we added during the recent debugging sessions. Remaining prints are error-path only: - AlbumCoverCache fetch failed - album/playlist cold-cache fetch failed - audio_handler playbackEventStream error - audio_handler queue supersession (rare race) - audio_handler forward/backward fill failed - artist_detail play failed - cacheFirst fetchAndPopulate failed (only when tag is set) Removed per-event chatter: - audio_handler player-state and processingState subscribers - audio_handler timing measurements (built initial / setAudioSources / forward fill / backward fill) - audio_handler cache hit/miss per-track (fired N times per play) - audio_handler register stream cache (verifiable via Settings) - audio_handler.configure log - albumProvider step-by-step lines (drift miss / online / drift hit / album hit but no tracks) - playlistDetailProvider step-by-step lines (calling get / drift write done / 404 evicted / drift hit / playlist hit but no tracks) - playlistsListProvider wire returned + drift rows lines - playTracks stage timings (serverUrl / token / configure / setQueue / play returned) - metadataPrefetcher warming N artists - artist_detail play tapped — N tracks success log - cacheFirst step-by-step (drift hit / drift miss / online / done) `tag` parameter on cacheFirst preserved for ad-hoc instrumentation.
89 lines
3.4 KiB
Dart
89 lines
3.4 KiB
Dart
// Drift-first reactive read pattern with REST cold-cache fallback (#357 plan C).
|
|
//
|
|
// Subscribes to a drift watch() stream. On each emission:
|
|
// - non-empty → map to result type T and yield
|
|
// - empty + online → fetch via REST, populate drift, await re-emission
|
|
// - empty + offline → yield mapped empty result (UI shows empty state)
|
|
//
|
|
// With `alwaysRefresh: true`, also kicks off a one-shot REST refresh
|
|
// in the background after the first non-empty emission. Use for
|
|
// aggregate lists (playlists, etc.) where the server may have rows
|
|
// the local sync didn't pick up — yields cache immediately, refreshes
|
|
// silently, and drift watch() re-emits with whatever new rows landed.
|
|
//
|
|
// The pattern lets every read provider trust drift as the source of
|
|
// truth. SyncController keeps drift fresh in the background; widget
|
|
// rebuilds happen automatically as drift writes propagate via watch().
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart' show debugPrint;
|
|
|
|
// `tag` parameter is preserved for future ad-hoc instrumentation.
|
|
// Normal operation only logs failure paths so the per-screen log
|
|
// noise stays low.
|
|
|
|
/// Wraps the watch + cold-cache fallback pattern. Generic over:
|
|
/// D — the drift row type (or TypedResult for joins)
|
|
/// T — the result type the caller wants (e.g. `List<ArtistRef>`)
|
|
///
|
|
/// `fetchAndPopulate` is invoked when drift is empty AND `isOnline()`
|
|
/// returns true. It must populate drift via its own side-effect; the
|
|
/// drift watch() stream will re-emit and this helper yields the
|
|
/// populated rows on the next iteration.
|
|
///
|
|
/// REST failures are swallowed — the helper falls through to yielding
|
|
/// the empty result. Caller is responsible for surfacing errors via
|
|
/// toast etc.
|
|
Stream<T> cacheFirst<D, T>({
|
|
required Stream<List<D>> driftStream,
|
|
required Future<void> Function() fetchAndPopulate,
|
|
required T Function(List<D>) toResult,
|
|
required Future<bool> Function() isOnline,
|
|
bool alwaysRefresh = false,
|
|
String? tag,
|
|
}) async* {
|
|
// Tracks whether we've already kicked off a stale-while-revalidate
|
|
// refresh for this stream subscription, so we don't fire one on every
|
|
// drift re-emission (otherwise the populate cycles forever).
|
|
var revalidated = false;
|
|
|
|
await for (final rows in driftStream) {
|
|
if (rows.isNotEmpty) {
|
|
yield toResult(rows);
|
|
// Stale-while-revalidate: yield cache immediately, then kick off
|
|
// a REST refresh in the background. Drift watch() picks up the
|
|
// resulting writes and re-emits via this same stream loop.
|
|
// Useful for aggregate lists (e.g. playlists) where the server
|
|
// may have rows the local sync didn't pick up.
|
|
if (alwaysRefresh && !revalidated && await isOnline()) {
|
|
revalidated = true;
|
|
unawaited(_safeFetch(fetchAndPopulate));
|
|
}
|
|
continue;
|
|
}
|
|
if (await isOnline()) {
|
|
try {
|
|
await fetchAndPopulate();
|
|
// The drift watch() stream re-emits with the populated rows on
|
|
// the next loop iteration. Don't yield here.
|
|
} catch (e, st) {
|
|
if (tag != null) {
|
|
debugPrint('cacheFirst[$tag]: fetchAndPopulate failed: $e\n$st');
|
|
}
|
|
yield toResult(rows); // empty result; caller surfaces error
|
|
}
|
|
} else {
|
|
yield toResult(rows); // empty result; offline
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _safeFetch(Future<void> Function() fn) async {
|
|
try {
|
|
await fn();
|
|
} catch (_) {
|
|
// Background revalidate — swallow; UI already showed cached state.
|
|
}
|
|
}
|