fix deadlock

clean up async handling
improve styled alerts
This commit is contained in:
Christien Rioux 2024-08-04 18:49:49 -05:00
parent 22390f31ff
commit 8edccb8a0f
21 changed files with 125 additions and 108 deletions

View file

@ -4,6 +4,7 @@
}, },
"menu": { "menu": {
"accounts_menu_tooltip": "Accounts Menu", "accounts_menu_tooltip": "Accounts Menu",
"settings_tooltip": "Settings",
"contacts_tooltip": "Contacts List", "contacts_tooltip": "Contacts List",
"new_chat_tooltip": "Start New Chat", "new_chat_tooltip": "Start New Chat",
"add_account_tooltip": "Add Account", "add_account_tooltip": "Add Account",

View file

@ -8,7 +8,7 @@ import '../../proto/proto.dart' as proto;
import '../account_manager.dart'; import '../account_manager.dart';
typedef AccountRecordState = proto.Account; typedef AccountRecordState = proto.Account;
typedef _sspUpdateState = ( typedef _SspUpdateState = (
AccountSpec accountSpec, AccountSpec accountSpec,
Future<void> Function() onSuccess Future<void> Function() onSuccess
); );
@ -96,5 +96,5 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
} }
} }
final _sspUpdate = SingleStateProcessor<_sspUpdateState>(); final _sspUpdate = SingleStateProcessor<_SspUpdateState>();
} }

View file

@ -24,13 +24,13 @@ class PerAccountCollectionBlocMapCubit extends BlocMapCubit<TypedKey,
// Add account record cubit // Add account record cubit
Future<void> _addPerAccountCollectionCubit( Future<void> _addPerAccountCollectionCubit(
{required TypedKey superIdentityRecordKey}) async => {required TypedKey superIdentityRecordKey}) async =>
add(() => MapEntry( add(
superIdentityRecordKey, superIdentityRecordKey,
PerAccountCollectionCubit( () async => PerAccountCollectionCubit(
locator: _locator, locator: _locator,
accountInfoCubit: AccountInfoCubit( accountInfoCubit: AccountInfoCubit(
accountRepository: _accountRepository, accountRepository: _accountRepository,
superIdentityRecordKey: superIdentityRecordKey)))); superIdentityRecordKey: superIdentityRecordKey)));
/// StateFollower ///////////////////////// /// StateFollower /////////////////////////

View file

