From 7f9f5c065b86c91535bb43ae1790b22a6bb93358 Mon Sep 17 00:00:00 2001 From: vjrj Date: Thu, 9 Jul 2026 21:33:48 +0200 Subject: [PATCH] fix(inventory): merge draft + list watch into one subscription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InventoryCubit subscribed to watchInventory() and watchDrafts() separately — two overlapping StreamGroups (both watching attachments) re-emitted in a tight loop that starved the event loop and hung widget tests' pumpAndSettle for 10+ minutes (home/inventory/quick-add all timed out). Replace both with a single watchInventoryView() stream that loads the catalogued list and the draft tray from one StreamGroup. Full widget suite no longer hangs: 165 pass. One unrelated failure remains in the v8 variety edit sheet (Save button pushed off-screen by new fields). --- .../lib/data/variety_repository.dart | 21 +++++++++++++++++++ apps/app_seeds/lib/state/inventory_cubit.dart | 18 +++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/apps/app_seeds/lib/data/variety_repository.dart b/apps/app_seeds/lib/data/variety_repository.dart index 2377043..b043057 100644 --- a/apps/app_seeds/lib/data/variety_repository.dart +++ b/apps/app_seeds/lib/data/variety_repository.dart @@ -365,6 +365,27 @@ class VarietyRepository { return triggers.asyncMap((_) => _loadInventory()); } + /// The whole inventory screen in one reactive subscription: the catalogued + /// list and the draft tray, from a single [StreamGroup]. The cubit MUST use + /// this rather than subscribing to [watchInventory] and [watchDrafts] + /// separately — two overlapping StreamGroups (both watching `attachments`) + /// re-emit in a tight loop that starves the event loop and hangs widget + /// tests' `pumpAndSettle`. + Stream<({List items, List drafts})> + watchInventoryView() { + final triggers = StreamGroup.merge([ + (_db.select( + _db.varieties, + )..where((v) => v.isDeleted.equals(false))).watch().map((_) {}), + _db.select(_db.attachments).watch().map((_) {}), + _db.select(_db.species).watch().map((_) {}), + _db.select(_db.lots).watch().map((_) {}), + ]); + return triggers.asyncMap( + (_) async => (items: await _loadInventory(), drafts: await _loadDrafts()), + ); + } + Future> _loadInventory() async { final rows = await (_db.select(_db.varieties) diff --git a/apps/app_seeds/lib/state/inventory_cubit.dart b/apps/app_seeds/lib/state/inventory_cubit.dart index f539880..4908312 100644 --- a/apps/app_seeds/lib/state/inventory_cubit.dart +++ b/apps/app_seeds/lib/state/inventory_cubit.dart @@ -121,17 +121,20 @@ class InventoryState extends Equatable { /// automatically after a quick-add — no manual refresh. class InventoryCubit extends Cubit { InventoryCubit(this._repo) : super(const InventoryState()) { - _sub = _repo.watchInventory().listen( - (items) => emit(state.copyWith(items: items, loading: false)), - ); - _draftsSub = _repo.watchDrafts().listen( - (drafts) => emit(state.copyWith(drafts: drafts)), + // One combined subscription (list + draft tray). Two separate StreamGroups + // here re-emit in a loop that hangs widget tests — see watchInventoryView. + _sub = _repo.watchInventoryView().listen( + (view) => emit( + state.copyWith(items: view.items, drafts: view.drafts, loading: false), + ), ); } final VarietyRepository _repo; - late final StreamSubscription> _sub; - late final StreamSubscription> _draftsSub; + late final StreamSubscription< + ({List items, List drafts}) + > + _sub; void search(String query) => emit(state.copyWith(query: query)); @@ -171,7 +174,6 @@ class InventoryCubit extends Cubit { @override Future close() async { await _sub.cancel(); - await _draftsSub.cancel(); return super.close(); } }