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:bloc/bloc.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../init.dart';
import '../repository/account_repository/account_repository.dart'; import '../repository/account_repository/account_repository.dart';
class ActiveLocalAccountCubit extends Cubit<TypedKey?> { class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
ActiveLocalAccountCubit(AccountRepository accountRepository) ActiveLocalAccountCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository, : _accountRepository = accountRepository,
super(null) { super(accountRepository.getActiveLocalAccount()) {
// Subscribe to streams // 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) { _accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) { switch (change) {
case AccountRepositoryChange.activeLocalAccount: case AccountRepositoryChange.activeLocalAccount:

View File

@ -3,25 +3,14 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import '../../init.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../repository/account_repository/account_repository.dart'; import '../repository/account_repository/account_repository.dart';
class LocalAccountsCubit extends Cubit<IList<LocalAccount>> { class LocalAccountsCubit extends Cubit<IList<LocalAccount>> {
LocalAccountsCubit(AccountRepository accountRepository) LocalAccountsCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository, : _accountRepository = accountRepository,
super(IList<LocalAccount>()) { super(accountRepository.getLocalAccounts()) {
// Subscribe to streams // 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) { _accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) { switch (change) {
case AccountRepositoryChange.localAccounts: case AccountRepositoryChange.localAccounts:

View File

@ -3,25 +3,14 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import '../../init.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../repository/account_repository/account_repository.dart'; import '../repository/account_repository/account_repository.dart';
class UserLoginsCubit extends Cubit<IList<UserLogin>> { class UserLoginsCubit extends Cubit<IList<UserLogin>> {
UserLoginsCubit(AccountRepository accountRepository) UserLoginsCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository, : _accountRepository = accountRepository,
super(IList<UserLogin>()) { super(accountRepository.getUserLogins()) {
// Subscribe to streams // 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) { _accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) { switch (change) {
case AccountRepositoryChange.userLogins: case AccountRepositoryChange.userLogins:

View File

@ -202,18 +202,24 @@ class AccountRepository {
createAccountCallback: (parent) async { createAccountCallback: (parent) async {
// Make empty contact list // Make empty contact list
log.debug('Creating contacts 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); .scope((r) async => r.recordPointer);
// Make empty contact invitation record list // Make empty contact invitation record list
log.debug('Creating contact invitation records list'); log.debug('Creating contact invitation records list');
final contactInvitationRecords = final contactInvitationRecords = await (await DHTShortArray.create(
await (await DHTShortArray.create(parent: parent)) debugName:
.scope((r) async => r.recordPointer); 'AccountRepository::_newLocalAccount::ContactInvitations',
parent: parent))
.scope((r) async => r.recordPointer);
// Make empty chat record list // Make empty chat record list
log.debug('Creating chat records 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); .scope((r) async => r.recordPointer);
// Make account object // Make account object
@ -391,6 +397,7 @@ class AccountRepository {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final record = await pool.openOwned( final record = await pool.openOwned(
userLogin.accountRecordInfo.accountRecord, userLogin.accountRecordInfo.accountRecord,
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
parent: localAccount.identityMaster.identityRecordKey); parent: localAccount.identityMaster.identityRecordKey);
return record; 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_localizations/flutter_localizations.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:provider/provider.dart';
import 'account_manager/account_manager.dart'; import 'account_manager/account_manager.dart';
import 'init.dart';
import 'layout/splash.dart';
import 'router/router.dart'; import 'router/router.dart';
import 'settings/settings.dart'; import 'settings/settings.dart';
import 'tick.dart'; import 'tick.dart';
@ -23,57 +26,66 @@ class VeilidChatApp extends StatelessWidget {
final ThemeData initialThemeData; final ThemeData initialThemeData;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => FutureProvider<VeilidChatGlobalInit?>(
final localizationDelegate = LocalizedApp.of(context).delegate; initialData: null,
create: (context) async => VeilidChatGlobalInit.initialize(),
return ThemeProvider( builder: (context, child) {
initTheme: initialThemeData, final globalInit = context.watch<VeilidChatGlobalInit?>();
builder: (_, theme) => LocalizationProvider( if (globalInit == null) {
state: LocalizationProvider.of(context).state, // Splash screen until we're done with init
child: MultiBlocProvider( return const Splash();
providers: [ }
BlocProvider<ConnectionStateCubit>( // Once init is done, we proceed with the app
create: (context) => final localizationDelegate = LocalizedApp.of(context).delegate;
ConnectionStateCubit(ProcessorRepository.instance)), return ThemeProvider(
BlocProvider<RouterCubit>( initTheme: initialThemeData,
create: (context) => builder: (_, theme) => LocalizationProvider(
RouterCubit(AccountRepository.instance), state: LocalizationProvider.of(context).state,
), child: MultiBlocProvider(
BlocProvider<LocalAccountsCubit>( providers: [
create: (context) => BlocProvider<ConnectionStateCubit>(
LocalAccountsCubit(AccountRepository.instance), create: (context) => ConnectionStateCubit(
), ProcessorRepository.instance)),
BlocProvider<UserLoginsCubit>( BlocProvider<RouterCubit>(
create: (context) => create: (context) =>
UserLoginsCubit(AccountRepository.instance), RouterCubit(AccountRepository.instance),
), ),
BlocProvider<ActiveLocalAccountCubit>( BlocProvider<LocalAccountsCubit>(
create: (context) => create: (context) =>
ActiveLocalAccountCubit(AccountRepository.instance), LocalAccountsCubit(AccountRepository.instance),
), ),
BlocProvider<PreferencesCubit>( BlocProvider<UserLoginsCubit>(
create: (context) => create: (context) =>
PreferencesCubit(PreferencesRepository.instance), UserLoginsCubit(AccountRepository.instance),
) ),
], BlocProvider<ActiveLocalAccountCubit>(
child: BackgroundTicker( create: (context) => ActiveLocalAccountCubit(
builder: (context) => MaterialApp.router( AccountRepository.instance),
debugShowCheckedModeBanner: false, ),
routerConfig: context.watch<RouterCubit>().router(), BlocProvider<PreferencesCubit>(
title: translate('app.title'), create: (context) =>
theme: theme, PreferencesCubit(PreferencesRepository.instance),
localizationsDelegates: [ )
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
localizationDelegate
], ],
supportedLocales: localizationDelegate.supportedLocales, child: BackgroundTicker(
locale: localizationDelegate.currentLocale, 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 @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {

View File

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

View File

@ -1,13 +1,13 @@
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:bloc_tools/bloc_tools.dart'; import 'package:bloc_tools/bloc_tools.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import 'cubits.dart';
@immutable @immutable
class ActiveConversationState extends Equatable { class ActiveConversationState extends Equatable {
@ -39,9 +39,7 @@ typedef ActiveConversationsBlocMapState
// archived chats or contacts that are not actively in a chat. // archived chats or contacts that are not actively in a chat.
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey, class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit> AsyncValue<ActiveConversationState>, ActiveConversationCubit>
with with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
StateFollower<BlocBusyState<AsyncValue<IList<proto.Chat>>>, TypedKey,
proto.Chat> {
ActiveConversationsBlocMapCubit( ActiveConversationsBlocMapCubit(
{required ActiveAccountInfo activeAccountInfo, {required ActiveAccountInfo activeAccountInfo,
required ContactListCubit contactListCubit}) required ContactListCubit contactListCubit})
@ -77,18 +75,6 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
/// StateFollower ///////////////////////// /// 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 @override
Future<void> removeFromState(TypedKey key) => remove(key); Future<void> removeFromState(TypedKey key) => remove(key);

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ typedef WaitingInvitationsBlocMapState
class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey, class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<InvitationStatus>, WaitingInvitationCubit> AsyncValue<InvitationStatus>, WaitingInvitationCubit>
with with
StateFollower< StateMapFollower<
BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>>, BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>>,
TypedKey, TypedKey,
proto.ContactInvitationRecord> { proto.ContactInvitationRecord> {
@ -37,17 +37,6 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
contactInvitationRecord: contactInvitationRecord))); contactInvitationRecord: contactInvitationRecord)));
/// StateFollower ///////////////////////// /// 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 @override
Future<void> removeFromState(TypedKey key) => remove(key); Future<void> removeFromState(TypedKey key) => remove(key);

View File

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

View File

@ -6,6 +6,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import 'conversation_cubit.dart';
////////////////////////////////////////////////// //////////////////////////////////////////////////
// Mutable state for per-account contacts // Mutable state for per-account contacts
@ -14,7 +15,8 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
ContactListCubit({ ContactListCubit({
required ActiveAccountInfo activeAccountInfo, required ActiveAccountInfo activeAccountInfo,
required proto.Account account, required proto.Account account,
}) : super( }) : _activeAccountInfo = activeAccountInfo,
super(
open: () => _open(activeAccountInfo, account), open: () => _open(activeAccountInfo, account),
decodeElement: proto.Contact.fromBuffer); decodeElement: proto.Contact.fromBuffer);
@ -26,6 +28,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
final contactListRecordKey = account.contactList.toVeilid(); final contactListRecordKey = account.contactList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey, final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey,
debugName: 'ContactListCubit::_open::ContactList',
parent: accountRecordKey); parent: accountRecordKey);
return dhtRecord; return dhtRecord;
@ -60,9 +63,10 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
} }
Future<void> deleteContact({required proto.Contact contact}) async { Future<void> deleteContact({required proto.Contact contact}) async {
final pool = DHTRecordPool.instance; final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
final localConversationKey = contact.localConversationRecordKey.toVeilid(); final localConversationRecordKey =
final remoteConversationKey = contact.localConversationRecordKey.toVeilid();
final remoteConversationRecordKey =
contact.remoteConversationRecordKey.toVeilid(); contact.remoteConversationRecordKey.toVeilid();
// Remove Contact from account's list // Remove Contact from account's list
@ -85,17 +89,21 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (success && deletedItem != null) { if (success && deletedItem != null) {
try { 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) { } on Exception catch (e) {
log.debug('error removing local conversation record key: $e', e); log.debug('error deleting conversation records: $e', e);
}
try {
if (localConversationKey != remoteConversationKey) {
await pool.delete(remoteConversationKey);
}
} on Exception catch (e) {
log.debug('error removing remote conversation record key: $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 '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
@immutable @immutable
class ConversationState extends Equatable { class ConversationState extends Equatable {
@ -36,11 +37,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
_localConversationRecordKey = localConversationRecordKey, _localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey, _remoteConversationRecordKey = remoteConversationRecordKey,
_incrementalState = const ConversationState(
localConversation: null, remoteConversation: null),
super(const AsyncValue.loading()) { super(const AsyncValue.loading()) {
if (_localConversationRecordKey != null) { if (_localConversationRecordKey != null) {
Future.delayed(Duration.zero, () async { _initWait.add(() async {
await _setLocalConversation(() async { await _setLocalConversation(() async {
final accountRecordKey = _activeAccountInfo final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey; .userLogin.accountRecordInfo.accountRecord.recordKey;
@ -51,14 +50,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final writer = _activeAccountInfo.conversationWriter; final writer = _activeAccountInfo.conversationWriter;
final record = await pool.openWrite( final record = await pool.openWrite(
_localConversationRecordKey!, writer, _localConversationRecordKey!, writer,
parent: accountRecordKey, crypto: crypto); debugName: 'ConversationCubit::LocalConversation',
parent: accountRecordKey,
crypto: crypto);
return record; return record;
}); });
}); });
} }
if (_remoteConversationRecordKey != null) { if (_remoteConversationRecordKey != null) {
Future.delayed(Duration.zero, () async { _initWait.add(() async {
await _setRemoteConversation(() async { await _setRemoteConversation(() async {
final accountRecordKey = _activeAccountInfo final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey; .userLogin.accountRecordInfo.accountRecord.recordKey;
@ -67,7 +68,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto(); final crypto = await _cachedConversationCrypto();
final record = await pool.openRead(_remoteConversationRecordKey, final record = await pool.openRead(_remoteConversationRecordKey,
parent: accountRecordKey, crypto: crypto); debugName: 'ConversationCubit::RemoteConversation',
parent: accountRecordKey,
crypto: crypto);
return record; return record;
}); });
}); });
@ -76,6 +79,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
@override @override
Future<void> close() async { Future<void> close() async {
await _initWait();
await _localSubscription?.cancel(); await _localSubscription?.cancel();
await _remoteSubscription?.cancel(); await _remoteSubscription?.cancel();
await _localConversationCubit?.close(); await _localConversationCubit?.close();
@ -84,7 +88,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
await super.close(); await super.close();
} }
void updateLocalConversationState(AsyncValue<proto.Conversation> avconv) { void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when( final newState = avconv.when(
data: (conv) { data: (conv) {
_incrementalState = ConversationState( _incrementalState = ConversationState(
@ -106,7 +110,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
emit(newState); emit(newState);
} }
void updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) { void _updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when( final newState = avconv.when(
data: (conv) { data: (conv) {
_incrementalState = ConversationState( _incrementalState = ConversationState(
@ -135,7 +139,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
_localConversationCubit = DefaultDHTRecordCubit( _localConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer); open: open, decodeState: proto.Conversation.fromBuffer);
_localSubscription = _localSubscription =
_localConversationCubit!.stream.listen(updateLocalConversationState); _localConversationCubit!.stream.listen(_updateLocalConversationState);
} }
// Open remote converation key // Open remote converation key
@ -145,7 +149,57 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
_remoteConversationCubit = DefaultDHTRecordCubit( _remoteConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer); open: open, decodeState: proto.Conversation.fromBuffer);
_remoteSubscription = _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 // Initialize a local conversation
@ -174,23 +228,25 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
if (existingConversationRecordKey != null) { if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openWrite( localConversationRecord = await pool.openWrite(
existingConversationRecordKey, writer, existingConversationRecordKey, writer,
parent: accountRecordKey, crypto: crypto); debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey,
crypto: crypto);
} else { } else {
final localConversationRecordCreate = await pool.create( localConversationRecord = await pool.create(
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey, parent: accountRecordKey,
crypto: crypto, crypto: crypto,
writer: writer,
schema: DHTSchema.smpl( schema: DHTSchema.smpl(
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)])); 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 final out = localConversationRecord
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
.deleteScope((localConversation) async { .deleteScope((localConversation) async {
// Make messages log // Make messages log
return initLocalMessages( return _initLocalMessages(
activeAccountInfo: _activeAccountInfo, activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: _remoteIdentityPublicKey, remoteIdentityPublicKey: _remoteIdentityPublicKey,
localConversationKey: localConversation.key, localConversationKey: localConversation.key,
@ -211,7 +267,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final out = await callback(localConversation); final out = await callback(localConversation);
// Upon success emit the local conversation record to the state // Upon success emit the local conversation record to the state
updateLocalConversationState(AsyncValue.data(conversation)); _updateLocalConversationState(AsyncValue.data(conversation));
return out; return out;
}); });
@ -225,7 +281,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
} }
// Initialize local messages // Initialize local messages
Future<T> initLocalMessages<T>({ Future<T> _initLocalMessages<T>({
required ActiveAccountInfo activeAccountInfo, required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey, required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationKey, required TypedKey localConversationKey,
@ -236,12 +292,17 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final writer = activeAccountInfo.conversationWriter; final writer = activeAccountInfo.conversationWriter;
return (await DHTShortArray.create( 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)); .deleteScope((messages) async => await callback(messages));
} }
// Force refresh of conversation keys // Force refresh of conversation keys
Future<void> refresh() async { Future<void> refresh() async {
await _initWait();
final lcc = _localConversationCubit; final lcc = _localConversationCubit;
final rcc = _remoteConversationCubit; final rcc = _remoteConversationCubit;
@ -260,7 +321,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
.tryWriteProtobuf(proto.Conversation.fromBuffer, conversation); .tryWriteProtobuf(proto.Conversation.fromBuffer, conversation);
if (update != null) { if (update != null) {
updateLocalConversationState(AsyncValue.data(conversation)); _updateLocalConversationState(AsyncValue.data(conversation));
} }
return update; return update;
@ -286,7 +347,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit; DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription; StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription; StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
ConversationState _incrementalState; ConversationState _incrementalState = const ConversationState(
localConversation: null, remoteConversation: null);
// //
DHTRecordCrypto? _conversationCrypto; DHTRecordCrypto? _conversationCrypto;
final WaitSet _initWait = WaitSet();
} }

View File

@ -8,34 +8,38 @@ import 'app.dart';
import 'tools/tools.dart'; import 'tools/tools.dart';
import 'veilid_processor/veilid_processor.dart'; import 'veilid_processor/veilid_processor.dart';
final Completer<void> eventualInitialized = Completer<void>(); class VeilidChatGlobalInit {
VeilidChatGlobalInit._();
// Initialize Veilid // Initialize Veilid
Future<void> initializeVeilid() async { Future<void> _initializeVeilid() async {
// Init Veilid // Init Veilid
Veilid.instance.initializeVeilidCore( Veilid.instance.initializeVeilidCore(
getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
// Veilid logging // Veilid logging
initVeilidLog(kDebugMode); initVeilidLog(kDebugMode);
// Startup Veilid // Startup Veilid
await ProcessorRepository.instance.startup(); await ProcessorRepository.instance.startup();
// DHT Record Pool // DHT Record Pool
await DHTRecordPool.init(); await DHTRecordPool.init();
} }
// Initialize repositories // Initialize repositories
Future<void> initializeRepositories() async { Future<void> _initializeRepositories() async {
await AccountRepository.instance.init(); await AccountRepository.instance.init();
} }
Future<void> initializeVeilidChat() async { static Future<VeilidChatGlobalInit> initialize() async {
log.info('Initializing Veilid'); final veilidChatGlobalInit = VeilidChatGlobalInit._();
await initializeVeilid();
log.info('Initializing Repositories');
await initializeRepositories();
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( create: (context) => ActiveConversationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo, activeAccountInfo: widget.activeAccountInfo,
contactListCubit: context.read<ContactListCubit>()) contactListCubit: context.read<ContactListCubit>())
..followBloc(context.read<ChatListCubit>())), ..follow(context.read<ChatListCubit>())),
BlocProvider( BlocProvider(
create: (context) => ActiveSingleContactChatBlocMapCubit( create: (context) => ActiveSingleContactChatBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo, activeAccountInfo: widget.activeAccountInfo,
contactListCubit: context.read<ContactListCubit>(), contactListCubit: context.read<ContactListCubit>(),
chatListCubit: context.read<ChatListCubit>()) chatListCubit: context.read<ChatListCubit>())
..followBloc(context.read<ActiveConversationsBlocMapCubit>())), ..follow(context.read<ActiveConversationsBlocMapCubit>())),
BlocProvider( BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit( create: (context) => WaitingInvitationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo, account: account) activeAccountInfo: widget.activeAccountInfo, account: account)
..followBloc(context.read<ContactInvitationListCubit>())) ..follow(context.read<ContactInvitationListCubit>()))
], ],
child: MultiBlocListener(listeners: [ child: MultiBlocListener(listeners: [
BlocListener<WaitingInvitationsBlocMapCubit, 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 'default_app_bar.dart';
export 'home/home.dart'; export 'home/home.dart';
export 'home/home_account_ready/main_pager/main_pager.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 'package:intl/date_symbol_data_local.dart';
import 'app.dart'; import 'app.dart';
import 'init.dart';
import 'settings/preferences_repository.dart'; import 'settings/preferences_repository.dart';
import 'theme/theme.dart'; import 'theme/theme.dart';
import 'tools/tools.dart'; import 'tools/tools.dart';
@ -45,9 +44,6 @@ void main() async {
fallbackLocale: 'en_US', supportedLocales: ['en_US']); fallbackLocale: 'en_US', supportedLocales: ['en_US']);
await initializeDateFormatting(); await initializeDateFormatting();
// Start up Veilid and Veilid processor in the background
unawaited(initializeVeilidChat());
// Run the app // Run the app
// Hot reloads will only restart this part, not Veilid // Hot reloads will only restart this part, not Veilid
runApp(LocalizedApp(localizationDelegate, runApp(LocalizedApp(localizationDelegate,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,9 @@ library;
export 'src/async_tag_lock.dart'; export 'src/async_tag_lock.dart';
export 'src/async_value.dart'; export 'src/async_value.dart';
export 'src/delayed_wait_set.dart';
export 'src/serial_future.dart'; export 'src/serial_future.dart';
export 'src/single_future.dart'; export 'src/single_future.dart';
export 'src/single_state_processor.dart'; export 'src/single_state_processor.dart';
export 'src/single_stateless_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(); AsyncTagLock<Object> _keys = AsyncTagLock();
// Process a single future at a time per tag /// Process a single future at a time per tag
// ///
// The closure function is called to produce the future that is to be executed. /// 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 /// If a future with a particular tag is still executing, the onBusy callback
// is called. /// is called.
// When a tagged singleFuture finishes executing, the onDone 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 /// If an unhandled exception happens in the closure future, the onError callback
// is called. /// is called.
void singleFuture<T>(Object tag, Future<T> Function() closure, void singleFuture<T>(Object tag, Future<T> Function() closure,
{void Function()? onBusy, {void Function()? onBusy,
void Function(T)? onDone, 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 { singleFuture(this, () async {
var newState = newInputState; var newState = newInputState;
var newClosure = closure;
var done = false; var done = false;
while (!done) { while (!done) {
await closure(newState); await newClosure(newState);
// See if there's another state change to process // See if there's another state change to process
final next = _nextState; final nextState = _nextState;
final nextClosure = _nextClosure;
_nextState = null; _nextState = null;
if (next != null) { _nextClosure = null;
newState = next; if (nextState != null) {
newState = nextState;
newClosure = nextClosure!;
} else { } else {
done = true; done = true;
} }
@ -38,8 +42,26 @@ class SingleStateProcessor<State> {
}, onBusy: () { }, onBusy: () {
// Keep this state until we process again // Keep this state until we process again
_nextState = newInputState; _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; 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_map_cubit.dart';
export 'src/bloc_tools_extension.dart'; export 'src/bloc_tools_extension.dart';
export 'src/future_cubit.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/stream_wrapper_cubit.dart';
export 'src/transformer_cubit.dart'; export 'src/transformer_cubit.dart';

View File

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

View File

@ -3,6 +3,9 @@ import 'dart:async';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.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>; typedef BlocMapState<K, S> = IMap<K, S>;
@ -18,14 +21,15 @@ class _ItemEntry<S, B> {
// cubits. // cubits.
// //
// K = Key type for the bloc map, used to look up some mapped cubit // 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 // B = Bloc/cubit type for the value, output states of type S
abstract class BlocMapCubit<K, S, B extends BlocBase<S>> abstract class BlocMapCubit<K, V, B extends BlocBase<V>>
extends Cubit<BlocMapState<K, S>> { extends Cubit<BlocMapState<K, V>>
with StateMapFollowable<BlocMapState<K, V>, K, V> {
BlocMapCubit() BlocMapCubit()
: _entries = {}, : _entries = {},
_tagLock = AsyncTagLock(), _tagLock = AsyncTagLock(),
super(IMap<K, S>()); super(IMap<K, V>());
@override @override
Future<void> close() async { Future<void> close() async {
@ -34,6 +38,13 @@ abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
await super.close(); 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) { Future<void> add(MapEntry<K, B> Function() create) {
// Create new element // Create new element
final newElement = create(); 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 { _tagLock.protect(key, closure: () async {
// Remove entry with the same key if it exists // Remove entry with the same key if it exists
await _internalRemove(key); await _internalRemove(key);
@ -107,6 +118,10 @@ abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
return closure(entry.bloc); 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; 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_crypto.dart';
export 'dht_record_cubit.dart'; export 'dht_record_cubit.dart';
export 'dht_record_pool.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 { class DHTRecord {
DHTRecord( DHTRecord._(
{required VeilidRoutingContext routingContext, {required VeilidRoutingContext routingContext,
required SharedDHTRecordData sharedDHTRecordData, required SharedDHTRecordData sharedDHTRecordData,
required int defaultSubkey, required int defaultSubkey,
required KeyPair? writer, required KeyPair? writer,
required DHTRecordCrypto crypto}) required DHTRecordCrypto crypto,
required this.debugName})
: _crypto = crypto, : _crypto = crypto,
_routingContext = routingContext, _routingContext = routingContext,
_defaultSubkey = defaultSubkey, _defaultSubkey = defaultSubkey,
@ -34,6 +35,7 @@ class DHTRecord {
final int _defaultSubkey; final int _defaultSubkey;
final KeyPair? _writer; final KeyPair? _writer;
final DHTRecordCrypto _crypto; final DHTRecordCrypto _crypto;
final String debugName;
bool _open; bool _open;
@internal @internal

View File

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import '../../../veilid_support.dart'; import '../../../veilid_support.dart';
@ -20,7 +21,7 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
}) : _wantsCloseRecord = false, }) : _wantsCloseRecord = false,
_stateFunction = stateFunction, _stateFunction = stateFunction,
super(const AsyncValue.loading()) { super(const AsyncValue.loading()) {
Future.delayed(Duration.zero, () async { initWait.add(() async {
// Do record open/create // Do record open/create
_record = await open(); _record = await open();
_wantsCloseRecord = true; _wantsCloseRecord = true;
@ -73,6 +74,7 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
@override @override
Future<void> close() async { Future<void> close() async {
await initWait();
await _record.cancelWatch(); await _record.cancelWatch();
await _subscription?.cancel(); await _subscription?.cancel();
_subscription = null; _subscription = null;
@ -84,6 +86,8 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
} }
Future<void> refresh(List<ValueSubkeyRange> subkeys) async { Future<void> refresh(List<ValueSubkeyRange> subkeys) async {
await initWait();
var updateSubkeys = [...subkeys]; var updateSubkeys = [...subkeys];
for (final skr in subkeys) { for (final skr in subkeys) {
@ -107,69 +111,11 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
DHTRecord get record => _record; DHTRecord get record => _record;
@protected
final WaitSet initWait = WaitSet();
StreamSubscription<DHTRecordWatchChange>? _subscription; StreamSubscription<DHTRecordWatchChange>? _subscription;
late DHTRecord _record; late DHTRecord _record;
bool _wantsCloseRecord; bool _wantsCloseRecord;
final StateFunction<T> _stateFunction; 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; const int watchBackoffMax = 30;
/// Record pool that managed DHTRecords and allows for tagged deletion /// Record pool that managed DHTRecords and allows for tagged deletion
/// String versions of keys due to IMap<> json unsupported in key
@freezed @freezed
class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations {
const factory DHTRecordPoolAllocations({ const factory DHTRecordPoolAllocations({
required IMap<String, ISet<TypedKey>> @Default(IMapConst<String, ISet<TypedKey>>({}))
childrenByParent, // String key due to IMap<> json unsupported in key IMap<String, ISet<TypedKey>> childrenByParent,
required IMap<String, TypedKey> @Default(IMapConst<String, TypedKey>({}))
parentByChild, // String key due to IMap<> json unsupported in key IMap<String, TypedKey> parentByChild,
required ISet<TypedKey> rootRecords, @Default(ISetConst<TypedKey>({})) ISet<TypedKey> rootRecords,
@Default(IMapConst<String, String>({})) IMap<String, String> debugNames,
}) = _DHTRecordPoolAllocations; }) = _DHTRecordPoolAllocations;
factory DHTRecordPoolAllocations.fromJson(dynamic json) => factory DHTRecordPoolAllocations.fromJson(dynamic json) =>
@ -92,10 +94,7 @@ class OpenedRecordInfo {
class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> { class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext)
: _state = DHTRecordPoolAllocations( : _state = const DHTRecordPoolAllocations(),
childrenByParent: IMap(),
parentByChild: IMap(),
rootRecords: ISet()),
_mutex = Mutex(), _mutex = Mutex(),
_opened = <TypedKey, OpenedRecordInfo>{}, _opened = <TypedKey, OpenedRecordInfo>{},
_routingContext = routingContext, _routingContext = routingContext,
@ -129,8 +128,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
@override @override
DHTRecordPoolAllocations valueFromJson(Object? obj) => obj != null DHTRecordPoolAllocations valueFromJson(Object? obj) => obj != null
? DHTRecordPoolAllocations.fromJson(obj) ? DHTRecordPoolAllocations.fromJson(obj)
: DHTRecordPoolAllocations( : const DHTRecordPoolAllocations();
childrenByParent: IMap(), parentByChild: IMap(), rootRecords: ISet());
@override @override
Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson(); Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson();
@ -148,7 +146,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
Veilid get veilid => _veilid; Veilid get veilid => _veilid;
Future<OpenedRecordInfo> _recordCreateInner( Future<OpenedRecordInfo> _recordCreateInner(
{required VeilidRoutingContext dhtctx, {required String debugName,
required VeilidRoutingContext dhtctx,
required DHTSchema schema, required DHTSchema schema,
KeyPair? writer, KeyPair? writer,
TypedKey? parent}) async { TypedKey? parent}) async {
@ -169,13 +168,18 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
_opened[recordDescriptor.key] = openedRecordInfo; _opened[recordDescriptor.key] = openedRecordInfo;
// Register the dependency // Register the dependency
await _addDependencyInner(parent, recordDescriptor.key); await _addDependencyInner(
parent,
recordDescriptor.key,
debugName: debugName,
);
return openedRecordInfo; return openedRecordInfo;
} }
Future<OpenedRecordInfo> _recordOpenInner( Future<OpenedRecordInfo> _recordOpenInner(
{required VeilidRoutingContext dhtctx, {required String debugName,
required VeilidRoutingContext dhtctx,
required TypedKey recordKey, required TypedKey recordKey,
KeyPair? writer, KeyPair? writer,
TypedKey? parent}) async { TypedKey? parent}) async {
@ -198,7 +202,11 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
_opened[recordDescriptor.key] = newOpenedRecordInfo; _opened[recordDescriptor.key] = newOpenedRecordInfo;
// Register the dependency // Register the dependency
await _addDependencyInner(parent, recordKey); await _addDependencyInner(
parent,
recordKey,
debugName: debugName,
);
return newOpenedRecordInfo; return newOpenedRecordInfo;
} }
@ -218,7 +226,11 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
} }
// Register the dependency // Register the dependency
await _addDependencyInner(parent, recordKey); await _addDependencyInner(
parent,
recordKey,
debugName: debugName,
);
return openedRecordInfo; return openedRecordInfo;
} }
@ -259,6 +271,18 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
return allDeps.reversedView; 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 { Future<void> _deleteInner(TypedKey recordKey) async {
// Remove this child from parents // Remove this child from parents
await _removeDependenciesInner([recordKey]); await _removeDependenciesInner([recordKey]);
@ -269,7 +293,10 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
await _mutex.protect(() async { await _mutex.protect(() async {
final allDeps = _collectChildrenInner(recordKey); 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]; final ori = _opened[recordKey];
if (ori != null) { 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'); assert(_mutex.isLocked, 'should be locked here');
if (parent == null) { if (parent == null) {
if (_state.rootRecords.contains(child)) { if (_state.rootRecords.contains(child)) {
// Dependency already added // Dependency already added
return; return;
} }
_state = await store( _state = await store(_state.copyWith(
_state.copyWith(rootRecords: _state.rootRecords.add(child))); rootRecords: _state.rootRecords.add(child),
debugNames: _state.debugNames.add(child.toJson(), debugName)));
} else { } else {
final childrenOfParent = final childrenOfParent =
_state.childrenByParent[parent.toJson()] ?? ISet<TypedKey>(); _state.childrenByParent[parent.toJson()] ?? ISet<TypedKey>();
@ -320,7 +349,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
_state = await store(_state.copyWith( _state = await store(_state.copyWith(
childrenByParent: _state.childrenByParent childrenByParent: _state.childrenByParent
.add(parent.toJson(), childrenOfParent.add(child)), .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) { for (final child in childList) {
if (_state.rootRecords.contains(child)) { 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 { } else {
final parent = state.parentByChild[child.toJson()]; final parent = state.parentByChild[child.toJson()];
if (parent == null) { if (parent == null) {
@ -341,12 +373,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
if (children.isEmpty) { if (children.isEmpty) {
state = state.copyWith( state = state.copyWith(
childrenByParent: state.childrenByParent.remove(parent.toJson()), 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 { } else {
state = state.copyWith( state = state.copyWith(
childrenByParent: childrenByParent:
state.childrenByParent.add(parent.toJson(), children), 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 /// Create a root DHTRecord that has no dependent records
Future<DHTRecord> create({ Future<DHTRecord> create({
required String debugName,
VeilidRoutingContext? routingContext, VeilidRoutingContext? routingContext,
TypedKey? parent, TypedKey? parent,
DHTSchema schema = const DHTSchema.dflt(oCnt: 1), DHTSchema schema = const DHTSchema.dflt(oCnt: 1),
@ -371,9 +406,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
final dhtctx = routingContext ?? _routingContext; final dhtctx = routingContext ?? _routingContext;
final openedRecordInfo = await _recordCreateInner( 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, routingContext: dhtctx,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,
sharedDHTRecordData: openedRecordInfo.shared, sharedDHTRecordData: openedRecordInfo.shared,
@ -391,7 +431,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
/// Open a DHTRecord readonly /// Open a DHTRecord readonly
Future<DHTRecord> openRead(TypedKey recordKey, Future<DHTRecord> openRead(TypedKey recordKey,
{VeilidRoutingContext? routingContext, {required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent, TypedKey? parent,
int defaultSubkey = 0, int defaultSubkey = 0,
DHTRecordCrypto? crypto}) async => DHTRecordCrypto? crypto}) async =>
@ -399,9 +440,13 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
final dhtctx = routingContext ?? _routingContext; final dhtctx = routingContext ?? _routingContext;
final openedRecordInfo = await _recordOpenInner( 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, routingContext: dhtctx,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,
sharedDHTRecordData: openedRecordInfo.shared, sharedDHTRecordData: openedRecordInfo.shared,
@ -417,6 +462,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
Future<DHTRecord> openWrite( Future<DHTRecord> openWrite(
TypedKey recordKey, TypedKey recordKey,
KeyPair writer, { KeyPair writer, {
required String debugName,
VeilidRoutingContext? routingContext, VeilidRoutingContext? routingContext,
TypedKey? parent, TypedKey? parent,
int defaultSubkey = 0, int defaultSubkey = 0,
@ -426,12 +472,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
final dhtctx = routingContext ?? _routingContext; final dhtctx = routingContext ?? _routingContext;
final openedRecordInfo = await _recordOpenInner( final openedRecordInfo = await _recordOpenInner(
debugName: debugName,
dhtctx: dhtctx, dhtctx: dhtctx,
recordKey: recordKey, recordKey: recordKey,
parent: parent, parent: parent,
writer: writer); writer: writer);
final rec = DHTRecord( final rec = DHTRecord._(
debugName: debugName,
routingContext: dhtctx, routingContext: dhtctx,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,
writer: writer, writer: writer,
@ -453,6 +501,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
/// parent must be specified. /// parent must be specified.
Future<DHTRecord> openOwned( Future<DHTRecord> openOwned(
OwnedDHTRecordPointer ownedDHTRecordPointer, { OwnedDHTRecordPointer ownedDHTRecordPointer, {
required String debugName,
required TypedKey parent, required TypedKey parent,
VeilidRoutingContext? routingContext, VeilidRoutingContext? routingContext,
int defaultSubkey = 0, int defaultSubkey = 0,
@ -461,6 +510,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
openWrite( openWrite(
ownedDHTRecordPointer.recordKey, ownedDHTRecordPointer.recordKey,
ownedDHTRecordPointer.owner, ownedDHTRecordPointer.owner,
debugName: debugName,
routingContext: routingContext, routingContext: routingContext,
parent: parent, parent: parent,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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