// 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'; /// 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`) /// /// `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 cacheFirst({ required Stream> driftStream, required Future Function() fetchAndPopulate, required T Function(List) toResult, required Future Function() isOnline, bool alwaysRefresh = false, }) 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 (_) { yield toResult(rows); // empty result; caller surfaces error } } else { yield toResult(rows); // empty result; offline } } } Future _safeFetch(Future Function() fn) async { try { await fn(); } catch (_) { // Background revalidate — swallow; UI already showed cached state. } }