diff --git a/lib/account_manager/views/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page/new_account_page.dart index b249a99..f81f30d 100644 --- a/lib/account_manager/views/new_account_page/new_account_page.dart +++ b/lib/account_manager/views/new_account_page/new_account_page.dart @@ -54,12 +54,14 @@ class NewAccountPageState extends State { validator: FormBuilderValidators.compose([ FormBuilderValidators.required(), ]), + textInputAction: TextInputAction.next, ), FormBuilderTextField( name: formFieldPronouns, maxLength: 64, decoration: InputDecoration( labelText: translate('account.form_pronouns')), + textInputAction: TextInputAction.next, ), Row(children: [ const Spacer(), diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 420dfbf..0591b7e 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -83,12 +83,10 @@ class ChatListCubit extends DHTShortArrayCubit { Future deleteChat( {required TypedKey remoteConversationRecordKey}) async { final remoteConversationKey = remoteConversationRecordKey.toProto(); - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later - await operate((shortArray) async { + final deletedItem = await operate((shortArray) async { if (activeChatCubit.state == remoteConversationRecordKey) { activeChatCubit.setActiveChat(null); } @@ -101,19 +99,21 @@ class ChatListCubit extends DHTShortArrayCubit { if (c.remoteConversationRecordKey == remoteConversationKey) { // Found the right chat if (await shortArray.tryRemoveItem(i) != null) { - try { - await (await DHTShortArray.openOwned( - c.reconciledChatRecord.toVeilid(), - parent: accountRecordKey)) - .delete(); - } on Exception catch (e) { - log.debug('error removing reconciled chat record: $e', e); - } + return c; } - return; + return null; } } + return null; }); + if (deletedItem != null) { + try { + await DHTRecordPool.instance + .delete(deletedItem.reconciledChatRecord.toVeilid().recordKey); + } on Exception catch (e) { + log.debug('error removing reconciled chat record: $e', e); + } + } } final ActiveChatCubit activeChatCubit; diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 4ffd2bd..37aafb9 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -6,6 +6,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; import '../models/models.dart'; ////////////////////////////////////////////////// @@ -157,7 +158,7 @@ class ContactInvitationListCubit _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list - await operate((shortArray) async { + final deletedItem = await operate((shortArray) async { for (var i = 0; i < shortArray.length; i++) { final item = await shortArray.getItemProtobuf( proto.ContactInvitationRecord.fromBuffer, i); @@ -166,25 +167,37 @@ class ContactInvitationListCubit } 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(); + if (await shortArray.tryRemoveItem(i) != null) { + return item; } - return; + return null; } } + return null; }); + + if (deletedItem != null) { + // Delete the contact request inbox + final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid(); + await (await pool.openOwned(contactRequestInbox, + parent: accountRecordKey)) + .scope((contactRequestInbox) async { + // Wipe out old invitation so it shows up as invalid + await contactRequestInbox.tryWriteBytes(Uint8List(0)); + }); + try { + await pool.delete(contactRequestInbox.recordKey); + } on Exception catch (e) { + log.debug('error removing contact request inbox: $e', e); + } + if (!accepted) { + try { + await pool.delete(deletedItem.localConversationRecordKey.toVeilid()); + } on Exception catch (e) { + log.debug('error removing local conversation record: $e', e); + } + } + } } Future validateInvitation( diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index af66ac7..7f23475 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -14,8 +14,7 @@ class ContactListCubit extends DHTShortArrayCubit { ContactListCubit({ required ActiveAccountInfo activeAccountInfo, required proto.Account account, - }) : _activeAccountInfo = activeAccountInfo, - super( + }) : super( open: () => _open(activeAccountInfo, account), decodeElement: proto.Contact.fromBuffer); @@ -62,14 +61,12 @@ class ContactListCubit extends DHTShortArrayCubit { Future deleteContact({required proto.Contact contact}) async { final pool = DHTRecordPool.instance; - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; final localConversationKey = contact.localConversationRecordKey.toVeilid(); final remoteConversationKey = contact.remoteConversationRecordKey.toVeilid(); // Remove Contact from account's list - await operate((shortArray) async { + final deletedItem = await operate((shortArray) async { for (var i = 0; i < shortArray.length; i++) { final item = await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); @@ -78,29 +75,28 @@ class ContactListCubit extends DHTShortArrayCubit { } if (item.remoteConversationRecordKey == contact.remoteConversationRecordKey) { - await shortArray.tryRemoveItem(i); - break; + if (await shortArray.tryRemoveItem(i) != null) { + return item; + } + return null; } } + return null; + }); + + if (deletedItem != null) { try { - await (await pool.openRead(localConversationKey, - parent: accountRecordKey)) - .delete(); + await pool.delete(localConversationKey); } on Exception catch (e) { log.debug('error removing local conversation record key: $e', e); } try { if (localConversationKey != remoteConversationKey) { - await (await pool.openRead(remoteConversationKey, - parent: accountRecordKey)) - .delete(); + await pool.delete(remoteConversationKey); } } on Exception catch (e) { log.debug('error removing remote conversation record key: $e', e); } - }); + } } - - // - final ActiveAccountInfo _activeAccountInfo; } diff --git a/lib/main.dart b/lib/main.dart index 49c3cb7..64ee506 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,8 +27,7 @@ void main() async { // Ansi colors ansiColorDisabled = false; - // Catch errors - await runZonedGuarded(() async { + Future mainFunc() async { // Logs initLoggy(); @@ -53,7 +52,15 @@ void main() async { // Hot reloads will only restart this part, not Veilid runApp(LocalizedApp(localizationDelegate, VeilidChatApp(initialThemeData: initialThemeData))); - }, (error, stackTrace) { - log.error('Dart Runtime: {$error}\n{$stackTrace}'); - }); + } + + if (kDebugMode) { + // In debug mode, run the app without catching exceptions for debugging + await mainFunc(); + } else { + // Catch errors in production without killing the app + await runZonedGuarded(mainFunc, (error, stackTrace) { + log.error('Dart Runtime: {$error}\n{$stackTrace}'); + }); + } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart index 713b076..b5ae275 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart @@ -27,7 +27,6 @@ class DHTRecord { _defaultSubkey = defaultSubkey, _writer = writer, _open = true, - _valid = true, _sharedDHTRecordData = sharedDHTRecordData; final SharedDHTRecordData _sharedDHTRecordData; @@ -37,7 +36,6 @@ class DHTRecord { final DHTRecordCrypto _crypto; bool _open; - bool _valid; @internal StreamController? watchController; @internal @@ -59,9 +57,6 @@ class DHTRecord { OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); Future close() async { - if (!_valid) { - throw StateError('already deleted'); - } if (!_open) { return; } @@ -70,33 +65,26 @@ class DHTRecord { _open = false; } - void _markDeleted() { - _valid = false; - } - - Future delete() => DHTRecordPool.instance.delete(key); - Future scope(Future Function(DHTRecord) scopeFunction) async { try { return await scopeFunction(this); } finally { - if (_valid) { - await close(); - } + await close(); } } Future deleteScope(Future Function(DHTRecord) scopeFunction) async { try { final out = await scopeFunction(this); - if (_valid && _open) { + if (_open) { await close(); } return out; } on Exception catch (_) { - if (_valid) { - await delete(); + if (_open) { + await close(); } + await DHTRecordPool.instance.delete(key); rethrow; } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart index cce18c7..b78109b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart @@ -73,6 +73,7 @@ class SharedDHTRecordData { VeilidRoutingContext defaultRoutingContext; Map subkeySeqCache = {}; bool needsWatchStateUpdate = false; + bool deleteOnClose = false; } // Per opened record data @@ -182,7 +183,7 @@ class DHTRecordPool with TableDBBacked { // If we are opening a key that already exists // make sure we are using the same parent if one was specified - _validateParent(parent, recordKey); + _validateParentInner(parent, recordKey); // See if this has been opened yet final openedRecordInfo = _opened[recordKey]; @@ -232,54 +233,58 @@ class DHTRecordPool with TableDBBacked { } if (openedRecordInfo.records.isEmpty) { await _routingContext.closeDHTRecord(key); + if (openedRecordInfo.shared.deleteOnClose) { + await _deleteInner(key); + } _opened.remove(key); } }); } - Future delete(TypedKey recordKey) async { - final allDeletedRecords = {}; - final allDeletedRecordKeys = []; + // Collect all dependencies (including the record itself) + // in reverse (bottom-up/delete order) + List _collectChildrenInner(TypedKey recordKey) { + assert(_mutex.isLocked, 'should be locked here'); - await _mutex.protect(() async { - // Collect all dependencies (including the record itself) - final allDeps = []; - final currentDeps = [recordKey]; - while (currentDeps.isNotEmpty) { - final nextDep = currentDeps.removeLast(); + final allDeps = []; + final currentDeps = [recordKey]; + while (currentDeps.isNotEmpty) { + final nextDep = currentDeps.removeLast(); - // Remove this child from its parent - await _removeDependencyInner(nextDep); - - allDeps.add(nextDep); - final childDeps = - _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; - currentDeps.addAll(childDeps); - } - - // Delete all dependent records in parallel (including the record itself) - for (final dep in allDeps) { - // If record is opened, close it first - final openinfo = _opened[dep]; - if (openinfo != null) { - for (final rec in openinfo.records) { - allDeletedRecords.add(rec); - } - } - // Then delete - allDeletedRecordKeys.add(dep); - } - }); - - await Future.wait(allDeletedRecords.map((r) => r.close())); - for (final deletedRecord in allDeletedRecords) { - deletedRecord._markDeleted(); + allDeps.add(nextDep); + final childDeps = + _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; + currentDeps.addAll(childDeps); } - await Future.wait( - allDeletedRecordKeys.map(_routingContext.deleteDHTRecord)); + return allDeps.reversedView; } - void _validateParent(TypedKey? parent, TypedKey child) { + Future _deleteInner(TypedKey recordKey) async { + // Remove this child from parents + await _removeDependenciesInner([recordKey]); + await _routingContext.deleteDHTRecord(recordKey); + } + + Future delete(TypedKey recordKey) async { + await _mutex.protect(() async { + final allDeps = _collectChildrenInner(recordKey); + + assert(allDeps.singleOrNull == recordKey, 'must delete children first'); + + final ori = _opened[recordKey]; + if (ori != null) { + // delete after close + ori.shared.deleteOnClose = true; + } else { + // delete now + await _deleteInner(recordKey); + } + }); + } + + void _validateParentInner(TypedKey? parent, TypedKey child) { + assert(_mutex.isLocked, 'should be locked here'); + final childJson = child.toJson(); final existingParent = _state.parentByChild[childJson]; if (parent == null) { @@ -319,29 +324,35 @@ class DHTRecordPool with TableDBBacked { } } - Future _removeDependencyInner(TypedKey child) async { + Future _removeDependenciesInner(List childList) async { assert(_mutex.isLocked, 'should be locked here'); - if (_state.rootRecords.contains(child)) { - _state = await store( - _state.copyWith(rootRecords: _state.rootRecords.remove(child))); - } else { - final parent = _state.parentByChild[child.toJson()]; - if (parent == null) { - return; - } - final children = _state.childrenByParent[parent.toJson()]!.remove(child); - late final DHTRecordPoolAllocations newState; - if (children.isEmpty) { - newState = _state.copyWith( - childrenByParent: _state.childrenByParent.remove(parent.toJson()), - parentByChild: _state.parentByChild.remove(child.toJson())); + + var state = _state; + + for (final child in childList) { + if (_state.rootRecords.contains(child)) { + state = state.copyWith(rootRecords: state.rootRecords.remove(child)); } else { - newState = _state.copyWith( - childrenByParent: - _state.childrenByParent.add(parent.toJson(), children), - parentByChild: _state.parentByChild.remove(child.toJson())); + final parent = state.parentByChild[child.toJson()]; + if (parent == null) { + continue; + } + final children = state.childrenByParent[parent.toJson()]!.remove(child); + if (children.isEmpty) { + state = state.copyWith( + childrenByParent: state.childrenByParent.remove(parent.toJson()), + parentByChild: state.parentByChild.remove(child.toJson())); + } else { + state = state.copyWith( + childrenByParent: + state.childrenByParent.add(parent.toJson(), children), + parentByChild: state.parentByChild.remove(child.toJson())); + } } - _state = await store(newState); + } + + if (state != _state) { + _state = await store(state); } } @@ -595,10 +606,10 @@ class DHTRecordPool with TableDBBacked { _tickCount = 0; try { - // See if any opened records need watch state changes - final unord = Function()>[]; + final allSuccess = await _mutex.protect(() async { + // See if any opened records need watch state changes + final unord = Function()>[]; - await _mutex.protect(() async { for (final kv in _opened.entries) { final openedRecordKey = kv.key; final openedRecordInfo = kv.value; @@ -647,13 +658,15 @@ class DHTRecordPool with TableDBBacked { } } } + + // Process all watch changes + return unord.isEmpty || + (await unord.map((f) => f()).wait).reduce((a, b) => a && b); }); - // Process all watch changes // If any watched did not success, back off the attempts to // update the watches for a bit - final allSuccess = unord.isEmpty || - (await unord.map((f) => f()).wait).reduce((a, b) => a && b); + if (!allSuccess) { _watchBackoffTimer *= watchBackoffMultiplier; _watchBackoffTimer = min(_watchBackoffTimer, watchBackoffMax); diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart index 8136a61..2fca99e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart @@ -16,7 +16,11 @@ class DHTShortArray { // Constructors DHTShortArray._({required DHTRecord headRecord}) - : _head = _DHTShortArrayHead(headRecord: headRecord) {} + : _head = _DHTShortArrayHead(headRecord: headRecord) { + _head.onUpdatedHead = () { + _watchController?.sink.add(null); + }; + } // Create a DHTShortArray // if smplWriter is specified, uses a SMPL schema with a single writer @@ -52,12 +56,15 @@ class DHTShortArray { try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - if (!await dhtShortArray._head._tryWriteHead()) { - throw StateError('Failed to write head at this time'); - } + await dhtShortArray._head.operate((head) async { + if (!await head._writeHead()) { + throw StateError('Failed to write head at this time'); + } + }); return dhtShortArray; } on Exception catch (_) { - await dhtRecord.delete(); + await dhtRecord.close(); + await pool.delete(dhtRecord.key); rethrow; } } @@ -70,7 +77,7 @@ class DHTShortArray { parent: parent, routingContext: routingContext, crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - await dhtShortArray._head._refreshInner(); + await dhtShortArray._head.operate((head) => head._loadHead()); return dhtShortArray; } on Exception catch (_) { await dhtRecord.close(); @@ -90,7 +97,7 @@ class DHTShortArray { parent: parent, routingContext: routingContext, crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - await dhtShortArray._head._refreshInner(); + await dhtShortArray._head.operate((head) => head._loadHead()); return dhtShortArray; } on Exception catch (_) { await dhtRecord.close(); @@ -115,13 +122,14 @@ class DHTShortArray { //////////////////////////////////////////////////////////////////////////// // Public API - // External references for the shortarray - TypedKey get recordKey => _head.headRecord.key; - OwnedDHTRecordPointer get recordPointer => - _head.headRecord.ownedDHTRecordPointer; + /// Get the record key for this shortarray + TypedKey get recordKey => _head.recordKey; + + /// Get the record pointer foir this shortarray + OwnedDHTRecordPointer get recordPointer => _head.recordPointer; /// Returns the number of elements in the DHTShortArray - int get length => _head.index.length; + int get length => _head.length; /// Free all resources for the DHTShortArray Future close() async { @@ -131,8 +139,8 @@ class DHTShortArray { /// Free all resources for the DHTShortArray and delete it from the DHT Future delete() async { - await _watchController?.close(); - await _head.delete(); + await close(); + await DHTRecordPool.instance.delete(recordKey); } /// Runs a closure that guarantees the DHTShortArray @@ -169,23 +177,15 @@ class DHTShortArray { Future _getItemInner(_DHTShortArrayHead head, int pos, {bool forceRefresh = false}) async { - if (pos < 0 || pos >= head.index.length) { - throw IndexError.withLength(pos, head.index.length); + if (pos < 0 || pos >= length) { + throw IndexError.withLength(pos, length); } - final index = head.index[pos]; - final recordNumber = index ~/ head.stride; - final record = head.getLinkedRecord(recordNumber); - if (record == null) { - throw StateError('Record does not exist'); - } - final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); - - final refresh = forceRefresh || head.indexNeedsRefresh(index); + final (record, recordSubkey) = await head.lookupPosition(pos); + final refresh = forceRefresh || head.positionNeedsRefresh(pos); final out = record.get(subkey: recordSubkey, forceRefresh: refresh); - - await head.updateIndexSeq(index, false); + await head.updatePositionSeq(pos, false); return out; } @@ -197,7 +197,7 @@ class DHTShortArray { _head.operate((head) async { final out = []; - for (var pos = 0; pos < head.index.length; pos++) { + for (var pos = 0; pos < head.length; pos++) { final elem = await _getItemInner(head, pos, forceRefresh: forceRefresh); if (elem == null) { @@ -248,21 +248,14 @@ class DHTShortArray { final out = await _head .operateWrite((head) async => _tryAddItemInner(head, value)) ?? false; - - // Send update - _watchController?.sink.add(null); - return out; } Future _tryAddItemInner( _DHTShortArrayHead head, Uint8List value) async { - // Allocate empty index - final index = head.emptyIndex(); - - // Add new index - final pos = head.index.length; - head.index.add(index); + // Allocate empty index at the end of the list + final pos = head.length; + head.allocateIndex(pos); // Write item final (_, wasSet) = await _tryWriteItemInner(head, pos, value); @@ -271,7 +264,7 @@ class DHTShortArray { } // Get sequence number written - await head.updateIndexSeq(index, true); + await head.updatePositionSeq(pos, true); return true; } @@ -287,19 +280,13 @@ class DHTShortArray { (head) async => _tryInsertItemInner(head, pos, value)) ?? false; - // Send update - _watchController?.sink.add(null); - return out; } Future _tryInsertItemInner( _DHTShortArrayHead head, int pos, Uint8List value) async { - // Allocate empty index - final index = head.emptyIndex(); - - // Add new index - _head.index.insert(pos, index); + // Allocate empty index at position + head.allocateIndex(pos); // Write item final (_, wasSet) = await _tryWriteItemInner(head, pos, value); @@ -308,7 +295,7 @@ class DHTShortArray { } // Get sequence number written - await head.updateIndexSeq(index, true); + await head.updatePositionSeq(pos, true); return true; } @@ -325,24 +312,13 @@ class DHTShortArray { (head) async => _trySwapItemInner(head, aPos, bPos)) ?? false; - // Send update - _watchController?.sink.add(null); - return out; } Future _trySwapItemInner( _DHTShortArrayHead head, int aPos, int bPos) async { - // No-op case - if (aPos == bPos) { - return true; - } - // Swap indices - final aIdx = _head.index[aPos]; - final bIdx = _head.index[bPos]; - _head.index[aPos] = bIdx; - _head.index[bPos] = aIdx; + head.swapIndex(aPos, bPos); return true; } @@ -358,28 +334,17 @@ class DHTShortArray { final out = _head.operateWrite((head) async => _tryRemoveItemInner(head, pos)); - // Send update - _watchController?.sink.add(null); - return out; } Future _tryRemoveItemInner( _DHTShortArrayHead head, int pos) async { - final index = _head.index.removeAt(pos); - final recordNumber = index ~/ head.stride; - final record = head.getLinkedRecord(recordNumber); - if (record == null) { - throw StateError('Record does not exist'); - } - final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); - + final (record, recordSubkey) = await head.lookupPosition(pos); final result = await record.get(subkey: recordSubkey); if (result == null) { throw StateError('Element does not exist'); } - - head.freeIndex(index); + head.freeIndex(pos); return result; } @@ -405,15 +370,11 @@ class DHTShortArray { final out = await _head.operateWrite((head) async => _tryClearInner(head)) ?? false; - // Send update - _watchController?.sink.add(null); - return out; } Future _tryClearInner(_DHTShortArrayHead head) async { - head.index.clear(); - head.free.clear(); + head.clearIndex(); return true; } @@ -434,23 +395,15 @@ class DHTShortArray { return (null, false); } - // Send update - _watchController?.sink.add(null); - return out; } Future<(Uint8List?, bool)> _tryWriteItemInner( _DHTShortArrayHead head, int pos, Uint8List newValue) async { - if (pos < 0 || pos >= head.index.length) { - throw IndexError.withLength(pos, _head.index.length); + if (pos < 0 || pos >= head.length) { + throw IndexError.withLength(pos, head.length); } - - final index = head.index[pos]; - final recordNumber = index ~/ head.stride; - final record = await head.getOrCreateLinkedRecord(recordNumber); - final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); - + final (record, recordSubkey) = await head.lookupPosition(pos); final oldValue = await record.get(subkey: recordSubkey); final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); if (result != null) { @@ -471,9 +424,6 @@ class DHTShortArray { (_, wasSet) = await _tryWriteItemInner(head, pos, newValue); return wasSet; }, timeout: timeout); - - // Send update - _watchController?.sink.add(null); } /// Change an item at position 'pos' of the DHTShortArray. @@ -497,9 +447,6 @@ class DHTShortArray { (_, wasSet) = await _tryWriteItemInner(head, pos, updatedData); return wasSet; }, timeout: timeout); - - // Send update - _watchController?.sink.add(null); } /// Convenience function: @@ -569,13 +516,13 @@ class DHTShortArray { // rid of the controller and drop our subscriptions unawaited(_listenMutex.protect(() async { // Cancel watches of head record - await _head._cancelWatch(); + await _head.cancelWatch(); _watchController = null; })); }); // Start watching head record - await _head._watch(); + await _head.watch(); } // Return subscription return _watchController!.stream.listen((_) => onChanged()); diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart index 94aac2c..708bb38 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart @@ -1,40 +1,52 @@ part of 'dht_short_array.dart'; -//////////////////////////////////////////////////////////////// -// Internal Operations - class _DHTShortArrayHead { - _DHTShortArrayHead({required this.headRecord}) - : linkedRecords = [], - index = [], - free = [], - seqs = [], - localSeqs = [] { + _DHTShortArrayHead({required DHTRecord headRecord}) + : _headRecord = headRecord, + _linkedRecords = [], + _index = [], + _free = [], + _seqs = [], + _localSeqs = [] { _calculateStride(); } - proto.DHTShortArray toProto() { + void _calculateStride() { + switch (_headRecord.schema) { + case DHTSchemaDFLT(oCnt: final oCnt): + if (oCnt <= 1) { + throw StateError('Invalid DFLT schema in DHTShortArray'); + } + _stride = oCnt - 1; + case DHTSchemaSMPL(oCnt: final oCnt, members: final members): + if (oCnt != 0 || members.length != 1 || members[0].mCnt <= 1) { + throw StateError('Invalid SMPL schema in DHTShortArray'); + } + _stride = members[0].mCnt - 1; + } + assert(_stride <= DHTShortArray.maxElements, 'stride too long'); + } + + proto.DHTShortArray _toProto() { + assert(_headMutex.isLocked, 'should be in mutex here'); + final head = proto.DHTShortArray(); - head.keys.addAll(linkedRecords.map((lr) => lr.key.toProto())); - head.index.addAll(index); - head.seqs.addAll(seqs); + head.keys.addAll(_linkedRecords.map((lr) => lr.key.toProto())); + head.index.addAll(_index); + head.seqs.addAll(_seqs); // Do not serialize free list, it gets recreated // Do not serialize local seqs, they are only locally relevant return head; } - Future close() async { - final futures = >[headRecord.close()]; - for (final lr in linkedRecords) { - futures.add(lr.close()); - } - await Future.wait(futures); - } + TypedKey get recordKey => _headRecord.key; + OwnedDHTRecordPointer get recordPointer => _headRecord.ownedDHTRecordPointer; + int get length => _index.length; - Future delete() async { - final futures = >[headRecord.delete()]; - for (final lr in linkedRecords) { - futures.add(lr.delete()); + Future close() async { + final futures = >[_headRecord.close()]; + for (final lr in _linkedRecords) { + futures.add(lr.close()); } await Future.wait(futures); } @@ -46,55 +58,57 @@ class _DHTShortArrayHead { }); Future operateWrite( - Future Function(_DHTShortArrayHead) closure) async { - final oldLinkedRecords = List.of(linkedRecords); - final oldIndex = List.of(index); - final oldFree = List.of(free); - final oldSeqs = List.of(seqs); - try { - final out = await _headMutex.protect(() async { - final out = await closure(this); - // Write head assuming it has been changed - if (!await _tryWriteHead()) { - // Failed to write head means head got overwritten so write should - // be considered failed - return null; - } - return out; - }); - return out; - } on Exception { - // Exception means state needs to be reverted - linkedRecords = oldLinkedRecords; - index = oldIndex; - free = oldFree; - seqs = oldSeqs; + Future Function(_DHTShortArrayHead) closure) async => + _headMutex.protect(() async { + final oldLinkedRecords = List.of(_linkedRecords); + final oldIndex = List.of(_index); + final oldFree = List.of(_free); + final oldSeqs = List.of(_seqs); + try { + final out = await closure(this); + // Write head assuming it has been changed + if (!await _writeHead()) { + // Failed to write head means head got overwritten so write should + // be considered failed + return null; + } - rethrow; - } - } + onUpdatedHead?.call(); + return out; + } on Exception { + // Exception means state needs to be reverted + _linkedRecords = oldLinkedRecords; + _index = oldIndex; + _free = oldFree; + _seqs = oldSeqs; + + rethrow; + } + }); Future operateWriteEventual( Future Function(_DHTShortArrayHead) closure, {Duration? timeout}) async { - late List oldLinkedRecords; - late List oldIndex; - late List oldFree; - late List oldSeqs; - final timeoutTs = timeout == null ? null : Veilid.instance.now().offset(TimestampDuration.fromDuration(timeout)); - try { - await _headMutex.protect(() async { + + await _headMutex.protect(() async { + late List oldLinkedRecords; + late List oldIndex; + late List oldFree; + late List oldSeqs; + + try { // Iterate until we have a successful element and head write + do { // Save off old values each pass of tryWriteHead because the head // will have changed - oldLinkedRecords = List.of(linkedRecords); - oldIndex = List.of(index); - oldFree = List.of(free); - oldSeqs = List.of(seqs); + oldLinkedRecords = List.of(_linkedRecords); + oldIndex = List.of(_index); + oldFree = List.of(_free); + oldSeqs = List.of(_seqs); // Try to do the element write do { @@ -107,26 +121,30 @@ class _DHTShortArrayHead { } while (!await closure(this)); // Try to do the head write - } while (!await _tryWriteHead()); - }); - } on Exception { - // Exception means state needs to be reverted - linkedRecords = oldLinkedRecords; - index = oldIndex; - free = oldFree; - seqs = oldSeqs; + } while (!await _writeHead()); - rethrow; - } + onUpdatedHead?.call(); + } on Exception { + // Exception means state needs to be reverted + _linkedRecords = oldLinkedRecords; + _index = oldIndex; + _free = oldFree; + _seqs = oldSeqs; + + rethrow; + } + }); } /// Serialize and write out the current head record, possibly updating it /// if a newer copy is available online. Returns true if the write was /// successful - Future _tryWriteHead() async { - final headBuffer = toProto().writeToBuffer(); + Future _writeHead() async { + assert(_headMutex.isLocked, 'should be in mutex here'); - final existingData = await headRecord.tryWriteBytes(headBuffer); + final headBuffer = _toProto().writeToBuffer(); + + final existingData = await _headRecord.tryWriteBytes(headBuffer); if (existingData != null) { // Head write failed, incorporate update await _updateHead(proto.DHTShortArray.fromBuffer(existingData)); @@ -138,6 +156,8 @@ class _DHTShortArrayHead { /// Validate a new head record that has come in from the network Future _updateHead(proto.DHTShortArray head) async { + assert(_headMutex.isLocked, 'should be in mutex here'); + // Get the set of new linked keys and validate it final updatedLinkedKeys = head.keys.map((p) => p.toVeilid()).toList(); final updatedIndex = List.of(head.index); @@ -146,7 +166,7 @@ class _DHTShortArrayHead { // See which records are actually new final oldRecords = Map.fromEntries( - linkedRecords.map((lr) => MapEntry(lr.key, lr))); + _linkedRecords.map((lr) => MapEntry(lr.key, lr))); final newRecords = {}; final sameRecords = {}; final updatedLinkedRecords = []; @@ -173,32 +193,33 @@ class _DHTShortArrayHead { // From this point forward we should not throw an exception or everything // is possibly invalid. Just pass the exception up it happens and the caller // will have to delete this short array and reopen it if it can - await Future.wait(oldRecords.entries + await oldRecords.entries .where((e) => !sameRecords.containsKey(e.key)) - .map((e) => e.value.close())); + .map((e) => e.value.close()) + .wait; // Get the localseqs list from inspect results - final localReports = await [headRecord, ...updatedLinkedRecords].map((r) { - final start = (r.key == headRecord.key) ? 1 : 0; - return r - .inspect(subkeys: [ValueSubkeyRange.make(start, start + stride - 1)]); + final localReports = await [_headRecord, ...updatedLinkedRecords].map((r) { + final start = (r.key == _headRecord.key) ? 1 : 0; + return r.inspect( + subkeys: [ValueSubkeyRange.make(start, start + _stride - 1)]); }).wait; final updatedLocalSeqs = localReports.map((l) => l.localSeqs).expand((e) => e).toList(); // Make the new head cache - linkedRecords = updatedLinkedRecords; - index = updatedIndex; - free = updatedFree; - seqs = updatedSeqs; - localSeqs = updatedLocalSeqs; + _linkedRecords = updatedLinkedRecords; + _index = updatedIndex; + _free = updatedFree; + _seqs = updatedSeqs; + _localSeqs = updatedLocalSeqs; } - /// Pull the latest or updated copy of the head record from the network - Future _refreshInner( + // Pull the latest or updated copy of the head record from the network + Future _loadHead( {bool forceRefresh = true, bool onlyUpdates = false}) async { // Get an updated head record copy if one exists - final head = await headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, + final head = await _headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, subkey: 0, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); if (head == null) { if (onlyUpdates) { @@ -213,82 +234,110 @@ class _DHTShortArrayHead { return true; } - void _calculateStride() { - switch (headRecord.schema) { - case DHTSchemaDFLT(oCnt: final oCnt): - if (oCnt <= 1) { - throw StateError('Invalid DFLT schema in DHTShortArray'); - } - stride = oCnt - 1; - case DHTSchemaSMPL(oCnt: final oCnt, members: final members): - if (oCnt != 0 || members.length != 1 || members[0].mCnt <= 1) { - throw StateError('Invalid SMPL schema in DHTShortArray'); - } - stride = members[0].mCnt - 1; - } - assert(stride <= DHTShortArray.maxElements, 'stride too long'); - } + ///////////////////////////////////////////////////////////////////////////// + // Linked record management - DHTRecord? getLinkedRecord(int recordNumber) { + Future _getOrCreateLinkedRecord(int recordNumber) async { if (recordNumber == 0) { - return headRecord; - } - recordNumber--; - if (recordNumber >= linkedRecords.length) { - return null; - } - return linkedRecords[recordNumber]; - } - - Future getOrCreateLinkedRecord(int recordNumber) async { - if (recordNumber == 0) { - return headRecord; + return _headRecord; } final pool = DHTRecordPool.instance; recordNumber--; - while (recordNumber >= linkedRecords.length) { + while (recordNumber >= _linkedRecords.length) { // Linked records must use SMPL schema so writer can be specified // Use the same writer as the head record - final smplWriter = headRecord.writer!; - final parent = pool.getParentRecordKey(headRecord.key); - final routingContext = headRecord.routingContext; - final crypto = headRecord.crypto; + final smplWriter = _headRecord.writer!; + final parent = _headRecord.key; + final routingContext = _headRecord.routingContext; + final crypto = _headRecord.crypto; final schema = DHTSchema.smpl( oCnt: 0, - members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride)]); - final dhtCreateRecord = await pool.create( + members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]); + final dhtRecord = await pool.create( parent: parent, routingContext: routingContext, schema: schema, crypto: crypto, writer: smplWriter); - // Reopen with SMPL writer - await dhtCreateRecord.close(); - final dhtRecord = await pool.openWrite(dhtCreateRecord.key, smplWriter, - parent: parent, routingContext: routingContext, crypto: crypto); // Add to linked records - linkedRecords.add(dhtRecord); - if (!await _tryWriteHead()) { - await _refreshInner(); - } + _linkedRecords.add(dhtRecord); } - return linkedRecords[recordNumber]; + if (!await _writeHead()) { + throw StateError('failed to add linked record'); + } + return _linkedRecords[recordNumber]; } - int emptyIndex() { - if (free.isNotEmpty) { - return free.removeLast(); + /// Open a linked record for reading or writing, same as the head record + Future _openLinkedRecord(TypedKey recordKey) async { + final writer = _headRecord.writer; + return (writer != null) + ? await DHTRecordPool.instance.openWrite( + recordKey, + writer, + parent: _headRecord.key, + routingContext: _headRecord.routingContext, + ) + : await DHTRecordPool.instance.openRead( + recordKey, + parent: _headRecord.key, + routingContext: _headRecord.routingContext, + ); + } + + Future<(DHTRecord, int)> lookupPosition(int pos) async { + final idx = _index[pos]; + return lookupIndex(idx); + } + + Future<(DHTRecord, int)> lookupIndex(int idx) async { + final recordNumber = idx ~/ _stride; + final record = await _getOrCreateLinkedRecord(recordNumber); + final recordSubkey = (idx % _stride) + ((recordNumber == 0) ? 1 : 0); + return (record, recordSubkey); + } + + ///////////////////////////////////////////////////////////////////////////// + // Index management + + /// Allocate an empty index slot at a specific position + void allocateIndex(int pos) { + // Allocate empty index + final idx = _emptyIndex(); + _index.insert(pos, idx); + } + + int _emptyIndex() { + if (_free.isNotEmpty) { + return _free.removeLast(); } - if (index.length == DHTShortArray.maxElements) { + if (_index.length == DHTShortArray.maxElements) { throw StateError('too many elements'); } - return index.length; + return _index.length; } - void freeIndex(int idx) { - free.add(idx); + void swapIndex(int aPos, int bPos) { + if (aPos == bPos) { + return; + } + final aIdx = _index[aPos]; + final bIdx = _index[bPos]; + _index[aPos] = bIdx; + _index[bPos] = aIdx; + } + + void clearIndex() { + _index.clear(); + _free.clear(); + } + + /// Release an index at a particular position + void freeIndex(int pos) { + final idx = _index.removeAt(pos); + _free.add(idx); // xxx: free list optimization here? } @@ -299,7 +348,8 @@ class _DHTShortArrayHead { // Ensure nothing is duplicated in the linked keys set final newKeys = linkedKeys.toSet(); assert( - newKeys.length <= (DHTShortArray.maxElements + (stride - 1)) ~/ stride, + newKeys.length <= + (DHTShortArray.maxElements + (_stride - 1)) ~/ _stride, 'too many keys'); assert(newKeys.length == linkedKeys.length, 'duplicated linked keys'); final newIndex = index.toSet(); @@ -307,7 +357,7 @@ class _DHTShortArrayHead { assert(newIndex.length == index.length, 'duplicated index locations'); // Ensure all the index keys fit into the existing records - final indexCapacity = (linkedKeys.length + 1) * stride; + final indexCapacity = (linkedKeys.length + 1) * _stride; int? maxIndex; for (final idx in newIndex) { assert(idx >= 0 || idx < indexCapacity, 'index out of range'); @@ -328,117 +378,97 @@ class _DHTShortArrayHead { return free; } - /// Open a linked record for reading or writing, same as the head record - Future _openLinkedRecord(TypedKey recordKey) async { - final writer = headRecord.writer; - return (writer != null) - ? await DHTRecordPool.instance.openWrite( - recordKey, - writer, - parent: headRecord.key, - routingContext: headRecord.routingContext, - ) - : await DHTRecordPool.instance.openRead( - recordKey, - parent: headRecord.key, - routingContext: headRecord.routingContext, - ); - } - /// Check if we know that the network has a copy of an index that is newer /// than our local copy from looking at the seqs list in the head - bool indexNeedsRefresh(int index) { + bool positionNeedsRefresh(int pos) { + final idx = _index[pos]; + // If our local sequence number is unknown or hasnt been written yet // then a normal DHT operation is going to pull from the network anyway - if (localSeqs.length < index || localSeqs[index] == 0xFFFFFFFF) { + if (_localSeqs.length < idx || _localSeqs[idx] == 0xFFFFFFFF) { return false; } // If the remote sequence number record is unknown or hasnt been written // at this index yet, then we also do not refresh at this time as it // is the first time the index is being written to - if (seqs.length < index || seqs[index] == 0xFFFFFFFF) { + if (_seqs.length < idx || _seqs[idx] == 0xFFFFFFFF) { return false; } - return localSeqs[index] < seqs[index]; + return _localSeqs[idx] < _seqs[idx]; } /// Update the sequence number for a particular index in /// our local sequence number list. /// If a write is happening, update the network copy as well. - Future updateIndexSeq(int index, bool write) async { - final recordNumber = index ~/ stride; - final record = await getOrCreateLinkedRecord(recordNumber); - final recordSubkey = (index % stride) + ((recordNumber == 0) ? 1 : 0); + Future updatePositionSeq(int pos, bool write) async { + final idx = _index[pos]; + final (record, recordSubkey) = await lookupIndex(idx); final report = await record.inspect(subkeys: [ValueSubkeyRange.single(recordSubkey)]); - while (localSeqs.length <= index) { - localSeqs.add(0xFFFFFFFF); + while (_localSeqs.length <= idx) { + _localSeqs.add(0xFFFFFFFF); } - localSeqs[index] = report.localSeqs[0]; + _localSeqs[idx] = report.localSeqs[0]; if (write) { - while (seqs.length <= index) { - seqs.add(0xFFFFFFFF); + while (_seqs.length <= idx) { + _seqs.add(0xFFFFFFFF); } - seqs[index] = report.localSeqs[0]; + _seqs[idx] = report.localSeqs[0]; } } + ///////////////////////////////////////////////////////////////////////////// + // Watch For Updates + // Watch head for changes - Future _watch() async { + Future watch() async { // This will update any existing watches if necessary try { - await headRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); + await _headRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); // Update changes to the head record // Don't watch for local changes because this class already handles // notifying listeners and knows when it makes local changes _subscription ??= - await headRecord.listen(localChanges: false, _onUpdateHead); + await _headRecord.listen(localChanges: false, _onHeadValueChanged); } on Exception { // If anything fails, try to cancel the watches - await _cancelWatch(); + await cancelWatch(); rethrow; } } // Stop watching for changes to head and linked records - Future _cancelWatch() async { - await headRecord.cancelWatch(); + Future cancelWatch() async { + await _headRecord.cancelWatch(); await _subscription?.cancel(); _subscription = null; } - // Called when a head or linked record changes - Future _onUpdateHead( + Future _onHeadValueChanged( DHTRecord record, Uint8List? data, List subkeys) async { // If head record subkey zero changes, then the layout // of the dhtshortarray has changed - var updateHead = false; - if (record == headRecord && subkeys.containsSubkey(0)) { - updateHead = true; + if (data == null) { + throw StateError('head value changed without data'); + } + if (record.key != _headRecord.key || + subkeys.length != 1 || + subkeys[0] != ValueSubkeyRange.single(0)) { + throw StateError('watch returning wrong subkey range'); } - // If we have any other subkeys to update, do them first - final unord = List>.empty(growable: true); - for (final skr in subkeys) { - for (var subkey = skr.low; subkey <= skr.high; subkey++) { - // Skip head subkey - if (updateHead && subkey == 0) { - continue; - } - // Get the subkey, which caches the result in the local record store - unord.add(record.get(subkey: subkey, forceRefresh: true)); - } - } - await unord.wait; + // Decode updated head + final headData = proto.DHTShortArray.fromBuffer(data); // Then update the head record - if (updateHead) { - await _refreshInner(forceRefresh: false); - } + await _headMutex.protect(() async { + await _updateHead(headData); + onUpdatedHead?.call(); + }); } //////////////////////////////////////////////////////////////////////////// @@ -447,25 +477,26 @@ class _DHTShortArrayHead { final Mutex _headMutex = Mutex(); // Subscription to head record internal changes StreamSubscription? _subscription; + // Notify closure for external head changes + void Function()? onUpdatedHead; // Head DHT record - final DHTRecord headRecord; + final DHTRecord _headRecord; // How many elements per linked record - late final int stride; - -// List of additional records after the head record used for element data - List linkedRecords; + late final int _stride; + // List of additional records after the head record used for element data + List _linkedRecords; // Ordering of the subkey indices. // Elements are subkey numbers. Represents the element order. - List index; + List _index; // List of free subkeys for elements that have been removed. // Used to optimize allocations. - List free; + List _free; // The sequence numbers of each subkey. // Index is by subkey number not by element index. // (n-1 for head record and then the next n for linked records) - List seqs; + List _seqs; // The local sequence numbers for each subkey. - List localSeqs; + List _localSeqs; } diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 63361c7..e9ad6b6 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -93,7 +93,7 @@ extension IdentityMasterExtension on IdentityMaster { /// Deletes a master identity and the identity record under it Future delete() async { final pool = DHTRecordPool.instance; - await (await pool.openRead(masterRecordKey)).delete(); + await pool.delete(masterRecordKey); } Future get identityCrypto => diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index a6ec0d8..352be05 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -203,10 +203,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" equatable: dependency: "direct main" description: @@ -219,10 +219,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "6df5b5bb29f52644c4c653ef0ae7d26c8463f8d6551b0ac94561103ff6c5ca17" + sha256: "49154d1da38a34519b907b0e94a06705a59b7127728131dc4a54fe62fd95a83e" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "10.2.1" ffi: dependency: transitive description: @@ -728,10 +728,10 @@ packages: dependency: transitive description: name: vm_service - sha256: e7d5ecd604e499358c5fe35ee828c0298a320d54455e791e9dcf73486bc8d9f0 + sha256: a75f83f14ad81d5fe4b3319710b90dec37da0e22612326b696c9e1b8f34bbf48 url: "https://pub.dev" source: hosted - version: "14.1.0" + version: "14.2.0" watcher: dependency: transitive description: @@ -744,10 +744,10 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" web_socket_channel: dependency: transitive description: @@ -768,10 +768,10 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" xdg_directories: dependency: transitive description: @@ -790,4 +790,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.10.6" + flutter: ">=3.19.1" diff --git a/pubspec.lock b/pubspec.lock index c1c3af8..9a49fa3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -68,10 +68,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: cde9c8c155c1a1cafc5286807e16124e97f0cff739a47ec17aa9d26c3c37abcf + sha256: "7d235d64a81543a7e200a91b1149bef7d32241290fa483bae25b31be41449a7c" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.13" badges: dependency: "direct main" description: @@ -219,18 +219,18 @@ packages: dependency: transitive description: name: camera_android - sha256: "351429510121d179b9aac5a2e8cb525c3cd6c39f4d709c5f72dfb21726e52371" + sha256: "15a6543878a41c141807ffab496f66b7fef6da0f23372f5513fc6349e60f437e" url: "https://pub.dev" source: hosted - version: "0.10.8+16" + version: "0.10.8+17" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "7d0763dfcbf060f56aa254a68c103210280bee9e97bbe4fdef23e257a4f70ab9" + sha256: "8b113e43ee4434c9244c03c905432a0d5956cedaded3cd7381abaab89ce50297" url: "https://pub.dev" source: hosted - version: "0.9.14" + version: "0.9.14+1" camera_platform_interface: dependency: transitive description: @@ -379,10 +379,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" diffutil_dart: dependency: transitive description: @@ -403,10 +403,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "6df5b5bb29f52644c4c653ef0ae7d26c8463f8d6551b0ac94561103ff6c5ca17" + sha256: "49154d1da38a34519b907b0e94a06705a59b7127728131dc4a54fe62fd95a83e" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "10.2.1" ffi: dependency: transitive description: @@ -517,10 +517,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.0" flutter_parsed_text: dependency: transitive description: @@ -549,10 +549,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c" + sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" flutter_spinkit: dependency: "direct main" description: @@ -634,10 +634,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" + sha256: "7ecb2f391edbca5473db591b48555a8912dde60edd0fb3013bd6743033b2d3f8" url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.2.1" graphs: dependency: transitive description: @@ -818,10 +818,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "619ed5fd43ca9007a151f00c3dc43feedeaf235fe5647735d0237c38849d49dc" + sha256: "827765afbd4792ff3fd105ad593821ac0f6d8a7d352689013b07ee85be336312" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" motion_toast: dependency: "direct main" description: @@ -1009,10 +1009,10 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1041,10 +1041,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: b42d097e346a546fcf9ff2f5a0e39ea1315449608cfd9b2bc6513988b488a371 + sha256: "8e9732d5b6e4e28d50647dc6d7713bf421148cadf28c768a10e9810bf6f3d87a" url: "https://pub.dev" source: hosted - version: "0.7.5" + version: "0.7.6" qr_flutter: dependency: "direct main" description: @@ -1057,10 +1057,10 @@ packages: dependency: "direct main" description: name: quickalert - sha256: "0c21c9be68b9ae76082e1ad56db9f51202a38e617e08376f05375238277cfb5a" + sha256: b5d62b1e20b08cc0ff5f40b6da519bdc7a5de6082f13d90572cf4e72eea56c5e url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" quiver: dependency: transitive description: @@ -1113,10 +1113,10 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: "5cd3cd87e0cbd4e6685f6798a9bb4bcc170df20fb92beb662b978f5fccded634" + sha256: "5535ea3efa4599cf23ce52870a9580b52ece5d691aa90655ebec76d5081c9592" url: "https://pub.dev" source: hosted - version: "2.10.2" + version: "2.11.1" share_plus: dependency: "direct main" description: @@ -1129,10 +1129,10 @@ packages: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" shared_preferences: dependency: "direct main" description: @@ -1278,10 +1278,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" stack_trace: dependency: "direct main" description: @@ -1532,10 +1532,10 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" web_socket_channel: dependency: transitive description: @@ -1548,10 +1548,10 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" window_manager: dependency: "direct main" description: