concurrency work in prep for speeding things up

refactor splash screen to process initialization in a better way
more async tools for async cubit constructors
greatly improved StateMapFollower class
This commit is contained in:
Christien Rioux 2024-04-03 21:55:49 -04:00
parent 8da1dc7d32
commit 9bb20f4dd2
47 changed files with 886 additions and 579 deletions

View File

@ -3,24 +3,13 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../init.dart';
import '../repository/account_repository/account_repository.dart';
class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
ActiveLocalAccountCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository,
super(null) {
super(accountRepository.getActiveLocalAccount()) {
// Subscribe to streams
_initAccountRepositorySubscription();
// Initialize when we can
Future.delayed(Duration.zero, () async {
await eventualInitialized.future;
emit(_accountRepository.getActiveLocalAccount());
});
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) {
case AccountRepositoryChange.activeLocalAccount:

View File

@ -3,25 +3,14 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import '../../init.dart';
import '../models/models.dart';
import '../repository/account_repository/account_repository.dart';
class LocalAccountsCubit extends Cubit<IList<LocalAccount>> {
LocalAccountsCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository,
super(IList<LocalAccount>()) {
super(accountRepository.getLocalAccounts()) {
// Subscribe to streams
_initAccountRepositorySubscription();
// Initialize when we can
Future.delayed(Duration.zero, () async {
await eventualInitialized.future;
emit(_accountRepository.getLocalAccounts());
});
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) {
case AccountRepositoryChange.localAccounts:

View File

@ -3,25 +3,14 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import '../../init.dart';
import '../models/models.dart';
import '../repository/account_repository/account_repository.dart';
class UserLoginsCubit extends Cubit<IList<UserLogin>> {
UserLoginsCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository,
super(IList<UserLogin>()) {
super(accountRepository.getUserLogins()) {
// Subscribe to streams
_initAccountRepositorySubscription();
// Initialize when we can
Future.delayed(Duration.zero, () async {
await eventualInitialized.future;
emit(_accountRepository.getUserLogins());
});
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) {
case AccountRepositoryChange.userLogins:

View File

@ -202,18 +202,24 @@ class AccountRepository {
createAccountCallback: (parent) async {
// Make empty contact list
log.debug('Creating contacts list');
final contactList = await (await DHTShortArray.create(parent: parent))
final contactList = await (await DHTShortArray.create(
debugName: 'AccountRepository::_newLocalAccount::Contacts',
parent: parent))
.scope((r) async => r.recordPointer);
// Make empty contact invitation record list
log.debug('Creating contact invitation records list');
final contactInvitationRecords =
await (await DHTShortArray.create(parent: parent))
.scope((r) async => r.recordPointer);
final contactInvitationRecords = await (await DHTShortArray.create(
debugName:
'AccountRepository::_newLocalAccount::ContactInvitations',
parent: parent))
.scope((r) async => r.recordPointer);
// Make empty chat record list
log.debug('Creating chat records list');
final chatRecords = await (await DHTShortArray.create(parent: parent))
final chatRecords = await (await DHTShortArray.create(
debugName: 'AccountRepository::_newLocalAccount::Chats',
parent: parent))
.scope((r) async => r.recordPointer);
// Make account object
@ -391,6 +397,7 @@ class AccountRepository {
final pool = DHTRecordPool.instance;
final record = await pool.openOwned(
userLogin.accountRecordInfo.accountRecord,
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
parent: localAccount.identityMaster.identityRecordKey);
return record;

View File

@ -5,8 +5,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:provider/provider.dart';
import 'account_manager/account_manager.dart';
import 'init.dart';
import 'layout/splash.dart';
import 'router/router.dart';
import 'settings/settings.dart';
import 'tick.dart';
@ -23,57 +26,66 @@ class VeilidChatApp extends StatelessWidget {
final ThemeData initialThemeData;
@override
Widget build(BuildContext context) {
final localizationDelegate = LocalizedApp.of(context).delegate;
return ThemeProvider(
initTheme: initialThemeData,
builder: (_, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state,
child: MultiBlocProvider(
providers: [
BlocProvider<ConnectionStateCubit>(
create: (context) =>
ConnectionStateCubit(ProcessorRepository.instance)),
BlocProvider<RouterCubit>(
create: (context) =>
RouterCubit(AccountRepository.instance),
),
BlocProvider<LocalAccountsCubit>(
create: (context) =>
LocalAccountsCubit(AccountRepository.instance),
),
BlocProvider<UserLoginsCubit>(
create: (context) =>
UserLoginsCubit(AccountRepository.instance),
),
BlocProvider<ActiveLocalAccountCubit>(
create: (context) =>
ActiveLocalAccountCubit(AccountRepository.instance),
),
BlocProvider<PreferencesCubit>(
create: (context) =>
PreferencesCubit(PreferencesRepository.instance),
)
],
child: BackgroundTicker(
builder: (context) => MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: context.watch<RouterCubit>().router(),
title: translate('app.title'),
theme: theme,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
localizationDelegate
Widget build(BuildContext context) => FutureProvider<VeilidChatGlobalInit?>(
initialData: null,
create: (context) async => VeilidChatGlobalInit.initialize(),
builder: (context, child) {
final globalInit = context.watch<VeilidChatGlobalInit?>();
if (globalInit == null) {
// Splash screen until we're done with init
return const Splash();
}
// Once init is done, we proceed with the app
final localizationDelegate = LocalizedApp.of(context).delegate;
return ThemeProvider(
initTheme: initialThemeData,
builder: (_, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state,
child: MultiBlocProvider(
providers: [
BlocProvider<ConnectionStateCubit>(
create: (context) => ConnectionStateCubit(
ProcessorRepository.instance)),
BlocProvider<RouterCubit>(
create: (context) =>
RouterCubit(AccountRepository.instance),
),
BlocProvider<LocalAccountsCubit>(
create: (context) =>
LocalAccountsCubit(AccountRepository.instance),
),
BlocProvider<UserLoginsCubit>(
create: (context) =>
UserLoginsCubit(AccountRepository.instance),
),
BlocProvider<ActiveLocalAccountCubit>(
create: (context) => ActiveLocalAccountCubit(
AccountRepository.instance),
),
BlocProvider<PreferencesCubit>(
create: (context) =>
PreferencesCubit(PreferencesRepository.instance),
)
],
supportedLocales: localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale,
),
)),
));
}
child: BackgroundTicker(
builder: (context) => MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: context.watch<RouterCubit>().router(),
title: translate('app.title'),
theme: theme,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
localizationDelegate
],
supportedLocales:
localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale,
),
)),
));
});
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {

View File

@ -38,11 +38,13 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
_messagesUpdateQueue = StreamController(),
super(const AsyncValue.loading()) {
// Async Init
Future.delayed(Duration.zero, _init);
_initWait.add(_init);
}
@override
Future<void> close() async {
await _initWait();
await _messagesUpdateQueue.close();
await _localSubscription?.cancel();
await _remoteSubscription?.cancel();
@ -89,7 +91,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
_localMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openWrite(
_localMessagesRecordKey, writer,
parent: _localConversationRecordKey, crypto: _messagesCrypto),
debugName:
'SingleContactMessagesCubit::_initLocalMessages::LocalMessages',
parent: _localConversationRecordKey,
crypto: _messagesCrypto),
decodeElement: proto.Message.fromBuffer);
_localSubscription =
_localMessagesCubit!.stream.listen(_updateLocalMessagesState);
@ -100,7 +105,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
Future<void> _initRemoteMessages() async {
_remoteMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey,
parent: _remoteConversationRecordKey, crypto: _messagesCrypto),
debugName: 'SingleContactMessagesCubit::_initRemoteMessages::'
'RemoteMessages',
parent: _remoteConversationRecordKey,
crypto: _messagesCrypto),
decodeElement: proto.Message.fromBuffer);
_remoteSubscription =
_remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState);
@ -114,6 +122,9 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
_reconciledChatMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openOwned(_reconciledChatRecord,
debugName:
'SingleContactMessagesCubit::_initReconciledChatMessages::'
'ReconciledChat',
parent: accountRecordKey),
decodeElement: proto.Message.fromBuffer);
_reconciledChatSubscription =
@ -237,6 +248,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Force refresh of messages
Future<void> refresh() async {
await _initWait();
final lcc = _localMessagesCubit;
final rcc = _remoteMessagesCubit;
@ -249,10 +262,13 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
}
Future<void> addMessage({required proto.Message message}) async {
await _initWait();
await _localMessagesCubit!
.operateWrite((writer) => writer.tryAddItem(message.writeToBuffer()));
}
final WaitSet _initWait = WaitSet();
final ActiveAccountInfo _activeAccountInfo;
final TypedKey _remoteIdentityPublicKey;
final TypedKey _localConversationRecordKey;

View File

@ -1,13 +1,13 @@
import 'package:async_tools/async_tools.dart';
import 'package:bloc_tools/bloc_tools.dart';
import 'package:equatable/equatable.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import 'cubits.dart';
@immutable
class ActiveConversationState extends Equatable {
@ -39,9 +39,7 @@ typedef ActiveConversationsBlocMapState
// archived chats or contacts that are not actively in a chat.
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit>
with
StateFollower<BlocBusyState<AsyncValue<IList<proto.Chat>>>, TypedKey,
proto.Chat> {
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
ActiveConversationsBlocMapCubit(
{required ActiveAccountInfo activeAccountInfo,
required ContactListCubit contactListCubit})
@ -77,18 +75,6 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
/// StateFollower /////////////////////////
@override
IMap<TypedKey, proto.Chat> getStateMap(
BlocBusyState<AsyncValue<IList<proto.Chat>>> state) {
final stateValue = state.state.data?.value;
if (stateValue == null) {
return IMap();
}
return IMap.fromIterable(stateValue,
keyMapper: (e) => e.remoteConversationRecordKey.toVeilid(),
valueMapper: (e) => e);
}
@override
Future<void> removeFromState(TypedKey key) => remove(key);

View File

@ -18,7 +18,7 @@ import 'chat_list_cubit.dart';
class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<IList<proto.Message>>, SingleContactMessagesCubit>
with
StateFollower<ActiveConversationsBlocMapState, TypedKey,
StateMapFollower<ActiveConversationsBlocMapState, TypedKey,
AsyncValue<ActiveConversationState>> {
ActiveSingleContactChatBlocMapCubit(
{required ActiveAccountInfo activeAccountInfo,
@ -49,11 +49,6 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
/// StateFollower /////////////////////////
@override
IMap<TypedKey, AsyncValue<ActiveConversationState>> getStateMap(
ActiveConversationsBlocMapState state) =>
state;
@override
Future<void> removeFromState(TypedKey key) => remove(key);

View File

@ -1,5 +1,8 @@
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:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
@ -11,8 +14,10 @@ import '../../tools/tools.dart';
//////////////////////////////////////////////////
// Mutable state for per-account chat list
typedef ChatListCubitState = BlocBusyState<AsyncValue<IList<proto.Chat>>>;
class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
with StateMapFollowable<ChatListCubitState, TypedKey, proto.Chat> {
ChatListCubit({
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
@ -30,7 +35,7 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
final chatListRecordKey = account.chatList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey,
parent: accountRecordKey);
debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey);
return dhtRecord;
}
@ -61,9 +66,11 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
.userLogin.accountRecordInfo.accountRecord.recordKey;
// Make a record that can store the reconciled version of the chat
final reconciledChatRecord =
await (await DHTShortArray.create(parent: accountRecordKey))
.scope((r) async => r.recordPointer);
final reconciledChatRecord = await (await DHTShortArray.create(
debugName:
'ChatListCubit::getOrCreateChatSingleContact::ReconciledChat',
parent: accountRecordKey))
.scope((r) async => r.recordPointer);
// Create conversation type Chat
final chat = proto.Chat()
@ -86,26 +93,30 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
// Remove Chat from account's list
// if this fails, don't keep retrying, user can try again later
final (deletedItem, success) = await operateWrite((writer) async {
if (activeChatCubit.state == remoteConversationRecordKey) {
activeChatCubit.setActiveChat(null);
}
for (var i = 0; i < writer.length; i++) {
final cbuf = await writer.getItem(i);
if (cbuf == null) {
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.remoteConversationRecordKey == remoteConversationKey) {
// Found the right chat
if (await writer.tryRemoveItem(i) != null) {
return c;
}
return null;
}
}
return null;
});
final (deletedItem, success) =
// Ensure followers get their changes before we return
await syncFollowers(() => operateWrite((writer) async {
if (activeChatCubit.state == remoteConversationRecordKey) {
activeChatCubit.setActiveChat(null);
}
for (var i = 0; i < writer.length; i++) {
final cbuf = await writer.getItem(i);
if (cbuf == null) {
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.remoteConversationRecordKey == remoteConversationKey) {
// Found the right chat
if (await writer.tryRemoveItem(i) != null) {
return c;
}
return null;
}
}
return null;
}));
// Since followers are synced, we can safetly remove the reconciled
// chat record now
if (success && deletedItem != null) {
try {
await DHTRecordPool.instance
@ -116,6 +127,18 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
}
}
/// StateMapFollowable /////////////////////////
@override
IMap<TypedKey, proto.Chat> getStateMap(ChatListCubitState state) {
final stateValue = state.state.data?.value;
if (stateValue == null) {
return IMap();
}
return IMap.fromIterable(stateValue,
keyMapper: (e) => e.remoteConversationRecordKey.toVeilid(),
valueMapper: (e) => e);
}
final ActiveChatCubit activeChatCubit;
final ActiveAccountInfo _activeAccountInfo;
}

View File

@ -1,5 +1,8 @@
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:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:veilid_support/veilid_support.dart';
@ -23,11 +26,16 @@ typedef GetEncryptionKeyCallback = Future<SecretKey?> Function(
//////////////////////////////////////////////////
typedef ContactInvitiationListState
= BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>>;
//////////////////////////////////////////////////
// Mutable state for per-account contact invitations
class ContactInvitationListCubit
extends DHTShortArrayCubit<proto.ContactInvitationRecord> {
extends DHTShortArrayCubit<proto.ContactInvitationRecord>
with
StateMapFollowable<ContactInvitiationListState, TypedKey,
proto.ContactInvitationRecord> {
ContactInvitationListCubit({
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
@ -47,6 +55,7 @@ class ContactInvitationListCubit
final dhtRecord = await DHTShortArray.openOwned(
contactInvitationListRecordPointer,
debugName: 'ContactInvitationListCubit::_open::ContactInvitationList',
parent: accountRecordKey);
return dhtRecord;
@ -78,6 +87,8 @@ class ContactInvitationListCubit
// identity key
late final Uint8List signedContactInvitationBytes;
await (await pool.create(
debugName: 'ContactInvitationListCubit::createInvitation::'
'LocalConversation',
parent: _activeAccountInfo.accountRecordKey,
schema: DHTSchema.smpl(oCnt: 0, members: [
DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1)
@ -105,6 +116,8 @@ class ContactInvitationListCubit
// Subkey 0 is the ContactRequest from the initiator
// Subkey 1 will contain the invitation response accept/reject eventually
await (await pool.create(
debugName: 'ContactInvitationListCubit::createInvitation::'
'ContactRequestInbox',
parent: _activeAccountInfo.accountRecordKey,
schema: DHTSchema.smpl(oCnt: 1, members: [
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
@ -180,6 +193,8 @@ class ContactInvitationListCubit
// Delete the contact request inbox
final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid();
await (await pool.openOwned(contactRequestInbox,
debugName: 'ContactInvitationListCubit::deleteInvitation::'
'ContactRequestInbox',
parent: accountRecordKey))
.scope((contactRequestInbox) async {
// Wipe out old invitation so it shows up as invalid
@ -229,6 +244,8 @@ class ContactInvitationListCubit
-1;
await (await pool.openRead(contactRequestInboxKey,
debugName: 'ContactInvitationListCubit::validateInvitation::'
'ContactRequestInbox',
parent: _activeAccountInfo.accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
//
@ -282,6 +299,19 @@ class ContactInvitationListCubit
return out;
}
/// StateMapFollowable /////////////////////////
@override
IMap<TypedKey, proto.ContactInvitationRecord> getStateMap(
ContactInvitiationListState state) {
final stateValue = state.state.data?.value;
if (stateValue == null) {
return IMap();
}
return IMap.fromIterable(stateValue,
keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(),
valueMapper: (e) => e);
}
//
final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account;

View File

@ -33,6 +33,8 @@ class ContactRequestInboxCubit
final writer = TypedKeyPair(
kind: recordKey.kind, key: writerKey, secret: writerSecret);
return pool.openRead(recordKey,
debugName: 'ContactRequestInboxCubit::_open::'
'ContactRequestInbox',
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
parent: accountRecordKey,
defaultSubkey: 1);

View File

@ -16,7 +16,7 @@ typedef WaitingInvitationsBlocMapState
class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<InvitationStatus>, WaitingInvitationCubit>
with
StateFollower<
StateMapFollower<
BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>>,
TypedKey,
proto.ContactInvitationRecord> {
@ -37,17 +37,6 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
contactInvitationRecord: contactInvitationRecord)));
/// StateFollower /////////////////////////
@override
IMap<TypedKey, proto.ContactInvitationRecord> getStateMap(
BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>> state) {
final stateValue = state.state.data?.value;
if (stateValue == null) {
return IMap();
}
return IMap.fromIterable(stateValue,
keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(),
valueMapper: (e) => e);
}
@override
Future<void> removeFromState(TypedKey key) => remove(key);

View File

@ -38,6 +38,8 @@ class ValidContactInvitation {
final accountRecordKey = _activeAccountInfo.accountRecordKey;
return (await pool.openWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::accept::'
'ContactRequestInbox',
parent: accountRecordKey))
// ignore: prefer_expression_function_bodies
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
@ -103,6 +105,8 @@ class ValidContactInvitation {
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
return (await pool.openWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::reject::'
'ContactRequestInbox',
parent: accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
final cs =

View File

@ -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 'conversation_cubit.dart';
//////////////////////////////////////////////////
// Mutable state for per-account contacts
@ -14,7 +15,8 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
ContactListCubit({
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
}) : super(
}) : _activeAccountInfo = activeAccountInfo,
super(
open: () => _open(activeAccountInfo, account),
decodeElement: proto.Contact.fromBuffer);
@ -26,6 +28,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
final contactListRecordKey = account.contactList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey,
debugName: 'ContactListCubit::_open::ContactList',
parent: accountRecordKey);
return dhtRecord;
@ -60,9 +63,10 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
}
Future<void> deleteContact({required proto.Contact contact}) async {
final pool = DHTRecordPool.instance;
final localConversationKey = contact.localConversationRecordKey.toVeilid();
final remoteConversationKey =
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid();
final remoteConversationRecordKey =
contact.remoteConversationRecordKey.toVeilid();
// Remove Contact from account's list
@ -85,17 +89,21 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (success && deletedItem != null) {
try {
await pool.delete(localConversationKey);
// Make a conversation cubit to manipulate the conversation
final conversationCubit = ConversationCubit(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: remoteIdentityPublicKey,
localConversationRecordKey: localConversationRecordKey,
remoteConversationRecordKey: remoteConversationRecordKey,
);
// Delete the local and remote conversation records
await conversationCubit.delete();
} on Exception catch (e) {
log.debug('error removing local conversation record key: $e', e);
}
try {
if (localConversationKey != remoteConversationKey) {
await pool.delete(remoteConversationKey);
}
} on Exception catch (e) {
log.debug('error removing remote conversation record key: $e', e);
log.debug('error deleting conversation records: $e', e);
}
}
}
final ActiveAccountInfo _activeAccountInfo;
}

View File

@ -13,6 +13,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
@immutable
class ConversationState extends Equatable {
@ -36,11 +37,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
_localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey,
_incrementalState = const ConversationState(
localConversation: null, remoteConversation: null),
super(const AsyncValue.loading()) {
if (_localConversationRecordKey != null) {
Future.delayed(Duration.zero, () async {
_initWait.add(() async {
await _setLocalConversation(() async {
final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey;
@ -51,14 +50,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final writer = _activeAccountInfo.conversationWriter;
final record = await pool.openWrite(
_localConversationRecordKey!, writer,
parent: accountRecordKey, crypto: crypto);
debugName: 'ConversationCubit::LocalConversation',
parent: accountRecordKey,
crypto: crypto);
return record;
});
});
}
if (_remoteConversationRecordKey != null) {
Future.delayed(Duration.zero, () async {
_initWait.add(() async {
await _setRemoteConversation(() async {
final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey;
@ -67,7 +68,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto();
final record = await pool.openRead(_remoteConversationRecordKey,
parent: accountRecordKey, crypto: crypto);
debugName: 'ConversationCubit::RemoteConversation',
parent: accountRecordKey,
crypto: crypto);
return record;
});
});
@ -76,6 +79,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
@override
Future<void> close() async {
await _initWait();
await _localSubscription?.cancel();
await _remoteSubscription?.cancel();
await _localConversationCubit?.close();
@ -84,7 +88,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
await super.close();
}
void updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when(
data: (conv) {
_incrementalState = ConversationState(
@ -106,7 +110,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
emit(newState);
}
void updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
void _updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when(
data: (conv) {
_incrementalState = ConversationState(
@ -135,7 +139,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
_localConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer);
_localSubscription =
_localConversationCubit!.stream.listen(updateLocalConversationState);
_localConversationCubit!.stream.listen(_updateLocalConversationState);
}
// Open remote converation key
@ -145,7 +149,57 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
_remoteConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer);
_remoteSubscription =
_remoteConversationCubit!.stream.listen(updateRemoteConversationState);
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
}
Future<bool> delete() async {
final pool = DHTRecordPool.instance;
await _initWait();
final localConversationCubit = _localConversationCubit;
final remoteConversationCubit = _remoteConversationCubit;
final deleteSet = DelayedWaitSet();
if (localConversationCubit != null) {
final data = localConversationCubit.state.data;
if (data == null) {
log.warning('could not delete local conversation');
return false;
}
deleteSet.add(() async {
_localConversationCubit = null;
await localConversationCubit.close();
final conversation = data.value;
final messagesKey = conversation.messages.toVeilid();
await pool.delete(messagesKey);
await pool.delete(_localConversationRecordKey!);
_localConversationRecordKey = null;
});
}
if (remoteConversationCubit != null) {
final data = remoteConversationCubit.state.data;
if (data == null) {
log.warning('could not delete remote conversation');
return false;
}
deleteSet.add(() async {
_remoteConversationCubit = null;
await remoteConversationCubit.close();
final conversation = data.value;
final messagesKey = conversation.messages.toVeilid();
await pool.delete(messagesKey);
await pool.delete(_remoteConversationRecordKey!);
});
}
// Commit the delete futures
await deleteSet();
return true;
}
// Initialize a local conversation
@ -174,23 +228,25 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openWrite(
existingConversationRecordKey, writer,
parent: accountRecordKey, crypto: crypto);
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey,
crypto: crypto);
} else {
final localConversationRecordCreate = await pool.create(
localConversationRecord = await pool.create(
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey,
crypto: crypto,
writer: writer,
schema: DHTSchema.smpl(
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
await localConversationRecordCreate.close();
localConversationRecord = await pool.openWrite(
localConversationRecordCreate.key, writer,
parent: accountRecordKey, crypto: crypto);
}
final out = localConversationRecord
// ignore: prefer_expression_function_bodies
.deleteScope((localConversation) async {
// Make messages log
return initLocalMessages(
return _initLocalMessages(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: _remoteIdentityPublicKey,
localConversationKey: localConversation.key,
@ -211,7 +267,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final out = await callback(localConversation);
// Upon success emit the local conversation record to the state
updateLocalConversationState(AsyncValue.data(conversation));
_updateLocalConversationState(AsyncValue.data(conversation));
return out;
});
@ -225,7 +281,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
}
// Initialize local messages
Future<T> initLocalMessages<T>({
Future<T> _initLocalMessages<T>({
required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationKey,
@ -236,12 +292,17 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final writer = activeAccountInfo.conversationWriter;
return (await DHTShortArray.create(
parent: localConversationKey, crypto: crypto, smplWriter: writer))
debugName: 'ConversationCubit::initLocalMessages::LocalMessages',
parent: localConversationKey,
crypto: crypto,
smplWriter: writer))
.deleteScope((messages) async => await callback(messages));
}
// Force refresh of conversation keys
Future<void> refresh() async {
await _initWait();
final lcc = _localConversationCubit;
final rcc = _remoteConversationCubit;
@ -260,7 +321,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
.tryWriteProtobuf(proto.Conversation.fromBuffer, conversation);
if (update != null) {
updateLocalConversationState(AsyncValue.data(conversation));
_updateLocalConversationState(AsyncValue.data(conversation));
}
return update;
@ -286,7 +347,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
ConversationState _incrementalState;
ConversationState _incrementalState = const ConversationState(
localConversation: null, remoteConversation: null);
//
DHTRecordCrypto? _conversationCrypto;
final WaitSet _initWait = WaitSet();
}

View File

@ -8,34 +8,38 @@ import 'app.dart';
import 'tools/tools.dart';
import 'veilid_processor/veilid_processor.dart';
final Completer<void> eventualInitialized = Completer<void>();
class VeilidChatGlobalInit {
VeilidChatGlobalInit._();
// Initialize Veilid
Future<void> initializeVeilid() async {
// Init Veilid
Veilid.instance.initializeVeilidCore(
getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
// Initialize Veilid
Future<void> _initializeVeilid() async {
// Init Veilid
Veilid.instance.initializeVeilidCore(
getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
// Veilid logging
initVeilidLog(kDebugMode);
// Veilid logging
initVeilidLog(kDebugMode);
// Startup Veilid
await ProcessorRepository.instance.startup();
// Startup Veilid
await ProcessorRepository.instance.startup();
// DHT Record Pool
await DHTRecordPool.init();
}
// DHT Record Pool
await DHTRecordPool.init();
}
// Initialize repositories
Future<void> initializeRepositories() async {
await AccountRepository.instance.init();
}
Future<void> _initializeRepositories() async {
await AccountRepository.instance.init();
}
Future<void> initializeVeilidChat() async {
log.info('Initializing Veilid');
await initializeVeilid();
log.info('Initializing Repositories');
await initializeRepositories();
static Future<VeilidChatGlobalInit> initialize() async {
final veilidChatGlobalInit = VeilidChatGlobalInit._();
eventualInitialized.complete();
log.info('Initializing Veilid');
await veilidChatGlobalInit._initializeVeilid();
log.info('Initializing Repositories');
await veilidChatGlobalInit._initializeRepositories();
return veilidChatGlobalInit;
}
}

View File

@ -137,17 +137,17 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
create: (context) => ActiveConversationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo,
contactListCubit: context.read<ContactListCubit>())
..followBloc(context.read<ChatListCubit>())),
..follow(context.read<ChatListCubit>())),
BlocProvider(
create: (context) => ActiveSingleContactChatBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo,
contactListCubit: context.read<ContactListCubit>(),
chatListCubit: context.read<ChatListCubit>())
..followBloc(context.read<ActiveConversationsBlocMapCubit>())),
..follow(context.read<ActiveConversationsBlocMapCubit>())),
BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo, account: account)
..followBloc(context.read<ContactInvitationListCubit>()))
..follow(context.read<ContactInvitationListCubit>()))
],
child: MultiBlocListener(listeners: [
BlocListener<WaitingInvitationsBlocMapCubit,

View File

@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:radix_colors/radix_colors.dart';
import '../tools/tools.dart';
class IndexPage extends StatefulWidget {
const IndexPage({super.key});
@override
State<IndexPage> createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.hidden, OrientationCapability.normal);
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final monoTextStyle = textTheme.labelSmall!
.copyWith(fontFamily: 'Source Code Pro', fontSize: 11);
final emojiTextStyle = textTheme.labelSmall!
.copyWith(fontFamily: 'Noto Color Emoji', fontSize: 11);
return Scaffold(
body: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
RadixColors.dark.plum.step4,
RadixColors.dark.plum.step2,
])),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Hack to preload fonts
Offstage(child: Text('🧱', style: emojiTextStyle)),
// Hack to preload fonts
Offstage(child: Text('A', style: monoTextStyle)),
// Splash Screen
Expanded(
flex: 2,
child: SvgPicture.asset(
'assets/images/icon.svg',
)),
Expanded(
child: SvgPicture.asset(
'assets/images/title.svg',
))
]))),
));
}
}

View File

@ -1,4 +1,4 @@
export 'default_app_bar.dart';
export 'home/home.dart';
export 'home/home_account_ready/main_pager/main_pager.dart';
export 'index.dart';
export 'splash.dart';

53
lib/layout/splash.dart Normal file
View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:radix_colors/radix_colors.dart';
import '../tools/tools.dart';
class Splash extends StatefulWidget {
const Splash({super.key});
@override
State<Splash> createState() => _SplashState();
}
class _SplashState extends State<Splash> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.hidden, OrientationCapability.normal);
});
}
@override
Widget build(BuildContext context) => DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
RadixColors.dark.plum.step4,
RadixColors.dark.plum.step2,
])),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Splash Screen
Expanded(
flex: 2,
child: SvgPicture.asset(
'assets/images/icon.svg',
)),
Expanded(
child: SvgPicture.asset(
'assets/images/title.svg',
))
]))),
);
}

View File

@ -8,7 +8,6 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'app.dart';
import 'init.dart';
import 'settings/preferences_repository.dart';
import 'theme/theme.dart';
import 'tools/tools.dart';
@ -45,9 +44,6 @@ void main() async {
fallbackLocale: 'en_US', supportedLocales: ['en_US']);
await initializeDateFormatting();
// Start up Veilid and Veilid processor in the background
unawaited(initializeVeilidChat());
// Run the app
// Hot reloads will only restart this part, not Veilid
runApp(LocalizedApp(localizationDelegate,

View File

@ -9,7 +9,6 @@ import 'package:go_router/go_router.dart';
import 'package:stream_transform/stream_transform.dart';
import '../../../account_manager/account_manager.dart';
import '../../init.dart';
import '../../layout/layout.dart';
import '../../settings/settings.dart';
import '../../tools/tools.dart';
@ -24,19 +23,10 @@ final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
class RouterCubit extends Cubit<RouterState> {
RouterCubit(AccountRepository accountRepository)
: super(const RouterState(
isInitialized: false,
hasAnyAccount: false,
: super(RouterState(
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty,
hasActiveChat: false,
)) {
// Watch for changes that the router will care about
Future.delayed(Duration.zero, () async {
await eventualInitialized.future;
emit(state.copyWith(
isInitialized: true,
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty));
});
// Subscribe to repository streams
_accountRepositorySubscription = accountRepository.stream.listen((event) {
switch (event) {
@ -63,10 +53,6 @@ class RouterCubit extends Cubit<RouterState> {
/// Our application routes
List<RouteBase> get routes => [
GoRoute(
path: '/',
builder: (context, state) => const IndexPage(),
),
ShellRoute(
navigatorKey: _homeNavKey,
builder: (context, state, child) => HomeShell(
@ -75,11 +61,11 @@ class RouterCubit extends Cubit<RouterState> {
HomeAccountReadyShell(context: context, child: child))),
routes: [
GoRoute(
path: '/home',
path: '/',
builder: (context, state) => const HomeAccountReadyMain(),
),
GoRoute(
path: '/home/chat',
path: '/chat',
builder: (context, state) => const HomeAccountReadyChat(),
),
],
@ -103,17 +89,9 @@ class RouterCubit extends Cubit<RouterState> {
// No matter where we are, if there's not
switch (goRouterState.matchedLocation) {
case '/':
// Wait for initialization to complete
if (!eventualInitialized.isCompleted) {
return null;
}
return state.hasAnyAccount ? '/home' : '/new_account';
case '/new_account':
return state.hasAnyAccount ? '/home' : null;
case '/home':
return state.hasAnyAccount ? '/' : null;
case '/':
if (!state.hasAnyAccount) {
return '/new_account';
}
@ -123,11 +101,11 @@ class RouterCubit extends Cubit<RouterState> {
tabletLandscape: false,
desktop: false)) {
if (state.hasActiveChat) {
return '/home/chat';
return '/chat';
}
}
return null;
case '/home/chat':
case '/chat':
if (!state.hasAnyAccount) {
return '/new_account';
}
@ -137,10 +115,10 @@ class RouterCubit extends Cubit<RouterState> {
tabletLandscape: false,
desktop: false)) {
if (!state.hasActiveChat) {
return '/home';
return '/';
}
} else {
return '/home';
return '/';
}
return null;
case '/settings':

View File

@ -20,7 +20,6 @@ RouterState _$RouterStateFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$RouterState {
bool get isInitialized => throw _privateConstructorUsedError;
bool get hasAnyAccount => throw _privateConstructorUsedError;
bool get hasActiveChat => throw _privateConstructorUsedError;
@ -36,7 +35,7 @@ abstract class $RouterStateCopyWith<$Res> {
RouterState value, $Res Function(RouterState) then) =
_$RouterStateCopyWithImpl<$Res, RouterState>;
@useResult
$Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat});
$Res call({bool hasAnyAccount, bool hasActiveChat});
}
/// @nodoc
@ -52,15 +51,10 @@ class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isInitialized = null,
Object? hasAnyAccount = null,
Object? hasActiveChat = null,
}) {
return _then(_value.copyWith(
isInitialized: null == isInitialized
? _value.isInitialized
: isInitialized // ignore: cast_nullable_to_non_nullable
as bool,
hasAnyAccount: null == hasAnyAccount
? _value.hasAnyAccount
: hasAnyAccount // ignore: cast_nullable_to_non_nullable
@ -81,7 +75,7 @@ abstract class _$$RouterStateImplCopyWith<$Res>
__$$RouterStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat});
$Res call({bool hasAnyAccount, bool hasActiveChat});
}
/// @nodoc
@ -95,15 +89,10 @@ class __$$RouterStateImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isInitialized = null,
Object? hasAnyAccount = null,
Object? hasActiveChat = null,
}) {
return _then(_$RouterStateImpl(
isInitialized: null == isInitialized
? _value.isInitialized
: isInitialized // ignore: cast_nullable_to_non_nullable
as bool,
hasAnyAccount: null == hasAnyAccount
? _value.hasAnyAccount
: hasAnyAccount // ignore: cast_nullable_to_non_nullable
@ -120,15 +109,11 @@ class __$$RouterStateImplCopyWithImpl<$Res>
@JsonSerializable()
class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
const _$RouterStateImpl(
{required this.isInitialized,
required this.hasAnyAccount,
required this.hasActiveChat});
{required this.hasAnyAccount, required this.hasActiveChat});
factory _$RouterStateImpl.fromJson(Map<String, dynamic> json) =>
_$$RouterStateImplFromJson(json);
@override
final bool isInitialized;
@override
final bool hasAnyAccount;
@override
@ -136,7 +121,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'RouterState(isInitialized: $isInitialized, hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)';
return 'RouterState(hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)';
}
@override
@ -144,7 +129,6 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'RouterState'))
..add(DiagnosticsProperty('isInitialized', isInitialized))
..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount))
..add(DiagnosticsProperty('hasActiveChat', hasActiveChat));
}
@ -154,8 +138,6 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RouterStateImpl &&
(identical(other.isInitialized, isInitialized) ||
other.isInitialized == isInitialized) &&
(identical(other.hasAnyAccount, hasAnyAccount) ||
other.hasAnyAccount == hasAnyAccount) &&
(identical(other.hasActiveChat, hasActiveChat) ||
@ -164,8 +146,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, isInitialized, hasAnyAccount, hasActiveChat);
int get hashCode => Object.hash(runtimeType, hasAnyAccount, hasActiveChat);
@JsonKey(ignore: true)
@override
@ -183,15 +164,12 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
abstract class _RouterState implements RouterState {
const factory _RouterState(
{required final bool isInitialized,
required final bool hasAnyAccount,
{required final bool hasAnyAccount,
required final bool hasActiveChat}) = _$RouterStateImpl;
factory _RouterState.fromJson(Map<String, dynamic> json) =
_$RouterStateImpl.fromJson;
@override
bool get isInitialized;
@override
bool get hasAnyAccount;
@override

View File

@ -8,14 +8,12 @@ part of 'router_cubit.dart';
_$RouterStateImpl _$$RouterStateImplFromJson(Map<String, dynamic> json) =>
_$RouterStateImpl(
isInitialized: json['is_initialized'] as bool,
hasAnyAccount: json['has_any_account'] as bool,
hasActiveChat: json['has_active_chat'] as bool,
);
Map<String, dynamic> _$$RouterStateImplToJson(_$RouterStateImpl instance) =>
<String, dynamic>{
'is_initialized': instance.isInitialized,
'has_any_account': instance.hasAnyAccount,
'has_active_chat': instance.hasActiveChat,
};

View File

@ -3,8 +3,7 @@ part of 'router_cubit.dart';
@freezed
class RouterState with _$RouterState {
const factory RouterState(
{required bool isInitialized,
required bool hasAnyAccount,
{required bool hasAnyAccount,
required bool hasActiveChat}) = _RouterState;
factory RouterState.fromJson(dynamic json) =>

View File

@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:veilid_support/veilid_support.dart';
import 'init.dart';
import 'veilid_processor/veilid_processor.dart';
class BackgroundTicker extends StatefulWidget {
@ -53,10 +52,6 @@ class BackgroundTickerState extends State<BackgroundTicker> {
}
Future<void> _onTick() async {
// Don't tick until we are initialized
if (!eventualInitialized.isCompleted) {
return;
}
if (!ProcessorRepository
.instance.processorConnectionState.isPublicInternetReady) {
return;

View File

@ -3,7 +3,9 @@ library;
export 'src/async_tag_lock.dart';
export 'src/async_value.dart';
export 'src/delayed_wait_set.dart';
export 'src/serial_future.dart';
export 'src/single_future.dart';
export 'src/single_state_processor.dart';
export 'src/single_stateless_processor.dart';
export 'src/wait_set.dart';

View File

@ -0,0 +1,18 @@
class DelayedWaitSet {
DelayedWaitSet();
void add(Future<void> Function() closure) {
_closures.add(closure);
}
Future<void> call() async {
final futures = _closures.map((c) => c()).toList();
_closures = [];
if (futures.isEmpty) {
return;
}
await futures.wait;
}
List<Future<void> Function()> _closures = [];
}

View File

@ -4,14 +4,14 @@ import 'async_tag_lock.dart';
AsyncTagLock<Object> _keys = AsyncTagLock();
// Process a single future at a time per tag
//
// The closure function is called to produce the future that is to be executed.
// If a future with a particular tag is still executing, the onBusy callback
// is called.
// When a tagged singleFuture finishes executing, the onDone callback is called.
// If an unhandled exception happens in the closure future, the onError callback
// is called.
/// Process a single future at a time per tag
///
/// The closure function is called to produce the future that is to be executed.
/// If a future with a particular tag is still executing, the onBusy callback
/// is called.
/// When a tagged singleFuture finishes executing, the onDone callback is called.
/// If an unhandled exception happens in the closure future, the onError callback
/// is called.
void singleFuture<T>(Object tag, Future<T> Function() closure,
{void Function()? onBusy,
void Function(T)? onDone,
@ -40,3 +40,6 @@ void singleFuture<T>(Object tag, Future<T> Function() closure,
}
}());
}
Future<void> singleFuturePause(Object tag) async => _keys.lockTag(tag);
void singleFutureResume(Object tag) => _keys.unlockTag(tag);

View File

@ -22,15 +22,19 @@ class SingleStateProcessor<State> {
singleFuture(this, () async {
var newState = newInputState;
var newClosure = closure;
var done = false;
while (!done) {
await closure(newState);
await newClosure(newState);
// See if there's another state change to process
final next = _nextState;
final nextState = _nextState;
final nextClosure = _nextClosure;
_nextState = null;
if (next != null) {
newState = next;
_nextClosure = null;
if (nextState != null) {
newState = nextState;
newClosure = nextClosure!;
} else {
done = true;
}
@ -38,8 +42,26 @@ class SingleStateProcessor<State> {
}, onBusy: () {
// Keep this state until we process again
_nextState = newInputState;
_nextClosure = closure;
});
}
Future<void> pause() => singleFuturePause(this);
Future<void> resume() async {
// Process any next state before resuming the singlefuture
try {
final nextState = _nextState;
final nextClosure = _nextClosure;
_nextState = null;
_nextClosure = null;
if (nextState != null) {
await nextClosure!(nextState);
}
} finally {
singleFutureResume(this);
}
}
State? _nextState;
Future<void> Function(State)? _nextClosure;
}

View File

@ -0,0 +1,18 @@
class WaitSet {
WaitSet();
void add(Future<void> Function() closure) {
_futures.add(Future.delayed(Duration.zero, closure));
}
Future<void> call() async {
final futures = _futures;
_futures = [];
if (futures.isEmpty) {
return;
}
await futures.wait;
}
List<Future<void>> _futures = [];
}

View File

@ -6,6 +6,6 @@ export 'src/bloc_busy_wrapper.dart';
export 'src/bloc_map_cubit.dart';
export 'src/bloc_tools_extension.dart';
export 'src/future_cubit.dart';
export 'src/state_follower.dart';
export 'src/state_map_follower.dart';
export 'src/stream_wrapper_cubit.dart';
export 'src/transformer_cubit.dart';

View File

@ -14,6 +14,7 @@ class AsyncTransformerCubit<T, S> extends Cubit<AsyncValue<T>> {
_asyncTransform(input.state);
_subscription = input.stream.listen(_asyncTransform);
}
void _asyncTransform(AsyncValue<S> newInputState) {
_singleStateProcessor.updateState(newInputState, (newState) async {
// Emit the transformed state

View File

@ -3,6 +3,9 @@ import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart';
import 'state_map_follower.dart';
typedef BlocMapState<K, S> = IMap<K, S>;
@ -18,14 +21,15 @@ class _ItemEntry<S, B> {
// cubits.
//
// K = Key type for the bloc map, used to look up some mapped cubit
// S = State type for the value, keys will look up values of this type
// V = State type for the value, keys will look up values of this type
// B = Bloc/cubit type for the value, output states of type S
abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
extends Cubit<BlocMapState<K, S>> {
abstract class BlocMapCubit<K, V, B extends BlocBase<V>>
extends Cubit<BlocMapState<K, V>>
with StateMapFollowable<BlocMapState<K, V>, K, V> {
BlocMapCubit()
: _entries = {},
_tagLock = AsyncTagLock(),
super(IMap<K, S>());
super(IMap<K, V>());
@override
Future<void> close() async {
@ -34,6 +38,13 @@ abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
await super.close();
}
@protected
@override
// ignore: unnecessary_overrides
void emit(BlocMapState<K, V> state) {
super.emit(state);
}
Future<void> add(MapEntry<K, B> Function() create) {
// Create new element
final newElement = create();
@ -56,7 +67,7 @@ abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
});
}
Future<void> addState(K key, S value) =>
Future<void> addState(K key, V value) =>
_tagLock.protect(key, closure: () async {
// Remove entry with the same key if it exists
await _internalRemove(key);
@ -107,6 +118,10 @@ abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
return closure(entry.bloc);
});
final Map<K, _ItemEntry<S, B>> _entries;
/// StateMapFollowable /////////////////////////
@override
IMap<K, V> getStateMap(BlocMapState<K, V> s) => s;
final Map<K, _ItemEntry<V, B>> _entries;
final AsyncTagLock<K> _tagLock;
}

View File

@ -1,63 +0,0 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
// Mixin that automatically keeps two blocs/cubits in sync with each other
// Useful for having a BlocMapCubit 'follow' the state of another input cubit.
// As the state of the input cubit changes, the BlocMapCubit can add/remove
// mapped Cubits that automatically process the input state reactively.
//
// S = Input state type
// K = Key derived from elements of input state
// V = Value derived from elements of input state
abstract mixin class StateFollower<S extends Object, K, V> {
void follow({
required S initialInputState,
required Stream<S> stream,
}) {
//
_lastInputStateMap = IMap();
_updateFollow(initialInputState);
_subscription = stream.listen(_updateFollow);
}
void followBloc<B extends BlocBase<S>>(B bloc) =>
follow(initialInputState: bloc.state, stream: bloc.stream);
Future<void> close() async {
await _subscription.cancel();
}
IMap<K, V> getStateMap(S state);
Future<void> removeFromState(K key);
Future<void> updateState(K key, V value);
void _updateFollow(S newInputState) {
_singleStateProcessor.updateState(getStateMap(newInputState),
(newStateMap) async {
for (final k in _lastInputStateMap.keys) {
if (!newStateMap.containsKey(k)) {
// deleted
await removeFromState(k);
}
}
for (final newEntry in newStateMap.entries) {
final v = _lastInputStateMap.get(newEntry.key);
if (v == null || v != newEntry.value) {
// added or changed
await updateState(newEntry.key, newEntry.value);
}
}
// Keep this state map for the next time
_lastInputStateMap = newStateMap;
});
}
late IMap<K, V> _lastInputStateMap;
late final StreamSubscription<S> _subscription;
final SingleStateProcessor<IMap<K, V>> _singleStateProcessor =
SingleStateProcessor();
}

View File

@ -0,0 +1,125 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart';
/// Mixin that automatically keeps two blocs/cubits in sync with each other
/// Useful for having a BlocMapCubit 'follow' the state of another input cubit.
/// As the state of the input cubit changes, the BlocMapCubit can add/remove
/// mapped Cubits that automatically process the input state reactively.
///
/// S = Input state type
/// K = Key derived from elements of input state
/// V = Value derived from elements of input state
mixin StateMapFollower<S extends Object, K, V> on Closable {
void follow(StateMapFollowable<S, K, V> followable) {
assert(_following == null, 'can only follow one followable at a time');
_following = followable;
_lastInputStateMap = IMap();
_subscription = followable.registerFollower(this);
}
Future<void> unfollow() async {
await _subscription?.cancel();
_subscription = null;
_following?.unregisterFollower(this);
_following = null;
}
@override
@mustCallSuper
Future<void> close() async {
await unfollow();
await super.close();
}
Future<void> removeFromState(K key);
Future<void> updateState(K key, V value);
void _updateFollow(IMap<K, V> newInputState) {
final following = _following;
if (following == null) {
return;
}
_singleStateProcessor.updateState(newInputState, (newStateMap) async {
for (final k in _lastInputStateMap.keys) {
if (!newStateMap.containsKey(k)) {
// deleted
await removeFromState(k);
}
}
for (final newEntry in newStateMap.entries) {
final v = _lastInputStateMap.get(newEntry.key);
if (v == null || v != newEntry.value) {
// added or changed
await updateState(newEntry.key, newEntry.value);
}
}
// Keep this state map for the next time
_lastInputStateMap = newStateMap;
});
}
StateMapFollowable<S, K, V>? _following;
late IMap<K, V> _lastInputStateMap;
late StreamSubscription<IMap<K, V>>? _subscription;
final SingleStateProcessor<IMap<K, V>> _singleStateProcessor =
SingleStateProcessor();
}
/// Interface that allows a StateMapFollower to follow some other class's
/// state changes
abstract mixin class StateMapFollowable<S extends Object, K, V>
implements StateStreamable<S> {
IMap<K, V> getStateMap(S state);
StreamSubscription<IMap<K, V>> registerFollower(
StateMapFollower<S, K, V> follower) {
final stateMapTransformer = StreamTransformer<S, IMap<K, V>>.fromHandlers(
handleData: (d, s) => s.add(getStateMap(d)));
if (_followers.isEmpty) {
// start transforming stream
_transformedStream = stream.transform(stateMapTransformer);
}
_followers.add(follower);
follower._updateFollow(getStateMap(state));
return _transformedStream!.listen((s) => follower._updateFollow(s));
}
void unregisterFollower(StateMapFollower<S, K, V> follower) {
_followers.remove(follower);
if (_followers.isEmpty) {
// stop transforming stream
_transformedStream = null;
}
}
Future<T> syncFollowers<T>(Future<T> Function() closure) async {
// pause all followers
await _followers.map((f) => f._singleStateProcessor.pause()).wait;
// run closure
final out = await closure();
// resume all followers and wait for current state map to be updated
final resumeState = getStateMap(state);
await _followers.map((f) async {
// Ensure the latest state has been updated
try {
f._updateFollow(resumeState);
} finally {
// Resume processing of the follower
await f._singleStateProcessor.resume();
}
}).wait;
return out;
}
Stream<IMap<K, V>>? _transformedStream;
final List<StateMapFollower<S, K, V>> _followers = [];
}

View File

@ -1,3 +1,4 @@
export 'default_dht_record_cubit.dart';
export 'dht_record_crypto.dart';
export 'dht_record_cubit.dart';
export 'dht_record_pool.dart';

View File

@ -0,0 +1,66 @@
import 'dart:typed_data';
import '../../../veilid_support.dart';
/// Cubit that watches the default subkey value of a dhtrecord
class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
DefaultDHTRecordCubit({
required super.open,
required T Function(List<int> data) decodeState,
}) : super(
initialStateFunction: _makeInitialStateFunction(decodeState),
stateFunction: _makeStateFunction(decodeState),
watchFunction: _makeWatchFunction());
// DefaultDHTRecordCubit.value({
// required super.record,
// required T Function(List<int> data) decodeState,
// }) : super.value(
// initialStateFunction: _makeInitialStateFunction(decodeState),
// stateFunction: _makeStateFunction(decodeState),
// watchFunction: _makeWatchFunction());
static InitialStateFunction<T> _makeInitialStateFunction<T>(
T Function(List<int> data) decodeState) =>
(record) async {
final initialData = await record.get();
if (initialData == null) {
return null;
}
return decodeState(initialData);
};
static StateFunction<T> _makeStateFunction<T>(
T Function(List<int> data) decodeState) =>
(record, subkeys, updatedata) async {
final defaultSubkey = record.subkeyOrDefault(-1);
if (subkeys.containsSubkey(defaultSubkey)) {
final Uint8List data;
final firstSubkey = subkeys.firstOrNull!.low;
if (firstSubkey != defaultSubkey || updatedata == null) {
final maybeData = await record.get(forceRefresh: true);
if (maybeData == null) {
return null;
}
data = maybeData;
} else {
data = updatedata;
}
final newState = decodeState(data);
return newState;
}
return null;
};
static WatchFunction _makeWatchFunction() => (record) async {
final defaultSubkey = record.subkeyOrDefault(-1);
await record.watch(subkeys: [ValueSubkeyRange.single(defaultSubkey)]);
};
Future<void> refreshDefault() async {
await initWait();
final defaultSubkey = record.subkeyOrDefault(-1);
await refresh([ValueSubkeyRange(low: defaultSubkey, high: defaultSubkey)]);
}
}

View File

@ -16,12 +16,13 @@ class DHTRecordWatchChange extends Equatable {
/////////////////////////////////////////////////
class DHTRecord {
DHTRecord(
DHTRecord._(
{required VeilidRoutingContext routingContext,
required SharedDHTRecordData sharedDHTRecordData,
required int defaultSubkey,
required KeyPair? writer,
required DHTRecordCrypto crypto})
required DHTRecordCrypto crypto,
required this.debugName})
: _crypto = crypto,
_routingContext = routingContext,
_defaultSubkey = defaultSubkey,
@ -34,6 +35,7 @@ class DHTRecord {
final int _defaultSubkey;
final KeyPair? _writer;
final DHTRecordCrypto _crypto;
final String debugName;
bool _open;
@internal

View File

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:async_tools/async_tools.dart';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import '../../../veilid_support.dart';
@ -20,7 +21,7 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
}) : _wantsCloseRecord = false,
_stateFunction = stateFunction,
super(const AsyncValue.loading()) {
Future.delayed(Duration.zero, () async {
initWait.add(() async {
// Do record open/create
_record = await open();
_wantsCloseRecord = true;
@ -73,6 +74,7 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
@override
Future<void> close() async {
await initWait();
await _record.cancelWatch();
await _subscription?.cancel();
_subscription = null;
@ -84,6 +86,8 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
}
Future<void> refresh(List<ValueSubkeyRange> subkeys) async {
await initWait();
var updateSubkeys = [...subkeys];
for (final skr in subkeys) {
@ -107,69 +111,11 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
DHTRecord get record => _record;
@protected
final WaitSet initWait = WaitSet();
StreamSubscription<DHTRecordWatchChange>? _subscription;
late DHTRecord _record;
bool _wantsCloseRecord;
final StateFunction<T> _stateFunction;
}
// Cubit that watches the default subkey value of a dhtrecord
class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
DefaultDHTRecordCubit({
required super.open,
required T Function(List<int> data) decodeState,
}) : super(
initialStateFunction: _makeInitialStateFunction(decodeState),
stateFunction: _makeStateFunction(decodeState),
watchFunction: _makeWatchFunction());
// DefaultDHTRecordCubit.value({
// required super.record,
// required T Function(List<int> data) decodeState,
// }) : super.value(
// initialStateFunction: _makeInitialStateFunction(decodeState),
// stateFunction: _makeStateFunction(decodeState),
// watchFunction: _makeWatchFunction());
static InitialStateFunction<T> _makeInitialStateFunction<T>(
T Function(List<int> data) decodeState) =>
(record) async {
final initialData = await record.get();
if (initialData == null) {
return null;
}
return decodeState(initialData);
};
static StateFunction<T> _makeStateFunction<T>(
T Function(List<int> data) decodeState) =>
(record, subkeys, updatedata) async {
final defaultSubkey = record.subkeyOrDefault(-1);
if (subkeys.containsSubkey(defaultSubkey)) {
final Uint8List data;
final firstSubkey = subkeys.firstOrNull!.low;
if (firstSubkey != defaultSubkey || updatedata == null) {
final maybeData = await record.get(forceRefresh: true);
if (maybeData == null) {
return null;
}
data = maybeData;
} else {
data = updatedata;
}
final newState = decodeState(data);
return newState;
}
return null;
};
static WatchFunction _makeWatchFunction() => (record) async {
final defaultSubkey = record.subkeyOrDefault(-1);
await record.watch(subkeys: [ValueSubkeyRange.single(defaultSubkey)]);
};
Future<void> refreshDefault() async {
final defaultSubkey = _record.subkeyOrDefault(-1);
await refresh([ValueSubkeyRange(low: defaultSubkey, high: defaultSubkey)]);
}
}

View File

@ -18,14 +18,16 @@ const int watchBackoffMultiplier = 2;
const int watchBackoffMax = 30;
/// Record pool that managed DHTRecords and allows for tagged deletion
/// String versions of keys due to IMap<> json unsupported in key
@freezed
class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations {
const factory DHTRecordPoolAllocations({
required IMap<String, ISet<TypedKey>>
childrenByParent, // String key due to IMap<> json unsupported in key
required IMap<String, TypedKey>
parentByChild, // String key due to IMap<> json unsupported in key
required ISet<TypedKey> rootRecords,
@Default(IMapConst<String, ISet<TypedKey>>({}))
IMap<String, ISet<TypedKey>> childrenByParent,
@Default(IMapConst<String, TypedKey>({}))
IMap<String, TypedKey> parentByChild,
@Default(ISetConst<TypedKey>({})) ISet<TypedKey> rootRecords,
@Default(IMapConst<String, String>({})) IMap<String, String> debugNames,
}) = _DHTRecordPoolAllocations;
factory DHTRecordPoolAllocations.fromJson(dynamic json) =>
@ -92,10 +94,7 @@ class OpenedRecordInfo {
class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext)
: _state = DHTRecordPoolAllocations(
childrenByParent: IMap(),
parentByChild: IMap(),
rootRecords: ISet()),
: _state = const DHTRecordPoolAllocations(),
_mutex = Mutex(),
_opened = <TypedKey, OpenedRecordInfo>{},
_routingContext = routingContext,
@ -129,8 +128,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
@override
DHTRecordPoolAllocations valueFromJson(Object? obj) => obj != null
? DHTRecordPoolAllocations.fromJson(obj)
: DHTRecordPoolAllocations(
childrenByParent: IMap(), parentByChild: IMap(), rootRecords: ISet());
: const DHTRecordPoolAllocations();
@override
Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson();
@ -148,7 +146,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
Veilid get veilid => _veilid;
Future<OpenedRecordInfo> _recordCreateInner(
{required VeilidRoutingContext dhtctx,
{required String debugName,
required VeilidRoutingContext dhtctx,
required DHTSchema schema,
KeyPair? writer,
TypedKey? parent}) async {
@ -169,13 +168,18 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
_opened[recordDescriptor.key] = openedRecordInfo;
// Register the dependency
await _addDependencyInner(parent, recordDescriptor.key);
await _addDependencyInner(
parent,
recordDescriptor.key,
debugName: debugName,
);
return openedRecordInfo;
}
Future<OpenedRecordInfo> _recordOpenInner(
{required VeilidRoutingContext dhtctx,
{required String debugName,
required VeilidRoutingContext dhtctx,
required TypedKey recordKey,
KeyPair? writer,
TypedKey? parent}) async {
@ -198,7 +202,11 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
_opened[recordDescriptor.key] = newOpenedRecordInfo;
// Register the dependency
await _addDependencyInner(parent, recordKey);
await _addDependencyInner(
parent,
recordKey,
debugName: debugName,
);
return newOpenedRecordInfo;
}
@ -218,7 +226,11 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
}
// Register the dependency
await _addDependencyInner(parent, recordKey);
await _addDependencyInner(
parent,
recordKey,
debugName: debugName,
);
return openedRecordInfo;
}
@ -259,6 +271,18 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
return allDeps.reversedView;
}
void _debugPrintChildren(TypedKey recordKey, {List<TypedKey>? allDeps}) {
allDeps ??= _collectChildrenInner(recordKey);
// ignore: avoid_print
print('Parent: $recordKey (${_state.debugNames[recordKey.toString()]})');
for (final dep in allDeps) {
if (dep != recordKey) {
// ignore: avoid_print
print(' Child: $dep (${_state.debugNames[dep.toString()]})');
}
}
}
Future<void> _deleteInner(TypedKey recordKey) async {
// Remove this child from parents
await _removeDependenciesInner([recordKey]);
@ -269,7 +293,10 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
await _mutex.protect(() async {
final allDeps = _collectChildrenInner(recordKey);
assert(allDeps.singleOrNull == recordKey, 'must delete children first');
if (allDeps.singleOrNull != recordKey) {
_debugPrintChildren(recordKey, allDeps: allDeps);
assert(false, 'must delete children first');
}
final ori = _opened[recordKey];
if (ori != null) {
@ -301,15 +328,17 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
}
}
Future<void> _addDependencyInner(TypedKey? parent, TypedKey child) async {
Future<void> _addDependencyInner(TypedKey? parent, TypedKey child,
{required String debugName}) async {
assert(_mutex.isLocked, 'should be locked here');
if (parent == null) {
if (_state.rootRecords.contains(child)) {
// Dependency already added
return;
}
_state = await store(
_state.copyWith(rootRecords: _state.rootRecords.add(child)));
_state = await store(_state.copyWith(
rootRecords: _state.rootRecords.add(child),
debugNames: _state.debugNames.add(child.toJson(), debugName)));
} else {
final childrenOfParent =
_state.childrenByParent[parent.toJson()] ?? ISet<TypedKey>();
@ -320,7 +349,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
_state = await store(_state.copyWith(
childrenByParent: _state.childrenByParent
.add(parent.toJson(), childrenOfParent.add(child)),
parentByChild: _state.parentByChild.add(child.toJson(), parent)));
parentByChild: _state.parentByChild.add(child.toJson(), parent),
debugNames: _state.debugNames.add(child.toJson(), debugName)));
}
}
@ -331,7 +361,9 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
for (final child in childList) {
if (_state.rootRecords.contains(child)) {
state = state.copyWith(rootRecords: state.rootRecords.remove(child));
state = state.copyWith(
rootRecords: state.rootRecords.remove(child),
debugNames: state.debugNames.remove(child.toJson()));
} else {
final parent = state.parentByChild[child.toJson()];
if (parent == null) {
@ -341,12 +373,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
if (children.isEmpty) {
state = state.copyWith(
childrenByParent: state.childrenByParent.remove(parent.toJson()),
parentByChild: state.parentByChild.remove(child.toJson()));
parentByChild: state.parentByChild.remove(child.toJson()),
debugNames: state.debugNames.remove(child.toJson()));
} else {
state = state.copyWith(
childrenByParent:
state.childrenByParent.add(parent.toJson(), children),
parentByChild: state.parentByChild.remove(child.toJson()));
parentByChild: state.parentByChild.remove(child.toJson()),
debugNames: state.debugNames.remove(child.toJson()));
}
}
}
@ -360,6 +394,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
/// Create a root DHTRecord that has no dependent records
Future<DHTRecord> create({
required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTSchema schema = const DHTSchema.dflt(oCnt: 1),
@ -371,9 +406,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
final dhtctx = routingContext ?? _routingContext;
final openedRecordInfo = await _recordCreateInner(
dhtctx: dhtctx, schema: schema, writer: writer, parent: parent);
debugName: debugName,
dhtctx: dhtctx,
schema: schema,
writer: writer,
parent: parent);
final rec = DHTRecord(
final rec = DHTRecord._(
debugName: debugName,
routingContext: dhtctx,
defaultSubkey: defaultSubkey,
sharedDHTRecordData: openedRecordInfo.shared,
@ -391,7 +431,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
/// Open a DHTRecord readonly
Future<DHTRecord> openRead(TypedKey recordKey,
{VeilidRoutingContext? routingContext,
{required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
int defaultSubkey = 0,
DHTRecordCrypto? crypto}) async =>
@ -399,9 +440,13 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
final dhtctx = routingContext ?? _routingContext;
final openedRecordInfo = await _recordOpenInner(
dhtctx: dhtctx, recordKey: recordKey, parent: parent);
debugName: debugName,
dhtctx: dhtctx,
recordKey: recordKey,
parent: parent);
final rec = DHTRecord(
final rec = DHTRecord._(
debugName: debugName,
routingContext: dhtctx,
defaultSubkey: defaultSubkey,
sharedDHTRecordData: openedRecordInfo.shared,
@ -417,6 +462,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
Future<DHTRecord> openWrite(
TypedKey recordKey,
KeyPair writer, {
required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
int defaultSubkey = 0,
@ -426,12 +472,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
final dhtctx = routingContext ?? _routingContext;
final openedRecordInfo = await _recordOpenInner(
debugName: debugName,
dhtctx: dhtctx,
recordKey: recordKey,
parent: parent,
writer: writer);
final rec = DHTRecord(
final rec = DHTRecord._(
debugName: debugName,
routingContext: dhtctx,
defaultSubkey: defaultSubkey,
writer: writer,
@ -453,6 +501,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
/// parent must be specified.
Future<DHTRecord> openOwned(
OwnedDHTRecordPointer ownedDHTRecordPointer, {
required String debugName,
required TypedKey parent,
VeilidRoutingContext? routingContext,
int defaultSubkey = 0,
@ -461,6 +510,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
openWrite(
ownedDHTRecordPointer.recordKey,
ownedDHTRecordPointer.owner,
debugName: debugName,
routingContext: routingContext,
parent: parent,
defaultSubkey: defaultSubkey,

View File

@ -22,11 +22,12 @@ DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson(
/// @nodoc
mixin _$DHTRecordPoolAllocations {
IMap<String, ISet<Typed<FixedEncodedString43>>> get childrenByParent =>
throw _privateConstructorUsedError; // String key due to IMap<> json unsupported in key
throw _privateConstructorUsedError;
IMap<String, Typed<FixedEncodedString43>> get parentByChild =>
throw _privateConstructorUsedError; // String key due to IMap<> json unsupported in key
throw _privateConstructorUsedError;
ISet<Typed<FixedEncodedString43>> get rootRecords =>
throw _privateConstructorUsedError;
IMap<String, String> get debugNames => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@ -43,7 +44,8 @@ abstract class $DHTRecordPoolAllocationsCopyWith<$Res> {
$Res call(
{IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent,
IMap<String, Typed<FixedEncodedString43>> parentByChild,
ISet<Typed<FixedEncodedString43>> rootRecords});
ISet<Typed<FixedEncodedString43>> rootRecords,
IMap<String, String> debugNames});
}
/// @nodoc
@ -63,6 +65,7 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res,
Object? childrenByParent = null,
Object? parentByChild = null,
Object? rootRecords = null,
Object? debugNames = null,
}) {
return _then(_value.copyWith(
childrenByParent: null == childrenByParent
@ -77,6 +80,10 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res,
? _value.rootRecords
: rootRecords // ignore: cast_nullable_to_non_nullable
as ISet<Typed<FixedEncodedString43>>,
debugNames: null == debugNames
? _value.debugNames
: debugNames // ignore: cast_nullable_to_non_nullable
as IMap<String, String>,
) as $Val);
}
}
@ -93,7 +100,8 @@ abstract class _$$DHTRecordPoolAllocationsImplCopyWith<$Res>
$Res call(
{IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent,
IMap<String, Typed<FixedEncodedString43>> parentByChild,
ISet<Typed<FixedEncodedString43>> rootRecords});
ISet<Typed<FixedEncodedString43>> rootRecords,
IMap<String, String> debugNames});
}
/// @nodoc
@ -112,6 +120,7 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res>
Object? childrenByParent = null,
Object? parentByChild = null,
Object? rootRecords = null,
Object? debugNames = null,
}) {
return _then(_$DHTRecordPoolAllocationsImpl(
childrenByParent: null == childrenByParent
@ -126,6 +135,10 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res>
? _value.rootRecords
: rootRecords // ignore: cast_nullable_to_non_nullable
as ISet<Typed<FixedEncodedString43>>,
debugNames: null == debugNames
? _value.debugNames
: debugNames // ignore: cast_nullable_to_non_nullable
as IMap<String, String>,
));
}
}
@ -134,25 +147,30 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res>
@JsonSerializable()
class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations {
const _$DHTRecordPoolAllocationsImpl(
{required this.childrenByParent,
required this.parentByChild,
required this.rootRecords});
{this.childrenByParent = const IMapConst<String, ISet<TypedKey>>({}),
this.parentByChild = const IMapConst<String, TypedKey>({}),
this.rootRecords = const ISetConst<TypedKey>({}),
this.debugNames = const IMapConst<String, String>({})});
factory _$DHTRecordPoolAllocationsImpl.fromJson(Map<String, dynamic> json) =>
_$$DHTRecordPoolAllocationsImplFromJson(json);
@override
@JsonKey()
final IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent;
// String key due to IMap<> json unsupported in key
@override
@JsonKey()
final IMap<String, Typed<FixedEncodedString43>> parentByChild;
// String key due to IMap<> json unsupported in key
@override
@JsonKey()
final ISet<Typed<FixedEncodedString43>> rootRecords;
@override
@JsonKey()
final IMap<String, String> debugNames;
@override
String toString() {
return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords)';
return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords, debugNames: $debugNames)';
}
@override
@ -165,13 +183,15 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations {
(identical(other.parentByChild, parentByChild) ||
other.parentByChild == parentByChild) &&
const DeepCollectionEquality()
.equals(other.rootRecords, rootRecords));
.equals(other.rootRecords, rootRecords) &&
(identical(other.debugNames, debugNames) ||
other.debugNames == debugNames));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, childrenByParent, parentByChild,
const DeepCollectionEquality().hash(rootRecords));
const DeepCollectionEquality().hash(rootRecords), debugNames);
@JsonKey(ignore: true)
@override
@ -190,22 +210,23 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations {
abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations {
const factory _DHTRecordPoolAllocations(
{required final IMap<String, ISet<Typed<FixedEncodedString43>>>
childrenByParent,
required final IMap<String, Typed<FixedEncodedString43>> parentByChild,
required final ISet<Typed<FixedEncodedString43>>
rootRecords}) = _$DHTRecordPoolAllocationsImpl;
{final IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent,
final IMap<String, Typed<FixedEncodedString43>> parentByChild,
final ISet<Typed<FixedEncodedString43>> rootRecords,
final IMap<String, String> debugNames}) = _$DHTRecordPoolAllocationsImpl;
factory _DHTRecordPoolAllocations.fromJson(Map<String, dynamic> json) =
_$DHTRecordPoolAllocationsImpl.fromJson;
@override
IMap<String, ISet<Typed<FixedEncodedString43>>> get childrenByParent;
@override // String key due to IMap<> json unsupported in key
@override
IMap<String, Typed<FixedEncodedString43>> get parentByChild;
@override // String key due to IMap<> json unsupported in key
@override
ISet<Typed<FixedEncodedString43>> get rootRecords;
@override
IMap<String, String> get debugNames;
@override
@JsonKey(ignore: true)
_$$DHTRecordPoolAllocationsImplCopyWith<_$DHTRecordPoolAllocationsImpl>
get copyWith => throw _privateConstructorUsedError;

View File

@ -9,19 +9,29 @@ part of 'dht_record_pool.dart';
_$DHTRecordPoolAllocationsImpl _$$DHTRecordPoolAllocationsImplFromJson(
Map<String, dynamic> json) =>
_$DHTRecordPoolAllocationsImpl(
childrenByParent:
IMap<String, ISet<Typed<FixedEncodedString43>>>.fromJson(
childrenByParent: json['childrenByParent'] == null
? const IMapConst<String, ISet<TypedKey>>({})
: IMap<String, ISet<Typed<FixedEncodedString43>>>.fromJson(
json['childrenByParent'] as Map<String, dynamic>,
(value) => value as String,
(value) => ISet<Typed<FixedEncodedString43>>.fromJson(value,
(value) => Typed<FixedEncodedString43>.fromJson(value))),
parentByChild: IMap<String, Typed<FixedEncodedString43>>.fromJson(
json['parentByChild'] as Map<String, dynamic>,
(value) => value as String,
(value) => Typed<FixedEncodedString43>.fromJson(value)),
rootRecords: ISet<Typed<FixedEncodedString43>>.fromJson(
json['rootRecords'],
(value) => Typed<FixedEncodedString43>.fromJson(value)),
parentByChild: json['parentByChild'] == null
? const IMapConst<String, TypedKey>({})
: IMap<String, Typed<FixedEncodedString43>>.fromJson(
json['parentByChild'] as Map<String, dynamic>,
(value) => value as String,
(value) => Typed<FixedEncodedString43>.fromJson(value)),
rootRecords: json['rootRecords'] == null
? const ISetConst<TypedKey>({})
: ISet<Typed<FixedEncodedString43>>.fromJson(json['rootRecords'],
(value) => Typed<FixedEncodedString43>.fromJson(value)),
debugNames: json['debugNames'] == null
? const IMapConst<String, String>({})
: IMap<String, String>.fromJson(
json['debugNames'] as Map<String, dynamic>,
(value) => value as String,
(value) => value as String),
);
Map<String, dynamic> _$$DHTRecordPoolAllocationsImplToJson(
@ -40,6 +50,10 @@ Map<String, dynamic> _$$DHTRecordPoolAllocationsImplToJson(
'rootRecords': instance.rootRecords.toJson(
(value) => value,
),
'debugNames': instance.debugNames.toJson(
(value) => value,
(value) => value,
),
};
_$OwnedDHTRecordPointerImpl _$$OwnedDHTRecordPointerImplFromJson(

View File

@ -28,7 +28,8 @@ class DHTShortArray {
// if smplWriter is specified, uses a SMPL schema with a single writer
// rather than the key owner
static Future<DHTShortArray> create(
{int stride = maxElements,
{required String debugName,
int stride = maxElements,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto,
@ -42,6 +43,7 @@ class DHTShortArray {
oCnt: 0,
members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]);
dhtRecord = await pool.create(
debugName: debugName,
parent: parent,
routingContext: routingContext,
schema: schema,
@ -50,6 +52,7 @@ class DHTShortArray {
} else {
final schema = DHTSchema.dflt(oCnt: stride + 1);
dhtRecord = await pool.create(
debugName: debugName,
parent: parent,
routingContext: routingContext,
schema: schema,
@ -72,11 +75,15 @@ class DHTShortArray {
}
static Future<DHTShortArray> openRead(TypedKey headRecordKey,
{VeilidRoutingContext? routingContext,
{required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto}) async {
final dhtRecord = await DHTRecordPool.instance.openRead(headRecordKey,
parent: parent, routingContext: routingContext, crypto: crypto);
debugName: debugName,
parent: parent,
routingContext: routingContext,
crypto: crypto);
try {
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
await dhtShortArray._head.operate((head) => head._loadHead());
@ -90,13 +97,17 @@ class DHTShortArray {
static Future<DHTShortArray> openWrite(
TypedKey headRecordKey,
KeyPair writer, {
required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto,
}) async {
final dhtRecord = await DHTRecordPool.instance.openWrite(
headRecordKey, writer,
parent: parent, routingContext: routingContext, crypto: crypto);
debugName: debugName,
parent: parent,
routingContext: routingContext,
crypto: crypto);
try {
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
await dhtShortArray._head.operate((head) => head._loadHead());
@ -109,6 +120,7 @@ class DHTShortArray {
static Future<DHTShortArray> openOwned(
OwnedDHTRecordPointer ownedDHTRecordPointer, {
required String debugName,
required TypedKey parent,
VeilidRoutingContext? routingContext,
DHTRecordCrypto? crypto,
@ -116,6 +128,7 @@ class DHTShortArray {
openWrite(
ownedDHTRecordPointer.recordKey,
ownedDHTRecordPointer.owner,
debugName: debugName,
routingContext: routingContext,
parent: parent,
crypto: crypto,

View File

@ -17,13 +17,13 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
required T Function(List<int> data) decodeElement,
}) : _decodeElement = decodeElement,
super(const BlocBusyState(AsyncValue.loading())) {
_initFuture = Future(() async {
_initWait.add(() async {
// Open DHT record
_shortArray = await open();
_wantsCloseRecord = true;
// Make initial state update
unawaited(_refreshNoWait());
await _refreshNoWait();
_subscription = await _shortArray.listen(_update);
});
}
@ -42,7 +42,7 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
// }
Future<void> refresh({bool forceRefresh = false}) async {
await _initFuture;
await _initWait();
await _refreshNoWait(forceRefresh: forceRefresh);
}
@ -75,7 +75,7 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
@override
Future<void> close() async {
await _initFuture;
await _initWait();
await _subscription?.cancel();
_subscription = null;
if (_wantsCloseRecord) {
@ -85,24 +85,24 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
}
Future<R?> operate<R>(Future<R?> Function(DHTShortArrayRead) closure) async {
await _initFuture;
await _initWait();
return _shortArray.operate(closure);
}
Future<(R?, bool)> operateWrite<R>(
Future<R?> Function(DHTShortArrayWrite) closure) async {
await _initFuture;
await _initWait();
return _shortArray.operateWrite(closure);
}
Future<void> operateWriteEventual(
Future<bool> Function(DHTShortArrayWrite) closure,
{Duration? timeout}) async {
await _initFuture;
await _initWait();
return _shortArray.operateWriteEventual(closure, timeout: timeout);
}
late final Future<void> _initFuture;
final WaitSet _initWait = WaitSet();
late final DHTShortArray _shortArray;
final T Function(List<int> data) _decodeElement;
StreamSubscription<void>? _subscription;

View File

@ -184,7 +184,7 @@ class _DHTShortArrayHead {
final oldRecord = oldRecords[newKey];
if (oldRecord == null) {
// Open the new record
final newRecord = await _openLinkedRecord(newKey);
final newRecord = await _openLinkedRecord(newKey, n);
newRecords[newKey] = newRecord;
updatedLinkedRecords.add(newRecord);
} else {
@ -263,6 +263,7 @@ class _DHTShortArrayHead {
oCnt: 0,
members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]);
final dhtRecord = await pool.create(
debugName: '${_headRecord.debugName}_linked_$recordNumber',
parent: parent,
routingContext: routingContext,
schema: schema,
@ -279,17 +280,20 @@ class _DHTShortArrayHead {
}
/// Open a linked record for reading or writing, same as the head record
Future<DHTRecord> _openLinkedRecord(TypedKey recordKey) async {
Future<DHTRecord> _openLinkedRecord(
TypedKey recordKey, int recordNumber) async {
final writer = _headRecord.writer;
return (writer != null)
? await DHTRecordPool.instance.openWrite(
recordKey,
writer,
debugName: '${_headRecord.debugName}_linked_$recordNumber',
parent: _headRecord.key,
routingContext: _headRecord.routingContext,
)
: await DHTRecordPool.instance.openRead(
recordKey,
debugName: '${_headRecord.debugName}_linked_$recordNumber',
parent: _headRecord.key,
routingContext: _headRecord.routingContext,
);

View File

@ -132,7 +132,10 @@ extension IdentityMasterExtension on IdentityMaster {
late final List<AccountRecordInfo> accountRecordInfo;
await (await pool.openRead(identityRecordKey,
parent: masterRecordKey, crypto: identityRecordCrypto))
debugName:
'IdentityMaster::readAccountsFromIdentity::IdentityRecord',
parent: masterRecordKey,
crypto: identityRecordCrypto))
.scope((identityRec) async {
final identity = await identityRec.getJson(Identity.fromJson);
if (identity == null) {
@ -161,14 +164,17 @@ extension IdentityMasterExtension on IdentityMaster {
/////// Add account with profile to DHT
// Open identity key for writing
veilidLoggy.debug('Opening master identity');
veilidLoggy.debug('Opening identity record');
return (await pool.openWrite(
identityRecordKey, identityWriter(identitySecret),
debugName: 'IdentityMaster::addAccountToIdentity::IdentityRecord',
parent: masterRecordKey))
.scope((identityRec) async {
// Create new account to insert into identity
veilidLoggy.debug('Creating new account');
return (await pool.create(parent: identityRec.key))
return (await pool.create(
debugName: 'IdentityMaster::addAccountToIdentity::AccountRecord',
parent: identityRec.key))
.deleteScope((accountRec) async {
final account = await createAccountCallback(accountRec.key);
// Write account key
@ -222,11 +228,16 @@ class IdentityMasterWithSecrets {
// IdentityMaster DHT record is public/unencrypted
veilidLoggy.debug('Creating master identity record');
return (await pool.create(crypto: const DHTRecordCryptoPublic()))
return (await pool.create(
debugName:
'IdentityMasterWithSecrets::create::IdentityMasterRecord',
crypto: const DHTRecordCryptoPublic()))
.deleteScope((masterRec) async {
veilidLoggy.debug('Creating identity record');
// Identity record is private
return (await pool.create(parent: masterRec.key))
return (await pool.create(
debugName: 'IdentityMasterWithSecrets::create::IdentityRecord',
parent: masterRec.key))
.scope((identityRec) async {
// Make IdentityMaster
final masterRecordKey = masterRec.key;
@ -282,7 +293,9 @@ Future<IdentityMaster> openIdentityMaster(
final pool = DHTRecordPool.instance;
// IdentityMaster DHT record is public/unencrypted
return (await pool.openRead(identityMasterRecordKey))
return (await pool.openRead(identityMasterRecordKey,
debugName:
'IdentityMaster::openIdentityMaster::IdentityMasterRecord'))
.deleteScope((masterRec) async {
final identityMaster =
(await masterRec.getJson(IdentityMaster.fromJson, forceRefresh: true))!;