// 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) // // 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, }) async* { await for (final rows in driftStream) { if (rows.isNotEmpty) { yield toResult(rows); 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 } } }