@ -78,13 +78,13 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
await _accountRecordSubscription?.cancel(); await _accountRecordSubscription?.cancel();
_accountRecordSubscription = null; _accountRecordSubscription = null;
// Update state to 'loading'
nextState = _updateAccountRecordState(nextState, null);
emit(nextState);
// Close AccountRecordCubit // Close AccountRecordCubit
await accountRecordCubit?.close(); await accountRecordCubit?.close();
accountRecordCubit = null; accountRecordCubit = null;
// Update state to 'loading'
nextState = _updateAccountRecordState(nextState, null);
emit(nextState);
} else { } else {
///////////////// Logged in /////////////////// ///////////////// Logged in ///////////////////

View file

@ -120,23 +120,23 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
try { try {
final success = await AccountRepository.instance.deleteLocalAccount( final success = await AccountRepository.instance.deleteLocalAccount(
widget.superIdentityRecordKey, widget.accountRecord); widget.superIdentityRecordKey, widget.accountRecord);
if (success && mounted) { if (mounted) {
if (success) {
context context
.read<NotificationsCubit>() .read<NotificationsCubit>()
.info(text: translate('edit_account_page.account_removed')); .info(text: translate('edit_account_page.account_removed'));
GoRouterHelper(context).pop(); GoRouterHelper(context).pop();
} else if (mounted) { } else {
context context
.read<NotificationsCubit>() .read<NotificationsCubit>()
.error(text: translate('edit_account_page.failed_to_remove')); .error(text: translate('edit_account_page.failed_to_remove'));
} }
}
} finally { } finally {
if (mounted) {
setState(() { setState(() {
_isInAsyncCall = false; _isInAsyncCall = false;
}); });
} }
}
} on Exception catch (e, st) { } on Exception catch (e, st) {
if (mounted) { if (mounted) {
await showErrorStacktraceModal( await showErrorStacktraceModal(
@ -188,23 +188,22 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
try { try {
final success = await AccountRepository.instance.destroyAccount( final success = await AccountRepository.instance.destroyAccount(
widget.superIdentityRecordKey, widget.accountRecord); widget.superIdentityRecordKey, widget.accountRecord);
if (success && mounted) { if (mounted) {
if (success) {
context context
.read<NotificationsCubit>() .read<NotificationsCubit>()
.info(text: translate('edit_account_page.account_destroyed')); .info(text: translate('edit_account_page.account_destroyed'));
GoRouterHelper(context).pop(); GoRouterHelper(context).pop();
} else if (mounted) { } else {
context context.read<NotificationsCubit>().error(
.read<NotificationsCubit>() text: translate('edit_account_page.failed_to_destroy'));
.error(text: translate('edit_account_page.failed_to_destroy')); }
} }
} finally { } finally {
if (mounted) {
setState(() { setState(() {
_isInAsyncCall = false; _isInAsyncCall = false;
}); });
} }
}
} on Exception catch (e, st) { } on Exception catch (e, st) {
if (mounted) { if (mounted) {
await showErrorStacktraceModal( await showErrorStacktraceModal(

View file

@ -252,7 +252,7 @@ class ContactInvitationListCubit
.openRecordRead(contactRequestInboxKey, .openRecordRead(contactRequestInboxKey,
debugName: 'ContactInvitationListCubit::validateInvitation::' debugName: 'ContactInvitationListCubit::validateInvitation::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: pool.getParentRecordKey(contactRequestInboxKey) ?? parent: await pool.getParentRecordKey(contactRequestInboxKey) ??
_accountInfo.accountRecordKey) _accountInfo.accountRecordKey)
.withCancel(cancelRequest)) .withCancel(cancelRequest))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async { .maybeDeleteScope(!isSelf, (contactRequestInbox) async {

View file

@ -48,15 +48,15 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
Future<void> _addWaitingInvitation( Future<void> _addWaitingInvitation(
{required proto.ContactInvitationRecord {required proto.ContactInvitationRecord
contactInvitationRecord}) async => contactInvitationRecord}) async =>
add(() => MapEntry( add(
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(), contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(),
WaitingInvitationCubit( () async => WaitingInvitationCubit(
ContactRequestInboxCubit( ContactRequestInboxCubit(
accountInfo: _accountInfo, accountInfo: _accountInfo,
contactInvitationRecord: contactInvitationRecord), contactInvitationRecord: contactInvitationRecord),
accountInfo: _accountInfo, accountInfo: _accountInfo,
accountRecordCubit: _accountRecordCubit, accountRecordCubit: _accountRecordCubit,
contactInvitationRecord: contactInvitationRecord))); contactInvitationRecord: contactInvitationRecord));
// Process all accepted or rejected invitations // Process all accepted or rejected invitations
Future<void> _invitationStatusListener( Future<void> _invitationStatusListener(

View file

@ -37,7 +37,7 @@ class ValidContactInvitation {
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::accept::' debugName: 'ValidContactInvitation::accept::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: pool.getParentRecordKey(_contactRequestInboxKey) ?? parent: await pool.getParentRecordKey(_contactRequestInboxKey) ??
_accountInfo.accountRecordKey)) _accountInfo.accountRecordKey))
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
.maybeDeleteScope(!isSelf, (contactRequestInbox) async { .maybeDeleteScope(!isSelf, (contactRequestInbox) async {

View file

@ -78,7 +78,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
{required TypedKey remoteIdentityPublicKey, {required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationRecordKey, required TypedKey localConversationRecordKey,
required TypedKey remoteConversationRecordKey}) async => required TypedKey remoteConversationRecordKey}) async =>
add(() { add(localConversationRecordKey, () async {
// Conversation cubit the tracks the state between the local // Conversation cubit the tracks the state between the local
// and remote halves of a contact's relationship with this account // and remote halves of a contact's relationship with this account
final conversationCubit = ConversationCubit( final conversationCubit = ConversationCubit(
@ -123,7 +123,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
loading: AsyncValue.loading, loading: AsyncValue.loading,
error: AsyncValue.error)); error: AsyncValue.error));
return MapEntry(localConversationRecordKey, transformedCubit); return transformedCubit;
}); });
/// StateFollower ///////////////////////// /// StateFollower /////////////////////////

View file

@ -55,24 +55,17 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
Future<void> _addConversationMessages(_SingleContactChatState state) async { Future<void> _addConversationMessages(_SingleContactChatState state) async {
// xxx could use atomic update() function // xxx could use atomic update() function
await update(state.localConversationRecordKey,
final cubit = await tryOperateAsync<SingleContactMessagesCubit>( onUpdate: (cubit) async =>
state.localConversationRecordKey, closure: (cubit) async { cubit.updateRemoteMessagesRecordKey(state.remoteMessagesRecordKey),
await cubit.updateRemoteMessagesRecordKey(state.remoteMessagesRecordKey); onCreate: () async => SingleContactMessagesCubit(
return cubit;
});
if (cubit == null) {
await add(() => MapEntry(
state.localConversationRecordKey,
SingleContactMessagesCubit(
accountInfo: _accountInfo, accountInfo: _accountInfo,
remoteIdentityPublicKey: state.remoteIdentityPublicKey, remoteIdentityPublicKey: state.remoteIdentityPublicKey,
localConversationRecordKey: state.localConversationRecordKey, localConversationRecordKey: state.localConversationRecordKey,
remoteConversationRecordKey: state.remoteConversationRecordKey, remoteConversationRecordKey: state.remoteConversationRecordKey,
localMessagesRecordKey: state.localMessagesRecordKey, localMessagesRecordKey: state.localMessagesRecordKey,
remoteMessagesRecordKey: state.remoteMessagesRecordKey, remoteMessagesRecordKey: state.remoteMessagesRecordKey,
))); ));
}
} }
_SingleContactChatState? _mapStateValue( _SingleContactChatState? _mapStateValue(

View file

@ -73,7 +73,8 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final record = await pool.openRecordRead(_remoteConversationRecordKey, final record = await pool.openRecordRead(_remoteConversationRecordKey,
debugName: 'ConversationCubit::RemoteConversation', debugName: 'ConversationCubit::RemoteConversation',
parent: pool.getParentRecordKey(_remoteConversationRecordKey) ?? parent:
await pool.getParentRecordKey(_remoteConversationRecordKey) ??
accountInfo.accountRecordKey, accountInfo.accountRecordKey,
crypto: crypto); crypto: crypto);

View file

@ -95,8 +95,8 @@ Future<void> showErrorModal(
required String title, required String title,
required String text}) async { required String text}) async {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; // final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; // final scaleConfig = theme.extension<ScaleConfig>()!;
await Alert( await Alert(
context: context, context: context,
@ -145,8 +145,8 @@ Future<void> showWarningModal(
required String title, required String title,
required String text}) async { required String text}) async {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; // final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; // final scaleConfig = theme.extension<ScaleConfig>()!;
await Alert( await Alert(
context: context, context: context,
@ -184,8 +184,8 @@ Future<void> showWarningWidgetModal(
required String title, required String title,
required Widget child}) async { required Widget child}) async {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; // final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; // final scaleConfig = theme.extension<ScaleConfig>()!;
await Alert( await Alert(
context: context, context: context,
@ -223,8 +223,8 @@ Future<bool> showConfirmModal(
required String title, required String title,
required String text}) async { required String text}) async {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; // final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; // final scaleConfig = theme.extension<ScaleConfig>()!;
var confirm = false; var confirm = false;

View file

@ -28,11 +28,7 @@ class BackgroundTickerState extends State<BackgroundTicker> {
@override @override
void dispose() { void dispose() {
final tickTimer = _tickTimer; _tickTimer?.cancel();
if (tickTimer != null) {
tickTimer.cancel();
}
super.dispose(); super.dispose();
} }

View file

@ -37,10 +37,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: async_tools name: async_tools
sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.5" version: "0.1.6"
bloc: bloc:
dependency: transitive dependency: transitive
description: description:
@ -53,10 +53,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: bloc_advanced_tools name: bloc_advanced_tools
sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94" sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.5" version: "0.1.7"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -650,7 +650,7 @@ packages:
path: "../../../../veilid/veilid-flutter" path: "../../../../veilid/veilid-flutter"
relative: true relative: true
source: path source: path
version: "0.3.3" version: "0.3.4"
veilid_support: veilid_support:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -14,7 +14,7 @@ dependencies:
path: ../ path: ../
dev_dependencies: dev_dependencies:
async_tools: ^0.1.5 async_tools: ^0.1.6
integration_test: integration_test:
sdk: flutter sdk: flutter
lint_hard: ^4.0.0 lint_hard: ^4.0.0

View file

@ -79,7 +79,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
return false; return false;
} }
await serialFuturePause((this, _sfListen)); await serialFutureClose((this, _sfListen));
await _watchController?.close(); await _watchController?.close();
_watchController = null; _watchController = null;
await DHTRecordPool.instance._recordClosed(this); await DHTRecordPool.instance._recordClosed(this);

View file

@ -65,7 +65,7 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer {
class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> { class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext)
: _state = const DHTRecordPoolAllocations(), : _state = const DHTRecordPoolAllocations(),
_mutex = Mutex(), _mutex = Mutex(debugLockTimeout: 30),
_recordTagLock = AsyncTagLock(), _recordTagLock = AsyncTagLock(),
_opened = <TypedKey, _OpenedRecordInfo>{}, _opened = <TypedKey, _OpenedRecordInfo>{},
_markedForDelete = <TypedKey>{}, _markedForDelete = <TypedKey>{},
@ -207,10 +207,8 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
); );
/// Get the parent of a DHTRecord key if it exists /// Get the parent of a DHTRecord key if it exists
TypedKey? getParentRecordKey(TypedKey child) { Future<TypedKey?> getParentRecordKey(TypedKey child) =>
final childJson = child.toJson(); _mutex.protect(() async => _getParentRecordKeyInner(child));
return _state.parentByChild[childJson];
}
/// Check if record is allocated /// Check if record is allocated
Future<bool> isValidRecordKey(TypedKey key) => Future<bool> isValidRecordKey(TypedKey key) =>
@ -505,12 +503,16 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
// Check to see if this key can finally be deleted // Check to see if this key can finally be deleted
// If any parents are marked for deletion, try them first // If any parents are marked for deletion, try them first
Future<void> _checkForLateDeletesInner(TypedKey key) async { Future<void> _checkForLateDeletesInner(TypedKey key) async {
if (!_mutex.isLocked) {
throw StateError('should be locked here');
}
// Get parent list in bottom up order including our own key // Get parent list in bottom up order including our own key
final parents = <TypedKey>[]; final parents = <TypedKey>[];
TypedKey? nextParent = key; TypedKey? nextParent = key;
while (nextParent != null) { while (nextParent != null) {
parents.add(nextParent); parents.add(nextParent);
nextParent = getParentRecordKey(nextParent); nextParent = _getParentRecordKeyInner(nextParent);
} }
// If any parent is ready to delete all its children do it // If any parent is ready to delete all its children do it
@ -547,6 +549,10 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
// Actual delete function // Actual delete function
Future<void> _finalizeDeleteRecordInner(TypedKey recordKey) async { Future<void> _finalizeDeleteRecordInner(TypedKey recordKey) async {
if (!_mutex.isLocked) {
throw StateError('should be locked here');
}
log('_finalizeDeleteRecordInner: key=$recordKey'); log('_finalizeDeleteRecordInner: key=$recordKey');
// Remove this child from parents // Remove this child from parents
@ -557,6 +563,10 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
// Deep delete mechanism inside mutex // Deep delete mechanism inside mutex
Future<bool> _deleteRecordInner(TypedKey recordKey) async { Future<bool> _deleteRecordInner(TypedKey recordKey) async {
if (!_mutex.isLocked) {
throw StateError('should be locked here');
}
final toDelete = _readyForDeleteInner(recordKey); final toDelete = _readyForDeleteInner(recordKey);
if (toDelete.isNotEmpty) { if (toDelete.isNotEmpty) {
// delete now // delete now
@ -656,7 +666,20 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
} }
} }
TypedKey? _getParentRecordKeyInner(TypedKey child) {
if (!_mutex.isLocked) {
throw StateError('should be locked here');
}
final childJson = child.toJson();
return _state.parentByChild[childJson];
}
bool _isValidRecordKeyInner(TypedKey key) { bool _isValidRecordKeyInner(TypedKey key) {
if (!_mutex.isLocked) {
throw StateError('should be locked here');
}
if (_state.rootRecords.contains(key)) { if (_state.rootRecords.contains(key)) {
return true; return true;
} }
@ -667,6 +690,10 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
} }
bool _isDeletedRecordKeyInner(TypedKey key) { bool _isDeletedRecordKeyInner(TypedKey key) {
if (!_mutex.isLocked) {
throw StateError('should be locked here');
}
// Is this key gone? // Is this key gone?
if (!_isValidRecordKeyInner(key)) { if (!_isValidRecordKeyInner(key)) {
return true; return true;
@ -679,7 +706,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
if (_markedForDelete.contains(nextParent)) { if (_markedForDelete.contains(nextParent)) {
return true; return true;
} }
nextParent = getParentRecordKey(nextParent); nextParent = _getParentRecordKeyInner(nextParent);
} }
return false; return false;

View file

@ -37,10 +37,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: async_tools name: async_tools
sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.5" version: "0.1.6"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -53,10 +53,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: bloc_advanced_tools name: bloc_advanced_tools
sha256: "2ad82be752ab5e983ad9097ed9f334e47a4472c04d5c6b61c99a1bb14a039053" sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.6" version: "0.1.7"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:

View file

@ -7,9 +7,9 @@ environment:
sdk: '>=3.2.0 <4.0.0' sdk: '>=3.2.0 <4.0.0'
dependencies: dependencies:
async_tools: ^0.1.5 async_tools: ^0.1.6
bloc: ^8.1.4 bloc: ^8.1.4
bloc_advanced_tools: ^0.1.6 bloc_advanced_tools: ^0.1.7
charcode: ^1.3.1 charcode: ^1.3.1
collection: ^1.18.0 collection: ^1.18.0
equatable: ^2.0.5 equatable: ^2.0.5

View file

@ -85,10 +85,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: async_tools name: async_tools
sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.5" version: "0.1.6"
awesome_extensions: awesome_extensions:
dependency: "direct main" dependency: "direct main"
description: description:
@ -141,10 +141,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: bloc_advanced_tools name: bloc_advanced_tools
sha256: "2ad82be752ab5e983ad9097ed9f334e47a4472c04d5c6b61c99a1bb14a039053" sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.6" version: "0.1.7"
blurry_modal_progress_hud: blurry_modal_progress_hud:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -14,12 +14,12 @@ dependencies:
animated_theme_switcher: ^2.0.10 animated_theme_switcher: ^2.0.10
ansicolor: ^2.0.2 ansicolor: ^2.0.2
archive: ^3.6.1 archive: ^3.6.1
async_tools: ^0.1.5 async_tools: ^0.1.6
awesome_extensions: ^2.0.16 awesome_extensions: ^2.0.16
badges: ^3.1.2 badges: ^3.1.2
basic_utils: ^5.7.0 basic_utils: ^5.7.0
bloc: ^8.1.4 bloc: ^8.1.4
bloc_advanced_tools: ^0.1.6 bloc_advanced_tools: ^0.1.7
blurry_modal_progress_hud: ^1.1.1 blurry_modal_progress_hud: ^1.1.1
change_case: ^2.1.0 change_case: ^2.1.0
charcode: ^1.3.1 charcode: ^1.3.1