From c6f017b0d15a00b9d67dde791828a7995aebb281 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 27 Feb 2024 12:45:58 -0500 Subject: [PATCH] busy handling --- lib/chat/cubits/messages_cubit.dart | 18 +++--- lib/chat/views/chat_component.dart | 3 +- .../active_conversations_bloc_map_cubit.dart | 11 ++-- lib/chat_list/cubits/chat_list_cubit.dart | 29 ++++----- .../chat_single_contact_item_widget.dart | 36 +++++++---- .../chat_single_contact_list_widget.dart | 3 +- .../cubits/contact_invitation_list_cubit.dart | 59 ++++++++++--------- .../waiting_invitations_bloc_map_cubit.dart | 10 ++-- .../views/contact_invitation_item_widget.dart | 11 +++- .../views/contact_invitation_list_widget.dart | 9 ++- lib/contacts/cubits/contact_list_cubit.dart | 58 +++++++++--------- lib/contacts/views/contact_item_widget.dart | 53 ++++++++++------- lib/contacts/views/contact_list_widget.dart | 11 +++- .../home_account_ready_shell.dart | 10 ++-- .../main_pager/account_page.dart | 16 +++-- lib/tools/widget_helpers.dart | 12 ++++ packages/async_tools/lib/async_tools.dart | 1 + .../lib/src/single_state_processor.dart | 3 +- .../lib/src/single_stateless_processor.dart | 47 +++++++++++++++ .../lib/src/async_transformer_cubit.dart | 2 +- .../bloc_tools/lib/src/bloc_busy_wrapper.dart | 25 +++++++- .../bloc_tools/lib/src/state_follower.dart | 2 +- .../src/dht_short_array_cubit.dart | 57 +++++++----------- 23 files changed, 307 insertions(+), 179 deletions(-) create mode 100644 packages/async_tools/lib/src/single_stateless_processor.dart diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart index b1846cf..febc23e 100644 --- a/lib/chat/cubits/messages_cubit.dart +++ b/lib/chat/cubits/messages_cubit.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -61,9 +62,10 @@ class MessagesCubit extends Cubit>> { await super.close(); } - void updateLocalMessagesState(AsyncValue> avmessages) { + void updateLocalMessagesState( + BlocBusyState>> avmessages) { // Updated local messages from online just update the state immediately - emit(avmessages); + emit(avmessages.state); } Future _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async { @@ -97,16 +99,17 @@ class MessagesCubit extends Cubit>> { // Insert at this position if (!skip) { // Insert into dht backing array - await _localMessagesCubit!.shortArray - .tryInsertItem(pos, newMessage.writeToBuffer()); + await _localMessagesCubit!.operate((shortArray) => + shortArray.tryInsertItem(pos, newMessage.writeToBuffer())); // Insert into local copy as well for this operation localMessages = localMessages.insert(pos, newMessage); } } } - void updateRemoteMessagesState(AsyncValue> avmessages) { - final remoteMessages = avmessages.data?.value; + void updateRemoteMessagesState( + BlocBusyState>> avmessages) { + final remoteMessages = avmessages.state.data?.value; if (remoteMessages == null) { return; } @@ -171,7 +174,8 @@ class MessagesCubit extends Cubit>> { } Future addMessage({required proto.Message message}) async { - await _localMessagesCubit!.shortArray.tryAddItem(message.writeToBuffer()); + await _localMessagesCubit!.operate( + (shortArray) => shortArray.tryAddItem(message.writeToBuffer())); } Future getMessagesCrypto() async { diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 6e16276..ab3d16f 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -48,7 +48,8 @@ class ChatComponent extends StatelessWidget { if (accountRecordInfo == null) { return debugPage('should always have an account record here'); } - final contactList = context.watch().state.data?.value; + final contactList = + context.watch().state.state.data?.value; if (contactList == null) { return debugPage('should always have a contact list here'); } diff --git a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart index 0c32523..7f67f10 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -36,7 +36,9 @@ typedef ActiveConversationsBlocMapState // Automatically follows the state of a ChatListCubit. class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> - with StateFollower>, TypedKey, proto.Chat> { + with + StateFollower>>, TypedKey, + proto.Chat> { ActiveConversationsBlocMapCubit( {required ActiveAccountInfo activeAccountInfo, required ContactListCubit contactListCubit}) @@ -73,8 +75,9 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit getStateMap(AsyncValue> state) { - final stateValue = state.data?.value; + IMap getStateMap( + BlocBusyState>> state) { + final stateValue = state.state.data?.value; if (stateValue == null) { return IMap(); } @@ -88,7 +91,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, proto.Chat value) async { - final contactList = _contactListCubit.state.data?.value; + final contactList = _contactListCubit.state.state.data?.value; if (contactList == null) { await addState(key, const AsyncValue.loading()); return; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 606c5b8..11eebad 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; -import 'package:bloc_tools/bloc_tools.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -44,7 +42,9 @@ class ChatListCubit extends DHTShortArrayCubit { // Add Chat to account's list // if this fails, don't keep retrying, user can try again later - if (await shortArray.tryAddItem(chat.writeToBuffer()) == false) { + final added = await operate( + (shortArray) => shortArray.tryAddItem(chat.writeToBuffer())); + if (!added) { throw Exception('Failed to add chat'); } } @@ -57,17 +57,18 @@ class ChatListCubit extends DHTShortArrayCubit { // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later - - for (var i = 0; i < shortArray.length; i++) { - final cbuf = await shortArray.getItem(i); - if (cbuf == null) { - throw Exception('Failed to get chat'); + await operate((shortArray) async { + for (var i = 0; i < shortArray.length; i++) { + final cbuf = await shortArray.getItem(i); + if (cbuf == null) { + throw Exception('Failed to get chat'); + } + final c = proto.Chat.fromBuffer(cbuf); + if (c.remoteConversationKey == remoteConversationKey) { + await shortArray.tryRemoveItem(i); + return; + } } - final c = proto.Chat.fromBuffer(cbuf); - if (c.remoteConversationKey == remoteConversationKey) { - await shortArray.tryRemoveItem(i); - return; - } - } + }); } } diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 7d64e43..7daa99c 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -10,10 +10,15 @@ import '../../theme/theme.dart'; import '../chat_list.dart'; class ChatSingleContactItemWidget extends StatelessWidget { - const ChatSingleContactItemWidget({required proto.Contact contact, super.key}) - : _contact = contact; + const ChatSingleContactItemWidget({ + required proto.Contact contact, + required bool disabled, + super.key, + }) : _contact = contact, + _disabled = disabled; final proto.Contact _contact; + final bool _disabled; @override // ignore: prefer_expression_function_bodies @@ -43,12 +48,14 @@ class ChatSingleContactItemWidget extends StatelessWidget { motion: const DrawerMotion(), children: [ SlidableAction( - onPressed: (context) async { - final chatListCubit = context.read(); - await chatListCubit.deleteChat( - remoteConversationRecordKey: - remoteConversationRecordKey); - }, + onPressed: _disabled + ? null + : (context) async { + final chatListCubit = context.read(); + await chatListCubit.deleteChat( + remoteConversationRecordKey: + remoteConversationRecordKey); + }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, icon: Icons.delete, @@ -67,11 +74,14 @@ class ChatSingleContactItemWidget extends StatelessWidget { // The child of the Slidable is what the user sees when the // component is not dragged. child: ListTile( - onTap: () { - singleFuture(activeChatCubit, () async { - activeChatCubit.setActiveChat(remoteConversationRecordKey); - }); - }, + onTap: _disabled + ? null + : () { + singleFuture(activeChatCubit, () async { + activeChatCubit + .setActiveChat(remoteConversationRecordKey); + }); + }, title: Text(_contact.editedProfile.name), /// xxx show last message here diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart index 4331187..5952dc4 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -45,7 +45,8 @@ class ChatSingleContactListWidget extends StatelessWidget { return const Text('...'); } return ChatSingleContactItemWidget( - contact: contact); + contact: contact, + disabled: contactListV.busy); }, filter: (value) { final lowerValue = value.toLowerCase(); diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 49bc4a6..399fafa 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -138,9 +138,11 @@ class ContactInvitationListCubit // Add ContactInvitationRecord to account's list // if this fails, don't keep retrying, user can try again later - if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) { - throw Exception('Failed to add contact invitation record'); - } + await operate((shortArray) async { + if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) { + throw Exception('Failed to add contact invitation record'); + } + }); }); }); @@ -155,31 +157,34 @@ class ContactInvitationListCubit _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list - for (var i = 0; i < shortArray.length; i++) { - final item = await shortArray.getItemProtobuf( - proto.ContactInvitationRecord.fromBuffer, i); - if (item == null) { - throw Exception('Failed to get contact invitation record'); - } - if (item.contactRequestInbox.recordKey.toVeilid() == - contactRequestInboxRecordKey) { - await shortArray.tryRemoveItem(i); - - await (await pool.openOwned(item.contactRequestInbox.toVeilid(), - parent: accountRecordKey)) - .scope((contactRequestInbox) async { - // Wipe out old invitation so it shows up as invalid - await contactRequestInbox.tryWriteBytes(Uint8List(0)); - await contactRequestInbox.delete(); - }); - if (!accepted) { - await (await pool.openRead(item.localConversationRecordKey.toVeilid(), - parent: accountRecordKey)) - .delete(); + await operate((shortArray) async { + for (var i = 0; i < shortArray.length; i++) { + final item = await shortArray.getItemProtobuf( + proto.ContactInvitationRecord.fromBuffer, i); + if (item == null) { + throw Exception('Failed to get contact invitation record'); + } + if (item.contactRequestInbox.recordKey.toVeilid() == + contactRequestInboxRecordKey) { + await shortArray.tryRemoveItem(i); + + await (await pool.openOwned(item.contactRequestInbox.toVeilid(), + parent: accountRecordKey)) + .scope((contactRequestInbox) async { + // Wipe out old invitation so it shows up as invalid + await contactRequestInbox.tryWriteBytes(Uint8List(0)); + await contactRequestInbox.delete(); + }); + if (!accepted) { + await (await pool.openRead( + item.localConversationRecordKey.toVeilid(), + parent: accountRecordKey)) + .delete(); + } + return; } - return; } - } + }); } Future validateInvitation( @@ -205,7 +210,7 @@ class ContactInvitationListCubit // inbox with our list of extant invitations // If we're chatting to ourselves, // we are validating an invitation we have created - final isSelf = state.data!.value.indexWhere((cir) => + final isSelf = state.state.data!.value.indexWhere((cir) => cir.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxKey) != -1; diff --git a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart index 3762d28..584d2a1 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -16,8 +16,10 @@ typedef WaitingInvitationsBlocMapState class WaitingInvitationsBlocMapCubit extends BlocMapCubit, WaitingInvitationCubit> with - StateFollower>, - TypedKey, proto.ContactInvitationRecord> { + StateFollower< + BlocBusyState>>, + TypedKey, + proto.ContactInvitationRecord> { WaitingInvitationsBlocMapCubit( {required this.activeAccountInfo, required this.account}); @@ -37,8 +39,8 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit getStateMap( - AsyncValue> state) { - final stateValue = state.data?.value; + BlocBusyState>> state) { + final stateValue = state.state.data?.value; if (stateValue == null) { return IMap(); } diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 36ee50a..c6e96c1 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -9,15 +9,20 @@ import '../contact_invitation.dart'; class ContactInvitationItemWidget extends StatelessWidget { const ContactInvitationItemWidget( - {required this.contactInvitationRecord, super.key}); + {required this.contactInvitationRecord, + required this.disabled, + super.key}); final proto.ContactInvitationRecord contactInvitationRecord; + final bool disabled; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'contactInvitationRecord', contactInvitationRecord)); + properties + ..add(DiagnosticsProperty( + 'contactInvitationRecord', contactInvitationRecord)) + ..add(DiagnosticsProperty('disabled', disabled)); } @override diff --git a/lib/contact_invitation/views/contact_invitation_list_widget.dart b/lib/contact_invitation/views/contact_invitation_list_widget.dart index e93f746..19243b7 100644 --- a/lib/contact_invitation/views/contact_invitation_list_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_list_widget.dart @@ -10,10 +10,12 @@ import 'contact_invitation_item_widget.dart'; class ContactInvitationListWidget extends StatefulWidget { const ContactInvitationListWidget({ required this.contactInvitationRecordList, + required this.disabled, super.key, }); final IList contactInvitationRecordList; + final bool disabled; @override ContactInvitationListWidgetState createState() => @@ -21,8 +23,10 @@ class ContactInvitationListWidget extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(IterableProperty( - 'contactInvitationRecordList', contactInvitationRecordList)); + properties + ..add(IterableProperty( + 'contactInvitationRecordList', contactInvitationRecordList)) + ..add(DiagnosticsProperty('disabled', disabled)); } } @@ -63,6 +67,7 @@ class ContactInvitationListWidgetState return ContactInvitationItemWidget( contactInvitationRecord: widget.contactInvitationRecordList[index], + disabled: widget.disabled, key: ObjectKey(widget.contactInvitationRecordList[index])) .paddingLTRB(4, 2, 4, 2); }, diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index c2f5200..af66ac7 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -53,9 +53,11 @@ class ContactListCubit extends DHTShortArrayCubit { // Add Contact to account's list // if this fails, don't keep retrying, user can try again later - if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) { - throw Exception('Failed to add contact record'); - } + await operate((shortArray) async { + if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) { + throw Exception('Failed to add contact record'); + } + }); } Future deleteContact({required proto.Contact contact}) async { @@ -67,34 +69,36 @@ class ContactListCubit extends DHTShortArrayCubit { contact.remoteConversationRecordKey.toVeilid(); // Remove Contact from account's list - for (var i = 0; i < shortArray.length; i++) { - final item = - await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); - if (item == null) { - throw Exception('Failed to get contact'); + await operate((shortArray) async { + for (var i = 0; i < shortArray.length; i++) { + final item = + await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); + if (item == null) { + throw Exception('Failed to get contact'); + } + if (item.remoteConversationRecordKey == + contact.remoteConversationRecordKey) { + await shortArray.tryRemoveItem(i); + break; + } } - if (item.remoteConversationRecordKey == - contact.remoteConversationRecordKey) { - await shortArray.tryRemoveItem(i); - break; - } - } - try { - await (await pool.openRead(localConversationKey, - parent: accountRecordKey)) - .delete(); - } on Exception catch (e) { - log.debug('error removing local conversation record key: $e', e); - } - try { - if (localConversationKey != remoteConversationKey) { - await (await pool.openRead(remoteConversationKey, + try { + await (await pool.openRead(localConversationKey, parent: accountRecordKey)) .delete(); + } on Exception catch (e) { + log.debug('error removing local conversation record key: $e', e); } - } on Exception catch (e) { - log.debug('error removing remote conversation record key: $e', e); - } + try { + if (localConversationKey != remoteConversationKey) { + await (await pool.openRead(remoteConversationKey, + parent: accountRecordKey)) + .delete(); + } + } on Exception catch (e) { + log.debug('error removing remote conversation record key: $e', e); + } + }); } // diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 864b9ab..4fc2bce 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -11,9 +11,11 @@ import '../../theme/theme.dart'; import '../contacts.dart'; class ContactItemWidget extends StatelessWidget { - const ContactItemWidget({required this.contact, super.key}); + const ContactItemWidget( + {required this.contact, required this.disabled, super.key}); final proto.Contact contact; + final bool disabled; @override // ignore: prefer_expression_function_bodies @@ -41,17 +43,22 @@ class ContactItemWidget extends StatelessWidget { motion: const DrawerMotion(), children: [ SlidableAction( - onPressed: (context) async { - final contactListCubit = context.read(); - final chatListCubit = context.read(); + onPressed: disabled || context.read().isBusy + ? null + : (context) async { + final contactListCubit = + context.read(); + final chatListCubit = context.read(); - // Remove any chats for this contact - await chatListCubit.deleteChat( - remoteConversationRecordKey: remoteConversationKey); + // Remove any chats for this contact + await chatListCubit.deleteChat( + remoteConversationRecordKey: + remoteConversationKey); - // Delete the contact itself - await contactListCubit.deleteContact(contact: contact); - }, + // Delete the contact itself + await contactListCubit.deleteContact( + contact: contact); + }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, icon: Icons.delete, @@ -70,17 +77,21 @@ class ContactItemWidget extends StatelessWidget { // The child of the Slidable is what the user sees when the // component is not dragged. child: ListTile( - onTap: () async { - // Start a chat - final chatListCubit = context.read(); - await chatListCubit.getOrCreateChatSingleContact( - remoteConversationRecordKey: remoteConversationKey); - // Click over to chats - if (context.mounted) { - await MainPager.of(context)?.pageController.animateToPage(1, - duration: 250.ms, curve: Curves.easeInOut); - } - }, + onTap: disabled || context.read().isBusy + ? null + : () async { + // Start a chat + final chatListCubit = context.read(); + await chatListCubit.getOrCreateChatSingleContact( + remoteConversationRecordKey: remoteConversationKey); + // Click over to chats + if (context.mounted) { + await MainPager.of(context) + ?.pageController + .animateToPage(1, + duration: 250.ms, curve: Curves.easeInOut); + } + }, title: Text(contact.editedProfile.name), subtitle: (contact.editedProfile.pronouns.isNotEmpty) ? Text(contact.editedProfile.pronouns) diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index 10c4a64..df8cf79 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -12,13 +12,17 @@ import 'contact_item_widget.dart'; import 'empty_contact_list_widget.dart'; class ContactListWidget extends StatelessWidget { - const ContactListWidget({required this.contactList, super.key}); + const ContactListWidget( + {required this.contactList, required this.disabled, super.key}); final IList contactList; + final bool disabled; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(IterableProperty('contactList', contactList)); + properties + ..add(IterableProperty('contactList', contactList)) + ..add(DiagnosticsProperty('disabled', disabled)); } @override @@ -36,7 +40,8 @@ class ContactListWidget extends StatelessWidget { ? const EmptyContactListWidget() : SearchableList( initialList: contactList.toList(), - builder: (l, i, c) => ContactItemWidget(contact: c), + builder: (l, i, c) => + ContactItemWidget(contact: c, disabled: disabled), filter: (value) { final lowerValue = value.toLowerCase(); return contactList diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 73db595..4fee8ab 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -1,4 +1,5 @@ import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -75,8 +76,7 @@ class HomeAccountReadyShellState extends State { // Process all accepted or rejected invitations void _invitationStatusListener( BuildContext context, WaitingInvitationsBlocMapState state) { - _singleInvitationStatusProcessor.updateState(state, - closure: (newState) async { + _singleInvitationStatusProcessor.updateState(state, (newState) async { final contactListCubit = context.read(); final contactInvitationListCubit = context.read(); @@ -146,7 +146,8 @@ class HomeAccountReadyShellState extends State { activeAccountInfo: widget.activeAccountInfo, contactListCubit: context.read()) ..follow( - initialInputState: const AsyncValue.loading(), + initialInputState: + const BlocBusyState(AsyncValue.loading()), stream: context.read().stream)), BlocProvider( create: (context) => @@ -167,7 +168,8 @@ class HomeAccountReadyShellState extends State { activeAccountInfo: widget.activeAccountInfo, account: account) ..follow( - initialInputState: const AsyncValue.loading(), + initialInputState: + const BlocBusyState(AsyncValue.loading()), stream: context .read() .stream)) diff --git a/lib/layout/home/home_account_ready/main_pager/account_page.dart b/lib/layout/home/home_account_ready/main_pager/account_page.dart index 49a5a49..b2c8384 100644 --- a/lib/layout/home/home_account_ready/main_pager/account_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/account_page.dart @@ -38,11 +38,14 @@ class AccountPageState extends State { final textTheme = theme.textTheme; final scale = theme.extension()!; + final cilState = context.watch().state; + final cilBusy = cilState.busy; final contactInvitationRecordList = - context.watch().state.data?.value ?? - const IListConst([]); - final contactList = context.watch().state.data?.value ?? - const IListConst([]); + cilState.state.data?.value ?? const IListConst([]); + + final ciState = context.watch().state; + final ciBusy = ciState.busy; + final contactList = ciState.state.data?.value ?? const IListConst([]); return SizedBox( child: Column(children: [ @@ -66,10 +69,11 @@ class AccountPageState extends State { initiallyExpanded: true, children: [ ContactInvitationListWidget( - contactInvitationRecordList: contactInvitationRecordList) + contactInvitationRecordList: contactInvitationRecordList, + disabled: cilBusy) ], ).paddingLTRB(8, 0, 8, 8), - ContactListWidget(contactList: contactList).expanded(), + ContactListWidget(contactList: contactList, disabled: ciBusy).expanded(), ])); } } diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 60ef937..b3bddb7 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -1,5 +1,6 @@ import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -77,6 +78,17 @@ extension AsyncValueBuilderExt on AsyncValue { data: (d) => debugPage('AsyncValue should not be data here')); } +extension BusyAsyncValueBuilderExt on BlocBusyState> { + Widget builder(Widget Function(BuildContext, T) builder) => + AbsorbPointer(absorbing: busy, child: state.builder(builder)); + Widget buildNotData( + {Widget Function()? loading, + Widget Function(Object, StackTrace?)? error}) => + AbsorbPointer( + absorbing: busy, + child: state.buildNotData(loading: loading, error: error)); +} + class AsyncBlocBuilder>, S> extends BlocBuilder> { AsyncBlocBuilder({ diff --git a/packages/async_tools/lib/async_tools.dart b/packages/async_tools/lib/async_tools.dart index 61d7e7b..70f7b61 100644 --- a/packages/async_tools/lib/async_tools.dart +++ b/packages/async_tools/lib/async_tools.dart @@ -6,3 +6,4 @@ export 'src/async_value.dart'; export 'src/serial_future.dart'; export 'src/single_future.dart'; export 'src/single_state_processor.dart'; +export 'src/single_stateless_processor.dart'; diff --git a/packages/async_tools/lib/src/single_state_processor.dart b/packages/async_tools/lib/src/single_state_processor.dart index ea1af10..18798fa 100644 --- a/packages/async_tools/lib/src/single_state_processor.dart +++ b/packages/async_tools/lib/src/single_state_processor.dart @@ -14,8 +14,7 @@ import '../async_tools.dart'; class SingleStateProcessor { SingleStateProcessor(); - void updateState(State newInputState, - {required Future Function(State) closure}) { + void updateState(State newInputState, Future Function(State) closure) { // Use a singlefuture here to ensure we get dont lose any updates // If the input stream gives us an update while we are // still processing the last update, the most recent input state will diff --git a/packages/async_tools/lib/src/single_stateless_processor.dart b/packages/async_tools/lib/src/single_stateless_processor.dart new file mode 100644 index 0000000..1b96b74 --- /dev/null +++ b/packages/async_tools/lib/src/single_stateless_processor.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import '../async_tools.dart'; + +// Process a single stateless update at a time ensuring each request +// gets processed asynchronously, and continuously while update is requested. +// +// This is useful for processing updates asynchronously without waiting +// from a synchronous execution context +class SingleStatelessProcessor { + SingleStatelessProcessor(); + + void update(Future Function() closure) { + singleFuture(this, () async { + do { + _more = false; + await closure(); + + // See if another update was requested + } while (_more); + }, onBusy: () { + // Keep this state until we process again + _more = true; + }); + } + + // Like update, but with a busy wrapper that clears once the updating is finished + void busyUpdate( + Future Function(Future Function(void Function(S))) busy, + Future Function(void Function(S)) closure) { + singleFuture( + this, + () async => busy((emit) async { + do { + _more = false; + await closure(emit); + + // See if another update was requested + } while (_more); + }), onBusy: () { + // Keep this state until we process again + _more = true; + }); + } + + bool _more = false; +} diff --git a/packages/bloc_tools/lib/src/async_transformer_cubit.dart b/packages/bloc_tools/lib/src/async_transformer_cubit.dart index 83691c6..d32f37e 100644 --- a/packages/bloc_tools/lib/src/async_transformer_cubit.dart +++ b/packages/bloc_tools/lib/src/async_transformer_cubit.dart @@ -10,7 +10,7 @@ class AsyncTransformerCubit extends Cubit> { _subscription = input.stream.listen(_asyncTransform); } void _asyncTransform(AsyncValue newInputState) { - _singleStateProcessor.updateState(newInputState, closure: (newState) async { + _singleStateProcessor.updateState(newInputState, (newState) async { // Emit the transformed state try { if (newState is AsyncLoading) { diff --git a/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart b/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart index a6bb2d7..2307d0e 100644 --- a/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart +++ b/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart @@ -17,7 +17,7 @@ class BlocBusyState extends Equatable { } mixin BlocBusyWrapper on BlocBase> { - Future busy(Future Function(void Function(S) emit) closure) async => + Future busyValue(Future Function(void Function(S) emit) closure) => _mutex.protect(() async { void busyemit(S state) { changedState = state; @@ -41,6 +41,27 @@ mixin BlocBusyWrapper on BlocBase> { return out; }); + Future busy(Future Function(void Function(S) emit) closure) => + _mutex.protect(() async { + void busyemit(S state) { + changedState = state; + } + + // Turn on busy state + emit(BlocBusyState._busy(state.state)); + + // Run the closure + await closure(busyemit); + + // If the closure did one or more 'busy emits' then + // take the most recent one and emit it for real + final finalState = changedState; + if (finalState != null && finalState != state.state) { + emit(BlocBusyState._busy(finalState)); + } else { + emit(BlocBusyState._busy(state.state)); + } + }); void changeState(S state) { if (_mutex.isLocked) { changedState = state; @@ -49,6 +70,8 @@ mixin BlocBusyWrapper on BlocBase> { } } + bool get isBusy => _mutex.isLocked; + final Mutex _mutex = Mutex(); S? changedState; } diff --git a/packages/bloc_tools/lib/src/state_follower.dart b/packages/bloc_tools/lib/src/state_follower.dart index 04b8138..7f69f5e 100644 --- a/packages/bloc_tools/lib/src/state_follower.dart +++ b/packages/bloc_tools/lib/src/state_follower.dart @@ -31,7 +31,7 @@ abstract mixin class StateFollower { void _updateFollow(S newInputState) { _singleStateProcessor.updateState(getStateMap(newInputState), - closure: (newStateMap) async { + (newStateMap) async { for (final k in _lastInputStateMap.keys) { if (!newStateMap.containsKey(k)) { // deleted diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index 79837b2..8309254 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -4,18 +4,19 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:mutex/mutex.dart'; import '../../veilid_support.dart'; -class DHTShortArrayCubit extends Cubit>>> - with BlocBusyWrapper>> { +typedef DHTShortArrayState = AsyncValue>; +typedef DHTShortArrayBusyState = BlocBusyState>; + +class DHTShortArrayCubit extends Cubit> + with BlocBusyWrapper> { DHTShortArrayCubit({ required Future Function() open, required T Function(List data) decodeElement, }) : _decodeElement = decodeElement, - _wantsUpdate = false, - _isUpdating = false, - _wantsCloseRecord = false, super(const BlocBusyState(AsyncValue.loading())) { Future.delayed(Duration.zero, () async { // Open DHT record @@ -33,9 +34,6 @@ class DHTShortArrayCubit extends Cubit>>> required T Function(List data) decodeElement, }) : _shortArray = shortArray, _decodeElement = decodeElement, - _wantsUpdate = false, - _isUpdating = false, - _wantsCloseRecord = false, super(const BlocBusyState(AsyncValue.loading())) { // Make initial state update _update(); @@ -59,37 +57,21 @@ class DHTShortArrayCubit extends Cubit>>> void _update() { // Run at most one background update process - -xxx convert to singleFuture with onBusy that sets wantsupdate - - _wantsUpdate = true; - if (_isUpdating) { - return; - } - _isUpdating = true; - Future.delayed(Duration.zero, () async { - // Keep updating until we don't want to update any more - // Because this is async, we could get an update while we're - // still processing the last one + // Because this is async, we could get an update while we're + // still processing the last one + _sspUpdate.busyUpdate>>(busy, (emit) async { try { - do { - _wantsUpdate = false; - try { - final initialState = await _getElements(); - emit(AsyncValue.data(initialState)); - } on Exception catch (e) { - emit(AsyncValue.error(e)); - } - } while (_wantsUpdate); - } finally { - // Note that this update future has finished - _isUpdating = false; + final initialState = await _getElementsInner(); + emit(AsyncValue.data(initialState)); + } on Exception catch (e) { + emit(AsyncValue.error(e)); } }); } // Get and decode the entire short array - Future> _getElements() async { + Future> _getElementsInner() async { + assert(isBusy, 'should only be called from a busy state'); var out = IList(); for (var i = 0; i < _shortArray.length; i++) { // Get the element bytes (throw if fails, array state is invalid) @@ -112,12 +94,13 @@ xxx convert to singleFuture with onBusy that sets wantsupdate await super.close(); } - DHTShortArray get shortArray => _shortArray; + Future operate(Future Function(DHTShortArray) closure) async => + _operateMutex.protect(() async => closure(_shortArray)); + final _operateMutex = Mutex(); late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; StreamSubscription? _subscription; - bool _wantsUpdate; - bool _isUpdating; - bool _wantsCloseRecord; + bool _wantsCloseRecord = false; + final _sspUpdate = SingleStatelessProcessor(); }