mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-10-01 06:55:46 -04:00
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:
parent
8da1dc7d32
commit
9bb20f4dd2
@ -3,24 +3,13 @@ import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../init.dart';
|
||||
import '../repository/account_repository/account_repository.dart';
|
||||
|
||||
class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
|
||||
ActiveLocalAccountCubit(AccountRepository accountRepository)
|
||||
: _accountRepository = accountRepository,
|
||||
super(null) {
|
||||
super(accountRepository.getActiveLocalAccount()) {
|
||||
// Subscribe to streams
|
||||
_initAccountRepositorySubscription();
|
||||
|
||||
// Initialize when we can
|
||||
Future.delayed(Duration.zero, () async {
|
||||
await eventualInitialized.future;
|
||||
emit(_accountRepository.getActiveLocalAccount());
|
||||
});
|
||||
}
|
||||
|
||||
void _initAccountRepositorySubscription() {
|
||||
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
|
||||
switch (change) {
|
||||
case AccountRepositoryChange.activeLocalAccount:
|
||||
|
@ -3,25 +3,14 @@ import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
|
||||
import '../../init.dart';
|
||||
import '../models/models.dart';
|
||||
import '../repository/account_repository/account_repository.dart';
|
||||
|
||||
class LocalAccountsCubit extends Cubit<IList<LocalAccount>> {
|
||||
LocalAccountsCubit(AccountRepository accountRepository)
|
||||
: _accountRepository = accountRepository,
|
||||
super(IList<LocalAccount>()) {
|
||||
super(accountRepository.getLocalAccounts()) {
|
||||
// Subscribe to streams
|
||||
_initAccountRepositorySubscription();
|
||||
|
||||
// Initialize when we can
|
||||
Future.delayed(Duration.zero, () async {
|
||||
await eventualInitialized.future;
|
||||
emit(_accountRepository.getLocalAccounts());
|
||||
});
|
||||
}
|
||||
|
||||
void _initAccountRepositorySubscription() {
|
||||
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
|
||||
switch (change) {
|
||||
case AccountRepositoryChange.localAccounts:
|
||||
|
@ -3,25 +3,14 @@ import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
|
||||
import '../../init.dart';
|
||||
import '../models/models.dart';
|
||||
import '../repository/account_repository/account_repository.dart';
|
||||
|
||||
class UserLoginsCubit extends Cubit<IList<UserLogin>> {
|
||||
UserLoginsCubit(AccountRepository accountRepository)
|
||||
: _accountRepository = accountRepository,
|
||||
super(IList<UserLogin>()) {
|
||||
super(accountRepository.getUserLogins()) {
|
||||
// Subscribe to streams
|
||||
_initAccountRepositorySubscription();
|
||||
|
||||
// Initialize when we can
|
||||
Future.delayed(Duration.zero, () async {
|
||||
await eventualInitialized.future;
|
||||
emit(_accountRepository.getUserLogins());
|
||||
});
|
||||
}
|
||||
|
||||
void _initAccountRepositorySubscription() {
|
||||
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
|
||||
switch (change) {
|
||||
case AccountRepositoryChange.userLogins:
|
||||
|
@ -202,18 +202,24 @@ class AccountRepository {
|
||||
createAccountCallback: (parent) async {
|
||||
// Make empty contact list
|
||||
log.debug('Creating contacts list');
|
||||
final contactList = await (await DHTShortArray.create(parent: parent))
|
||||
final contactList = await (await DHTShortArray.create(
|
||||
debugName: 'AccountRepository::_newLocalAccount::Contacts',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make empty contact invitation record list
|
||||
log.debug('Creating contact invitation records list');
|
||||
final contactInvitationRecords =
|
||||
await (await DHTShortArray.create(parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
final contactInvitationRecords = await (await DHTShortArray.create(
|
||||
debugName:
|
||||
'AccountRepository::_newLocalAccount::ContactInvitations',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make empty chat record list
|
||||
log.debug('Creating chat records list');
|
||||
final chatRecords = await (await DHTShortArray.create(parent: parent))
|
||||
final chatRecords = await (await DHTShortArray.create(
|
||||
debugName: 'AccountRepository::_newLocalAccount::Chats',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make account object
|
||||
@ -391,6 +397,7 @@ class AccountRepository {
|
||||
final pool = DHTRecordPool.instance;
|
||||
final record = await pool.openOwned(
|
||||
userLogin.accountRecordInfo.accountRecord,
|
||||
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
|
||||
parent: localAccount.identityMaster.identityRecordKey);
|
||||
|
||||
return record;
|
||||
|
112
lib/app.dart
112
lib/app.dart
@ -5,8 +5,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'account_manager/account_manager.dart';
|
||||
import 'init.dart';
|
||||
import 'layout/splash.dart';
|
||||
import 'router/router.dart';
|
||||
import 'settings/settings.dart';
|
||||
import 'tick.dart';
|
||||
@ -23,57 +26,66 @@ class VeilidChatApp extends StatelessWidget {
|
||||
final ThemeData initialThemeData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizationDelegate = LocalizedApp.of(context).delegate;
|
||||
|
||||
return ThemeProvider(
|
||||
initTheme: initialThemeData,
|
||||
builder: (_, theme) => LocalizationProvider(
|
||||
state: LocalizationProvider.of(context).state,
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ConnectionStateCubit>(
|
||||
create: (context) =>
|
||||
ConnectionStateCubit(ProcessorRepository.instance)),
|
||||
BlocProvider<RouterCubit>(
|
||||
create: (context) =>
|
||||
RouterCubit(AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<LocalAccountsCubit>(
|
||||
create: (context) =>
|
||||
LocalAccountsCubit(AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<UserLoginsCubit>(
|
||||
create: (context) =>
|
||||
UserLoginsCubit(AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<ActiveLocalAccountCubit>(
|
||||
create: (context) =>
|
||||
ActiveLocalAccountCubit(AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<PreferencesCubit>(
|
||||
create: (context) =>
|
||||
PreferencesCubit(PreferencesRepository.instance),
|
||||
)
|
||||
],
|
||||
child: BackgroundTicker(
|
||||
builder: (context) => MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: context.watch<RouterCubit>().router(),
|
||||
title: translate('app.title'),
|
||||
theme: theme,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
FormBuilderLocalizations.delegate,
|
||||
localizationDelegate
|
||||
Widget build(BuildContext context) => FutureProvider<VeilidChatGlobalInit?>(
|
||||
initialData: null,
|
||||
create: (context) async => VeilidChatGlobalInit.initialize(),
|
||||
builder: (context, child) {
|
||||
final globalInit = context.watch<VeilidChatGlobalInit?>();
|
||||
if (globalInit == null) {
|
||||
// Splash screen until we're done with init
|
||||
return const Splash();
|
||||
}
|
||||
// Once init is done, we proceed with the app
|
||||
final localizationDelegate = LocalizedApp.of(context).delegate;
|
||||
return ThemeProvider(
|
||||
initTheme: initialThemeData,
|
||||
builder: (_, theme) => LocalizationProvider(
|
||||
state: LocalizationProvider.of(context).state,
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ConnectionStateCubit>(
|
||||
create: (context) => ConnectionStateCubit(
|
||||
ProcessorRepository.instance)),
|
||||
BlocProvider<RouterCubit>(
|
||||
create: (context) =>
|
||||
RouterCubit(AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<LocalAccountsCubit>(
|
||||
create: (context) =>
|
||||
LocalAccountsCubit(AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<UserLoginsCubit>(
|
||||
create: (context) =>
|
||||
UserLoginsCubit(AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<ActiveLocalAccountCubit>(
|
||||
create: (context) => ActiveLocalAccountCubit(
|
||||
AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<PreferencesCubit>(
|
||||
create: (context) =>
|
||||
PreferencesCubit(PreferencesRepository.instance),
|
||||
)
|
||||
],
|
||||
supportedLocales: localizationDelegate.supportedLocales,
|
||||
locale: localizationDelegate.currentLocale,
|
||||
),
|
||||
)),
|
||||
));
|
||||
}
|
||||
child: BackgroundTicker(
|
||||
builder: (context) => MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: context.watch<RouterCubit>().router(),
|
||||
title: translate('app.title'),
|
||||
theme: theme,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
FormBuilderLocalizations.delegate,
|
||||
localizationDelegate
|
||||
],
|
||||
supportedLocales:
|
||||
localizationDelegate.supportedLocales,
|
||||
locale: localizationDelegate.currentLocale,
|
||||
),
|
||||
)),
|
||||
));
|
||||
});
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
|
@ -38,11 +38,13 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
_messagesUpdateQueue = StreamController(),
|
||||
super(const AsyncValue.loading()) {
|
||||
// Async Init
|
||||
Future.delayed(Duration.zero, _init);
|
||||
_initWait.add(_init);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _initWait();
|
||||
|
||||
await _messagesUpdateQueue.close();
|
||||
await _localSubscription?.cancel();
|
||||
await _remoteSubscription?.cancel();
|
||||
@ -89,7 +91,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
_localMessagesCubit = DHTShortArrayCubit(
|
||||
open: () async => DHTShortArray.openWrite(
|
||||
_localMessagesRecordKey, writer,
|
||||
parent: _localConversationRecordKey, crypto: _messagesCrypto),
|
||||
debugName:
|
||||
'SingleContactMessagesCubit::_initLocalMessages::LocalMessages',
|
||||
parent: _localConversationRecordKey,
|
||||
crypto: _messagesCrypto),
|
||||
decodeElement: proto.Message.fromBuffer);
|
||||
_localSubscription =
|
||||
_localMessagesCubit!.stream.listen(_updateLocalMessagesState);
|
||||
@ -100,7 +105,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
Future<void> _initRemoteMessages() async {
|
||||
_remoteMessagesCubit = DHTShortArrayCubit(
|
||||
open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey,
|
||||
parent: _remoteConversationRecordKey, crypto: _messagesCrypto),
|
||||
debugName: 'SingleContactMessagesCubit::_initRemoteMessages::'
|
||||
'RemoteMessages',
|
||||
parent: _remoteConversationRecordKey,
|
||||
crypto: _messagesCrypto),
|
||||
decodeElement: proto.Message.fromBuffer);
|
||||
_remoteSubscription =
|
||||
_remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState);
|
||||
@ -114,6 +122,9 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
|
||||
_reconciledChatMessagesCubit = DHTShortArrayCubit(
|
||||
open: () async => DHTShortArray.openOwned(_reconciledChatRecord,
|
||||
debugName:
|
||||
'SingleContactMessagesCubit::_initReconciledChatMessages::'
|
||||
'ReconciledChat',
|
||||
parent: accountRecordKey),
|
||||
decodeElement: proto.Message.fromBuffer);
|
||||
_reconciledChatSubscription =
|
||||
@ -237,6 +248,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
|
||||
// Force refresh of messages
|
||||
Future<void> refresh() async {
|
||||
await _initWait();
|
||||
|
||||
final lcc = _localMessagesCubit;
|
||||
final rcc = _remoteMessagesCubit;
|
||||
|
||||
@ -249,10 +262,13 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||
}
|
||||
|
||||
Future<void> addMessage({required proto.Message message}) async {
|
||||
await _initWait();
|
||||
|
||||
await _localMessagesCubit!
|
||||
.operateWrite((writer) => writer.tryAddItem(message.writeToBuffer()));
|
||||
}
|
||||
|
||||
final WaitSet _initWait = WaitSet();
|
||||
final ActiveAccountInfo _activeAccountInfo;
|
||||
final TypedKey _remoteIdentityPublicKey;
|
||||
final TypedKey _localConversationRecordKey;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:bloc_tools/bloc_tools.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import 'cubits.dart';
|
||||
|
||||
@immutable
|
||||
class ActiveConversationState extends Equatable {
|
||||
@ -39,9 +39,7 @@ typedef ActiveConversationsBlocMapState
|
||||
// archived chats or contacts that are not actively in a chat.
|
||||
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
AsyncValue<ActiveConversationState>, ActiveConversationCubit>
|
||||
with
|
||||
StateFollower<BlocBusyState<AsyncValue<IList<proto.Chat>>>, TypedKey,
|
||||
proto.Chat> {
|
||||
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
|
||||
ActiveConversationsBlocMapCubit(
|
||||
{required ActiveAccountInfo activeAccountInfo,
|
||||
required ContactListCubit contactListCubit})
|
||||
@ -77,18 +75,6 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
|
||||
/// StateFollower /////////////////////////
|
||||
|
||||
@override
|
||||
IMap<TypedKey, proto.Chat> getStateMap(
|
||||
BlocBusyState<AsyncValue<IList<proto.Chat>>> state) {
|
||||
final stateValue = state.state.data?.value;
|
||||
if (stateValue == null) {
|
||||
return IMap();
|
||||
}
|
||||
return IMap.fromIterable(stateValue,
|
||||
keyMapper: (e) => e.remoteConversationRecordKey.toVeilid(),
|
||||
valueMapper: (e) => e);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeFromState(TypedKey key) => remove(key);
|
||||
|
||||
|
@ -18,7 +18,7 @@ import 'chat_list_cubit.dart';
|
||||
class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
AsyncValue<IList<proto.Message>>, SingleContactMessagesCubit>
|
||||
with
|
||||
StateFollower<ActiveConversationsBlocMapState, TypedKey,
|
||||
StateMapFollower<ActiveConversationsBlocMapState, TypedKey,
|
||||
AsyncValue<ActiveConversationState>> {
|
||||
ActiveSingleContactChatBlocMapCubit(
|
||||
{required ActiveAccountInfo activeAccountInfo,
|
||||
@ -49,11 +49,6 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
|
||||
/// StateFollower /////////////////////////
|
||||
|
||||
@override
|
||||
IMap<TypedKey, AsyncValue<ActiveConversationState>> getStateMap(
|
||||
ActiveConversationsBlocMapState state) =>
|
||||
state;
|
||||
|
||||
@override
|
||||
Future<void> removeFromState(TypedKey key) => remove(key);
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:bloc_tools/bloc_tools.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
@ -11,8 +14,10 @@ import '../../tools/tools.dart';
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Mutable state for per-account chat list
|
||||
typedef ChatListCubitState = BlocBusyState<AsyncValue<IList<proto.Chat>>>;
|
||||
|
||||
class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
|
||||
class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
|
||||
with StateMapFollowable<ChatListCubitState, TypedKey, proto.Chat> {
|
||||
ChatListCubit({
|
||||
required ActiveAccountInfo activeAccountInfo,
|
||||
required proto.Account account,
|
||||
@ -30,7 +35,7 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
|
||||
final chatListRecordKey = account.chatList.toVeilid();
|
||||
|
||||
final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey,
|
||||
parent: accountRecordKey);
|
||||
debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey);
|
||||
|
||||
return dhtRecord;
|
||||
}
|
||||
@ -61,9 +66,11 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
|
||||
.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
// Make a record that can store the reconciled version of the chat
|
||||
final reconciledChatRecord =
|
||||
await (await DHTShortArray.create(parent: accountRecordKey))
|
||||
.scope((r) async => r.recordPointer);
|
||||
final reconciledChatRecord = await (await DHTShortArray.create(
|
||||
debugName:
|
||||
'ChatListCubit::getOrCreateChatSingleContact::ReconciledChat',
|
||||
parent: accountRecordKey))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Create conversation type Chat
|
||||
final chat = proto.Chat()
|
||||
@ -86,26 +93,30 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
|
||||
|
||||
// Remove Chat from account's list
|
||||
// if this fails, don't keep retrying, user can try again later
|
||||
final (deletedItem, success) = await operateWrite((writer) async {
|
||||
if (activeChatCubit.state == remoteConversationRecordKey) {
|
||||
activeChatCubit.setActiveChat(null);
|
||||
}
|
||||
for (var i = 0; i < writer.length; i++) {
|
||||
final cbuf = await writer.getItem(i);
|
||||
if (cbuf == null) {
|
||||
throw Exception('Failed to get chat');
|
||||
}
|
||||
final c = proto.Chat.fromBuffer(cbuf);
|
||||
if (c.remoteConversationRecordKey == remoteConversationKey) {
|
||||
// Found the right chat
|
||||
if (await writer.tryRemoveItem(i) != null) {
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
final (deletedItem, success) =
|
||||
// Ensure followers get their changes before we return
|
||||
await syncFollowers(() => operateWrite((writer) async {
|
||||
if (activeChatCubit.state == remoteConversationRecordKey) {
|
||||
activeChatCubit.setActiveChat(null);
|
||||
}
|
||||
for (var i = 0; i < writer.length; i++) {
|
||||
final cbuf = await writer.getItem(i);
|
||||
if (cbuf == null) {
|
||||
throw Exception('Failed to get chat');
|
||||
}
|
||||
final c = proto.Chat.fromBuffer(cbuf);
|
||||
if (c.remoteConversationRecordKey == remoteConversationKey) {
|
||||
// Found the right chat
|
||||
if (await writer.tryRemoveItem(i) != null) {
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}));
|
||||
// Since followers are synced, we can safetly remove the reconciled
|
||||
// chat record now
|
||||
if (success && deletedItem != null) {
|
||||
try {
|
||||
await DHTRecordPool.instance
|
||||
@ -116,6 +127,18 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
|
||||
}
|
||||
}
|
||||
|
||||
/// StateMapFollowable /////////////////////////
|
||||
@override
|
||||
IMap<TypedKey, proto.Chat> getStateMap(ChatListCubitState state) {
|
||||
final stateValue = state.state.data?.value;
|
||||
if (stateValue == null) {
|
||||
return IMap();
|
||||
}
|
||||
return IMap.fromIterable(stateValue,
|
||||
keyMapper: (e) => e.remoteConversationRecordKey.toVeilid(),
|
||||
valueMapper: (e) => e);
|
||||
}
|
||||
|
||||
final ActiveChatCubit activeChatCubit;
|
||||
final ActiveAccountInfo _activeAccountInfo;
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:bloc_tools/bloc_tools.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
@ -23,11 +26,16 @@ typedef GetEncryptionKeyCallback = Future<SecretKey?> Function(
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
typedef ContactInvitiationListState
|
||||
= BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>>;
|
||||
//////////////////////////////////////////////////
|
||||
// Mutable state for per-account contact invitations
|
||||
|
||||
class ContactInvitationListCubit
|
||||
extends DHTShortArrayCubit<proto.ContactInvitationRecord> {
|
||||
extends DHTShortArrayCubit<proto.ContactInvitationRecord>
|
||||
with
|
||||
StateMapFollowable<ContactInvitiationListState, TypedKey,
|
||||
proto.ContactInvitationRecord> {
|
||||
ContactInvitationListCubit({
|
||||
required ActiveAccountInfo activeAccountInfo,
|
||||
required proto.Account account,
|
||||
@ -47,6 +55,7 @@ class ContactInvitationListCubit
|
||||
|
||||
final dhtRecord = await DHTShortArray.openOwned(
|
||||
contactInvitationListRecordPointer,
|
||||
debugName: 'ContactInvitationListCubit::_open::ContactInvitationList',
|
||||
parent: accountRecordKey);
|
||||
|
||||
return dhtRecord;
|
||||
@ -78,6 +87,8 @@ class ContactInvitationListCubit
|
||||
// identity key
|
||||
late final Uint8List signedContactInvitationBytes;
|
||||
await (await pool.create(
|
||||
debugName: 'ContactInvitationListCubit::createInvitation::'
|
||||
'LocalConversation',
|
||||
parent: _activeAccountInfo.accountRecordKey,
|
||||
schema: DHTSchema.smpl(oCnt: 0, members: [
|
||||
DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1)
|
||||
@ -105,6 +116,8 @@ class ContactInvitationListCubit
|
||||
// Subkey 0 is the ContactRequest from the initiator
|
||||
// Subkey 1 will contain the invitation response accept/reject eventually
|
||||
await (await pool.create(
|
||||
debugName: 'ContactInvitationListCubit::createInvitation::'
|
||||
'ContactRequestInbox',
|
||||
parent: _activeAccountInfo.accountRecordKey,
|
||||
schema: DHTSchema.smpl(oCnt: 1, members: [
|
||||
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
|
||||
@ -180,6 +193,8 @@ class ContactInvitationListCubit
|
||||
// Delete the contact request inbox
|
||||
final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid();
|
||||
await (await pool.openOwned(contactRequestInbox,
|
||||
debugName: 'ContactInvitationListCubit::deleteInvitation::'
|
||||
'ContactRequestInbox',
|
||||
parent: accountRecordKey))
|
||||
.scope((contactRequestInbox) async {
|
||||
// Wipe out old invitation so it shows up as invalid
|
||||
@ -229,6 +244,8 @@ class ContactInvitationListCubit
|
||||
-1;
|
||||
|
||||
await (await pool.openRead(contactRequestInboxKey,
|
||||
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
||||
'ContactRequestInbox',
|
||||
parent: _activeAccountInfo.accountRecordKey))
|
||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||
//
|
||||
@ -282,6 +299,19 @@ class ContactInvitationListCubit
|
||||
return out;
|
||||
}
|
||||
|
||||
/// StateMapFollowable /////////////////////////
|
||||
@override
|
||||
IMap<TypedKey, proto.ContactInvitationRecord> getStateMap(
|
||||
ContactInvitiationListState state) {
|
||||
final stateValue = state.state.data?.value;
|
||||
if (stateValue == null) {
|
||||
return IMap();
|
||||
}
|
||||
return IMap.fromIterable(stateValue,
|
||||
keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(),
|
||||
valueMapper: (e) => e);
|
||||
}
|
||||
|
||||
//
|
||||
final ActiveAccountInfo _activeAccountInfo;
|
||||
final proto.Account _account;
|
||||
|
@ -33,6 +33,8 @@ class ContactRequestInboxCubit
|
||||
final writer = TypedKeyPair(
|
||||
kind: recordKey.kind, key: writerKey, secret: writerSecret);
|
||||
return pool.openRead(recordKey,
|
||||
debugName: 'ContactRequestInboxCubit::_open::'
|
||||
'ContactRequestInbox',
|
||||
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
|
||||
parent: accountRecordKey,
|
||||
defaultSubkey: 1);
|
||||
|
@ -16,7 +16,7 @@ typedef WaitingInvitationsBlocMapState
|
||||
class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
AsyncValue<InvitationStatus>, WaitingInvitationCubit>
|
||||
with
|
||||
StateFollower<
|
||||
StateMapFollower<
|
||||
BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>>,
|
||||
TypedKey,
|
||||
proto.ContactInvitationRecord> {
|
||||
@ -37,17 +37,6 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
contactInvitationRecord: contactInvitationRecord)));
|
||||
|
||||
/// StateFollower /////////////////////////
|
||||
@override
|
||||
IMap<TypedKey, proto.ContactInvitationRecord> getStateMap(
|
||||
BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>> state) {
|
||||
final stateValue = state.state.data?.value;
|
||||
if (stateValue == null) {
|
||||
return IMap();
|
||||
}
|
||||
return IMap.fromIterable(stateValue,
|
||||
keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(),
|
||||
valueMapper: (e) => e);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeFromState(TypedKey key) => remove(key);
|
||||
|
@ -38,6 +38,8 @@ class ValidContactInvitation {
|
||||
final accountRecordKey = _activeAccountInfo.accountRecordKey;
|
||||
|
||||
return (await pool.openWrite(_contactRequestInboxKey, _writer,
|
||||
debugName: 'ValidContactInvitation::accept::'
|
||||
'ContactRequestInbox',
|
||||
parent: accountRecordKey))
|
||||
// ignore: prefer_expression_function_bodies
|
||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||
@ -103,6 +105,8 @@ class ValidContactInvitation {
|
||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
return (await pool.openWrite(_contactRequestInboxKey, _writer,
|
||||
debugName: 'ValidContactInvitation::reject::'
|
||||
'ContactRequestInbox',
|
||||
parent: accountRecordKey))
|
||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||
final cs =
|
||||
|
@ -6,6 +6,7 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
import 'conversation_cubit.dart';
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Mutable state for per-account contacts
|
||||
@ -14,7 +15,8 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||
ContactListCubit({
|
||||
required ActiveAccountInfo activeAccountInfo,
|
||||
required proto.Account account,
|
||||
}) : super(
|
||||
}) : _activeAccountInfo = activeAccountInfo,
|
||||
super(
|
||||
open: () => _open(activeAccountInfo, account),
|
||||
decodeElement: proto.Contact.fromBuffer);
|
||||
|
||||
@ -26,6 +28,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||
final contactListRecordKey = account.contactList.toVeilid();
|
||||
|
||||
final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey,
|
||||
debugName: 'ContactListCubit::_open::ContactList',
|
||||
parent: accountRecordKey);
|
||||
|
||||
return dhtRecord;
|
||||
@ -60,9 +63,10 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||
}
|
||||
|
||||
Future<void> deleteContact({required proto.Contact contact}) async {
|
||||
final pool = DHTRecordPool.instance;
|
||||
final localConversationKey = contact.localConversationRecordKey.toVeilid();
|
||||
final remoteConversationKey =
|
||||
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
|
||||
final localConversationRecordKey =
|
||||
contact.localConversationRecordKey.toVeilid();
|
||||
final remoteConversationRecordKey =
|
||||
contact.remoteConversationRecordKey.toVeilid();
|
||||
|
||||
// Remove Contact from account's list
|
||||
@ -85,17 +89,21 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||
|
||||
if (success && deletedItem != null) {
|
||||
try {
|
||||
await pool.delete(localConversationKey);
|
||||
// Make a conversation cubit to manipulate the conversation
|
||||
final conversationCubit = ConversationCubit(
|
||||
activeAccountInfo: _activeAccountInfo,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||
localConversationRecordKey: localConversationRecordKey,
|
||||
remoteConversationRecordKey: remoteConversationRecordKey,
|
||||
);
|
||||
|
||||
// Delete the local and remote conversation records
|
||||
await conversationCubit.delete();
|
||||
} on Exception catch (e) {
|
||||
log.debug('error removing local conversation record key: $e', e);
|
||||
}
|
||||
try {
|
||||
if (localConversationKey != remoteConversationKey) {
|
||||
await pool.delete(remoteConversationKey);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
log.debug('error removing remote conversation record key: $e', e);
|
||||
log.debug('error deleting conversation records: $e', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final ActiveAccountInfo _activeAccountInfo;
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
|
||||
@immutable
|
||||
class ConversationState extends Equatable {
|
||||
@ -36,11 +37,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
_localConversationRecordKey = localConversationRecordKey,
|
||||
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
||||
_remoteConversationRecordKey = remoteConversationRecordKey,
|
||||
_incrementalState = const ConversationState(
|
||||
localConversation: null, remoteConversation: null),
|
||||
super(const AsyncValue.loading()) {
|
||||
if (_localConversationRecordKey != null) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
_initWait.add(() async {
|
||||
await _setLocalConversation(() async {
|
||||
final accountRecordKey = _activeAccountInfo
|
||||
.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
@ -51,14 +50,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
final writer = _activeAccountInfo.conversationWriter;
|
||||
final record = await pool.openWrite(
|
||||
_localConversationRecordKey!, writer,
|
||||
parent: accountRecordKey, crypto: crypto);
|
||||
debugName: 'ConversationCubit::LocalConversation',
|
||||
parent: accountRecordKey,
|
||||
crypto: crypto);
|
||||
return record;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (_remoteConversationRecordKey != null) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
_initWait.add(() async {
|
||||
await _setRemoteConversation(() async {
|
||||
final accountRecordKey = _activeAccountInfo
|
||||
.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
@ -67,7 +68,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
final pool = DHTRecordPool.instance;
|
||||
final crypto = await _cachedConversationCrypto();
|
||||
final record = await pool.openRead(_remoteConversationRecordKey,
|
||||
parent: accountRecordKey, crypto: crypto);
|
||||
debugName: 'ConversationCubit::RemoteConversation',
|
||||
parent: accountRecordKey,
|
||||
crypto: crypto);
|
||||
return record;
|
||||
});
|
||||
});
|
||||
@ -76,6 +79,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _initWait();
|
||||
await _localSubscription?.cancel();
|
||||
await _remoteSubscription?.cancel();
|
||||
await _localConversationCubit?.close();
|
||||
@ -84,7 +88,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
await super.close();
|
||||
}
|
||||
|
||||
void updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
|
||||
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
|
||||
final newState = avconv.when(
|
||||
data: (conv) {
|
||||
_incrementalState = ConversationState(
|
||||
@ -106,7 +110,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
emit(newState);
|
||||
}
|
||||
|
||||
void updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
|
||||
void _updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
|
||||
final newState = avconv.when(
|
||||
data: (conv) {
|
||||
_incrementalState = ConversationState(
|
||||
@ -135,7 +139,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
_localConversationCubit = DefaultDHTRecordCubit(
|
||||
open: open, decodeState: proto.Conversation.fromBuffer);
|
||||
_localSubscription =
|
||||
_localConversationCubit!.stream.listen(updateLocalConversationState);
|
||||
_localConversationCubit!.stream.listen(_updateLocalConversationState);
|
||||
}
|
||||
|
||||
// Open remote converation key
|
||||
@ -145,7 +149,57 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
_remoteConversationCubit = DefaultDHTRecordCubit(
|
||||
open: open, decodeState: proto.Conversation.fromBuffer);
|
||||
_remoteSubscription =
|
||||
_remoteConversationCubit!.stream.listen(updateRemoteConversationState);
|
||||
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
|
||||
}
|
||||
|
||||
Future<bool> delete() async {
|
||||
final pool = DHTRecordPool.instance;
|
||||
|
||||
await _initWait();
|
||||
final localConversationCubit = _localConversationCubit;
|
||||
final remoteConversationCubit = _remoteConversationCubit;
|
||||
|
||||
final deleteSet = DelayedWaitSet();
|
||||
|
||||
if (localConversationCubit != null) {
|
||||
final data = localConversationCubit.state.data;
|
||||
if (data == null) {
|
||||
log.warning('could not delete local conversation');
|
||||
return false;
|
||||
}
|
||||
|
||||
deleteSet.add(() async {
|
||||
_localConversationCubit = null;
|
||||
await localConversationCubit.close();
|
||||
final conversation = data.value;
|
||||
final messagesKey = conversation.messages.toVeilid();
|
||||
await pool.delete(messagesKey);
|
||||
await pool.delete(_localConversationRecordKey!);
|
||||
_localConversationRecordKey = null;
|
||||
});
|
||||
}
|
||||
|
||||
if (remoteConversationCubit != null) {
|
||||
final data = remoteConversationCubit.state.data;
|
||||
if (data == null) {
|
||||
log.warning('could not delete remote conversation');
|
||||
return false;
|
||||
}
|
||||
|
||||
deleteSet.add(() async {
|
||||
_remoteConversationCubit = null;
|
||||
await remoteConversationCubit.close();
|
||||
final conversation = data.value;
|
||||
final messagesKey = conversation.messages.toVeilid();
|
||||
await pool.delete(messagesKey);
|
||||
await pool.delete(_remoteConversationRecordKey!);
|
||||
});
|
||||
}
|
||||
|
||||
// Commit the delete futures
|
||||
await deleteSet();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Initialize a local conversation
|
||||
@ -174,23 +228,25 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
if (existingConversationRecordKey != null) {
|
||||
localConversationRecord = await pool.openWrite(
|
||||
existingConversationRecordKey, writer,
|
||||
parent: accountRecordKey, crypto: crypto);
|
||||
debugName:
|
||||
'ConversationCubit::initLocalConversation::LocalConversation',
|
||||
parent: accountRecordKey,
|
||||
crypto: crypto);
|
||||
} else {
|
||||
final localConversationRecordCreate = await pool.create(
|
||||
localConversationRecord = await pool.create(
|
||||
debugName:
|
||||
'ConversationCubit::initLocalConversation::LocalConversation',
|
||||
parent: accountRecordKey,
|
||||
crypto: crypto,
|
||||
writer: writer,
|
||||
schema: DHTSchema.smpl(
|
||||
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
|
||||
await localConversationRecordCreate.close();
|
||||
localConversationRecord = await pool.openWrite(
|
||||
localConversationRecordCreate.key, writer,
|
||||
parent: accountRecordKey, crypto: crypto);
|
||||
}
|
||||
final out = localConversationRecord
|
||||
// ignore: prefer_expression_function_bodies
|
||||
.deleteScope((localConversation) async {
|
||||
// Make messages log
|
||||
return initLocalMessages(
|
||||
return _initLocalMessages(
|
||||
activeAccountInfo: _activeAccountInfo,
|
||||
remoteIdentityPublicKey: _remoteIdentityPublicKey,
|
||||
localConversationKey: localConversation.key,
|
||||
@ -211,7 +267,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
final out = await callback(localConversation);
|
||||
|
||||
// Upon success emit the local conversation record to the state
|
||||
updateLocalConversationState(AsyncValue.data(conversation));
|
||||
_updateLocalConversationState(AsyncValue.data(conversation));
|
||||
|
||||
return out;
|
||||
});
|
||||
@ -225,7 +281,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
}
|
||||
|
||||
// Initialize local messages
|
||||
Future<T> initLocalMessages<T>({
|
||||
Future<T> _initLocalMessages<T>({
|
||||
required ActiveAccountInfo activeAccountInfo,
|
||||
required TypedKey remoteIdentityPublicKey,
|
||||
required TypedKey localConversationKey,
|
||||
@ -236,12 +292,17 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
final writer = activeAccountInfo.conversationWriter;
|
||||
|
||||
return (await DHTShortArray.create(
|
||||
parent: localConversationKey, crypto: crypto, smplWriter: writer))
|
||||
debugName: 'ConversationCubit::initLocalMessages::LocalMessages',
|
||||
parent: localConversationKey,
|
||||
crypto: crypto,
|
||||
smplWriter: writer))
|
||||
.deleteScope((messages) async => await callback(messages));
|
||||
}
|
||||
|
||||
// Force refresh of conversation keys
|
||||
Future<void> refresh() async {
|
||||
await _initWait();
|
||||
|
||||
final lcc = _localConversationCubit;
|
||||
final rcc = _remoteConversationCubit;
|
||||
|
||||
@ -260,7 +321,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
.tryWriteProtobuf(proto.Conversation.fromBuffer, conversation);
|
||||
|
||||
if (update != null) {
|
||||
updateLocalConversationState(AsyncValue.data(conversation));
|
||||
_updateLocalConversationState(AsyncValue.data(conversation));
|
||||
}
|
||||
|
||||
return update;
|
||||
@ -286,7 +347,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
|
||||
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
|
||||
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
|
||||
ConversationState _incrementalState;
|
||||
ConversationState _incrementalState = const ConversationState(
|
||||
localConversation: null, remoteConversation: null);
|
||||
//
|
||||
DHTRecordCrypto? _conversationCrypto;
|
||||
final WaitSet _initWait = WaitSet();
|
||||
}
|
||||
|
@ -8,34 +8,38 @@ import 'app.dart';
|
||||
import 'tools/tools.dart';
|
||||
import 'veilid_processor/veilid_processor.dart';
|
||||
|
||||
final Completer<void> eventualInitialized = Completer<void>();
|
||||
class VeilidChatGlobalInit {
|
||||
VeilidChatGlobalInit._();
|
||||
|
||||
// Initialize Veilid
|
||||
Future<void> initializeVeilid() async {
|
||||
// Init Veilid
|
||||
Veilid.instance.initializeVeilidCore(
|
||||
getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
|
||||
// Initialize Veilid
|
||||
Future<void> _initializeVeilid() async {
|
||||
// Init Veilid
|
||||
Veilid.instance.initializeVeilidCore(
|
||||
getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
|
||||
|
||||
// Veilid logging
|
||||
initVeilidLog(kDebugMode);
|
||||
// Veilid logging
|
||||
initVeilidLog(kDebugMode);
|
||||
|
||||
// Startup Veilid
|
||||
await ProcessorRepository.instance.startup();
|
||||
// Startup Veilid
|
||||
await ProcessorRepository.instance.startup();
|
||||
|
||||
// DHT Record Pool
|
||||
await DHTRecordPool.init();
|
||||
}
|
||||
// DHT Record Pool
|
||||
await DHTRecordPool.init();
|
||||
}
|
||||
|
||||
// Initialize repositories
|
||||
Future<void> initializeRepositories() async {
|
||||
await AccountRepository.instance.init();
|
||||
}
|
||||
Future<void> _initializeRepositories() async {
|
||||
await AccountRepository.instance.init();
|
||||
}
|
||||
|
||||
Future<void> initializeVeilidChat() async {
|
||||
log.info('Initializing Veilid');
|
||||
await initializeVeilid();
|
||||
log.info('Initializing Repositories');
|
||||
await initializeRepositories();
|
||||
static Future<VeilidChatGlobalInit> initialize() async {
|
||||
final veilidChatGlobalInit = VeilidChatGlobalInit._();
|
||||
|
||||
eventualInitialized.complete();
|
||||
log.info('Initializing Veilid');
|
||||
await veilidChatGlobalInit._initializeVeilid();
|
||||
log.info('Initializing Repositories');
|
||||
await veilidChatGlobalInit._initializeRepositories();
|
||||
|
||||
return veilidChatGlobalInit;
|
||||
}
|
||||
}
|
||||
|
@ -137,17 +137,17 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
|
||||
create: (context) => ActiveConversationsBlocMapCubit(
|
||||
activeAccountInfo: widget.activeAccountInfo,
|
||||
contactListCubit: context.read<ContactListCubit>())
|
||||
..followBloc(context.read<ChatListCubit>())),
|
||||
..follow(context.read<ChatListCubit>())),
|
||||
BlocProvider(
|
||||
create: (context) => ActiveSingleContactChatBlocMapCubit(
|
||||
activeAccountInfo: widget.activeAccountInfo,
|
||||
contactListCubit: context.read<ContactListCubit>(),
|
||||
chatListCubit: context.read<ChatListCubit>())
|
||||
..followBloc(context.read<ActiveConversationsBlocMapCubit>())),
|
||||
..follow(context.read<ActiveConversationsBlocMapCubit>())),
|
||||
BlocProvider(
|
||||
create: (context) => WaitingInvitationsBlocMapCubit(
|
||||
activeAccountInfo: widget.activeAccountInfo, account: account)
|
||||
..followBloc(context.read<ContactInvitationListCubit>()))
|
||||
..follow(context.read<ContactInvitationListCubit>()))
|
||||
],
|
||||
child: MultiBlocListener(listeners: [
|
||||
BlocListener<WaitingInvitationsBlocMapCubit,
|
||||
|
@ -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',
|
||||
))
|
||||
]))),
|
||||
));
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export 'default_app_bar.dart';
|
||||
export 'home/home.dart';
|
||||
export 'home/home_account_ready/main_pager/main_pager.dart';
|
||||
export 'index.dart';
|
||||
export 'splash.dart';
|
||||
|
53
lib/layout/splash.dart
Normal file
53
lib/layout/splash.dart
Normal 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',
|
||||
))
|
||||
]))),
|
||||
);
|
||||
}
|
@ -8,7 +8,6 @@ import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
|
||||
import 'app.dart';
|
||||
import 'init.dart';
|
||||
import 'settings/preferences_repository.dart';
|
||||
import 'theme/theme.dart';
|
||||
import 'tools/tools.dart';
|
||||
@ -45,9 +44,6 @@ void main() async {
|
||||
fallbackLocale: 'en_US', supportedLocales: ['en_US']);
|
||||
await initializeDateFormatting();
|
||||
|
||||
// Start up Veilid and Veilid processor in the background
|
||||
unawaited(initializeVeilidChat());
|
||||
|
||||
// Run the app
|
||||
// Hot reloads will only restart this part, not Veilid
|
||||
runApp(LocalizedApp(localizationDelegate,
|
||||
|
@ -9,7 +9,6 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
import '../../../account_manager/account_manager.dart';
|
||||
import '../../init.dart';
|
||||
import '../../layout/layout.dart';
|
||||
import '../../settings/settings.dart';
|
||||
import '../../tools/tools.dart';
|
||||
@ -24,19 +23,10 @@ final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
|
||||
|
||||
class RouterCubit extends Cubit<RouterState> {
|
||||
RouterCubit(AccountRepository accountRepository)
|
||||
: super(const RouterState(
|
||||
isInitialized: false,
|
||||
hasAnyAccount: false,
|
||||
: super(RouterState(
|
||||
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty,
|
||||
hasActiveChat: false,
|
||||
)) {
|
||||
// Watch for changes that the router will care about
|
||||
Future.delayed(Duration.zero, () async {
|
||||
await eventualInitialized.future;
|
||||
emit(state.copyWith(
|
||||
isInitialized: true,
|
||||
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty));
|
||||
});
|
||||
|
||||
// Subscribe to repository streams
|
||||
_accountRepositorySubscription = accountRepository.stream.listen((event) {
|
||||
switch (event) {
|
||||
@ -63,10 +53,6 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
|
||||
/// Our application routes
|
||||
List<RouteBase> get routes => [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const IndexPage(),
|
||||
),
|
||||
ShellRoute(
|
||||
navigatorKey: _homeNavKey,
|
||||
builder: (context, state, child) => HomeShell(
|
||||
@ -75,11 +61,11 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
HomeAccountReadyShell(context: context, child: child))),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
path: '/',
|
||||
builder: (context, state) => const HomeAccountReadyMain(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/home/chat',
|
||||
path: '/chat',
|
||||
builder: (context, state) => const HomeAccountReadyChat(),
|
||||
),
|
||||
],
|
||||
@ -103,17 +89,9 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
// No matter where we are, if there's not
|
||||
|
||||
switch (goRouterState.matchedLocation) {
|
||||
case '/':
|
||||
|
||||
// Wait for initialization to complete
|
||||
if (!eventualInitialized.isCompleted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.hasAnyAccount ? '/home' : '/new_account';
|
||||
case '/new_account':
|
||||
return state.hasAnyAccount ? '/home' : null;
|
||||
case '/home':
|
||||
return state.hasAnyAccount ? '/' : null;
|
||||
case '/':
|
||||
if (!state.hasAnyAccount) {
|
||||
return '/new_account';
|
||||
}
|
||||
@ -123,11 +101,11 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
tabletLandscape: false,
|
||||
desktop: false)) {
|
||||
if (state.hasActiveChat) {
|
||||
return '/home/chat';
|
||||
return '/chat';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
case '/home/chat':
|
||||
case '/chat':
|
||||
if (!state.hasAnyAccount) {
|
||||
return '/new_account';
|
||||
}
|
||||
@ -137,10 +115,10 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
tabletLandscape: false,
|
||||
desktop: false)) {
|
||||
if (!state.hasActiveChat) {
|
||||
return '/home';
|
||||
return '/';
|
||||
}
|
||||
} else {
|
||||
return '/home';
|
||||
return '/';
|
||||
}
|
||||
return null;
|
||||
case '/settings':
|
||||
|
@ -20,7 +20,6 @@ RouterState _$RouterStateFromJson(Map<String, dynamic> json) {
|
||||
|
||||
/// @nodoc
|
||||
mixin _$RouterState {
|
||||
bool get isInitialized => throw _privateConstructorUsedError;
|
||||
bool get hasAnyAccount => throw _privateConstructorUsedError;
|
||||
bool get hasActiveChat => throw _privateConstructorUsedError;
|
||||
|
||||
@ -36,7 +35,7 @@ abstract class $RouterStateCopyWith<$Res> {
|
||||
RouterState value, $Res Function(RouterState) then) =
|
||||
_$RouterStateCopyWithImpl<$Res, RouterState>;
|
||||
@useResult
|
||||
$Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat});
|
||||
$Res call({bool hasAnyAccount, bool hasActiveChat});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -52,15 +51,10 @@ class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? isInitialized = null,
|
||||
Object? hasAnyAccount = null,
|
||||
Object? hasActiveChat = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
isInitialized: null == isInitialized
|
||||
? _value.isInitialized
|
||||
: isInitialized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
hasAnyAccount: null == hasAnyAccount
|
||||
? _value.hasAnyAccount
|
||||
: hasAnyAccount // ignore: cast_nullable_to_non_nullable
|
||||
@ -81,7 +75,7 @@ abstract class _$$RouterStateImplCopyWith<$Res>
|
||||
__$$RouterStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat});
|
||||
$Res call({bool hasAnyAccount, bool hasActiveChat});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -95,15 +89,10 @@ class __$$RouterStateImplCopyWithImpl<$Res>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? isInitialized = null,
|
||||
Object? hasAnyAccount = null,
|
||||
Object? hasActiveChat = null,
|
||||
}) {
|
||||
return _then(_$RouterStateImpl(
|
||||
isInitialized: null == isInitialized
|
||||
? _value.isInitialized
|
||||
: isInitialized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
hasAnyAccount: null == hasAnyAccount
|
||||
? _value.hasAnyAccount
|
||||
: hasAnyAccount // ignore: cast_nullable_to_non_nullable
|
||||
@ -120,15 +109,11 @@ class __$$RouterStateImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||
const _$RouterStateImpl(
|
||||
{required this.isInitialized,
|
||||
required this.hasAnyAccount,
|
||||
required this.hasActiveChat});
|
||||
{required this.hasAnyAccount, required this.hasActiveChat});
|
||||
|
||||
factory _$RouterStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$RouterStateImplFromJson(json);
|
||||
|
||||
@override
|
||||
final bool isInitialized;
|
||||
@override
|
||||
final bool hasAnyAccount;
|
||||
@override
|
||||
@ -136,7 +121,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'RouterState(isInitialized: $isInitialized, hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)';
|
||||
return 'RouterState(hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -144,7 +129,6 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'RouterState'))
|
||||
..add(DiagnosticsProperty('isInitialized', isInitialized))
|
||||
..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount))
|
||||
..add(DiagnosticsProperty('hasActiveChat', hasActiveChat));
|
||||
}
|
||||
@ -154,8 +138,6 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$RouterStateImpl &&
|
||||
(identical(other.isInitialized, isInitialized) ||
|
||||
other.isInitialized == isInitialized) &&
|
||||
(identical(other.hasAnyAccount, hasAnyAccount) ||
|
||||
other.hasAnyAccount == hasAnyAccount) &&
|
||||
(identical(other.hasActiveChat, hasActiveChat) ||
|
||||
@ -164,8 +146,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, isInitialized, hasAnyAccount, hasActiveChat);
|
||||
int get hashCode => Object.hash(runtimeType, hasAnyAccount, hasActiveChat);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -183,15 +164,12 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||
|
||||
abstract class _RouterState implements RouterState {
|
||||
const factory _RouterState(
|
||||
{required final bool isInitialized,
|
||||
required final bool hasAnyAccount,
|
||||
{required final bool hasAnyAccount,
|
||||
required final bool hasActiveChat}) = _$RouterStateImpl;
|
||||
|
||||
factory _RouterState.fromJson(Map<String, dynamic> json) =
|
||||
_$RouterStateImpl.fromJson;
|
||||
|
||||
@override
|
||||
bool get isInitialized;
|
||||
@override
|
||||
bool get hasAnyAccount;
|
||||
@override
|
||||
|
@ -8,14 +8,12 @@ part of 'router_cubit.dart';
|
||||
|
||||
_$RouterStateImpl _$$RouterStateImplFromJson(Map<String, dynamic> json) =>
|
||||
_$RouterStateImpl(
|
||||
isInitialized: json['is_initialized'] as bool,
|
||||
hasAnyAccount: json['has_any_account'] as bool,
|
||||
hasActiveChat: json['has_active_chat'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$RouterStateImplToJson(_$RouterStateImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'is_initialized': instance.isInitialized,
|
||||
'has_any_account': instance.hasAnyAccount,
|
||||
'has_active_chat': instance.hasActiveChat,
|
||||
};
|
||||
|
@ -3,8 +3,7 @@ part of 'router_cubit.dart';
|
||||
@freezed
|
||||
class RouterState with _$RouterState {
|
||||
const factory RouterState(
|
||||
{required bool isInitialized,
|
||||
required bool hasAnyAccount,
|
||||
{required bool hasAnyAccount,
|
||||
required bool hasActiveChat}) = _RouterState;
|
||||
|
||||
factory RouterState.fromJson(dynamic json) =>
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import 'init.dart';
|
||||
import 'veilid_processor/veilid_processor.dart';
|
||||
|
||||
class BackgroundTicker extends StatefulWidget {
|
||||
@ -53,10 +52,6 @@ class BackgroundTickerState extends State<BackgroundTicker> {
|
||||
}
|
||||
|
||||
Future<void> _onTick() async {
|
||||
// Don't tick until we are initialized
|
||||
if (!eventualInitialized.isCompleted) {
|
||||
return;
|
||||
}
|
||||
if (!ProcessorRepository
|
||||
.instance.processorConnectionState.isPublicInternetReady) {
|
||||
return;
|
||||
|
@ -3,7 +3,9 @@ library;
|
||||
|
||||
export 'src/async_tag_lock.dart';
|
||||
export 'src/async_value.dart';
|
||||
export 'src/delayed_wait_set.dart';
|
||||
export 'src/serial_future.dart';
|
||||
export 'src/single_future.dart';
|
||||
export 'src/single_state_processor.dart';
|
||||
export 'src/single_stateless_processor.dart';
|
||||
export 'src/wait_set.dart';
|
||||
|
18
packages/async_tools/lib/src/delayed_wait_set.dart
Normal file
18
packages/async_tools/lib/src/delayed_wait_set.dart
Normal 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 = [];
|
||||
}
|
@ -4,14 +4,14 @@ import 'async_tag_lock.dart';
|
||||
|
||||
AsyncTagLock<Object> _keys = AsyncTagLock();
|
||||
|
||||
// Process a single future at a time per tag
|
||||
//
|
||||
// The closure function is called to produce the future that is to be executed.
|
||||
// If a future with a particular tag is still executing, the onBusy callback
|
||||
// is called.
|
||||
// When a tagged singleFuture finishes executing, the onDone callback is called.
|
||||
// If an unhandled exception happens in the closure future, the onError callback
|
||||
// is called.
|
||||
/// Process a single future at a time per tag
|
||||
///
|
||||
/// The closure function is called to produce the future that is to be executed.
|
||||
/// If a future with a particular tag is still executing, the onBusy callback
|
||||
/// is called.
|
||||
/// When a tagged singleFuture finishes executing, the onDone callback is called.
|
||||
/// If an unhandled exception happens in the closure future, the onError callback
|
||||
/// is called.
|
||||
void singleFuture<T>(Object tag, Future<T> Function() closure,
|
||||
{void Function()? onBusy,
|
||||
void Function(T)? onDone,
|
||||
@ -40,3 +40,6 @@ void singleFuture<T>(Object tag, Future<T> Function() closure,
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
Future<void> singleFuturePause(Object tag) async => _keys.lockTag(tag);
|
||||
void singleFutureResume(Object tag) => _keys.unlockTag(tag);
|
||||
|
@ -22,15 +22,19 @@ class SingleStateProcessor<State> {
|
||||
|
||||
singleFuture(this, () async {
|
||||
var newState = newInputState;
|
||||
var newClosure = closure;
|
||||
var done = false;
|
||||
while (!done) {
|
||||
await closure(newState);
|
||||
await newClosure(newState);
|
||||
|
||||
// See if there's another state change to process
|
||||
final next = _nextState;
|
||||
final nextState = _nextState;
|
||||
final nextClosure = _nextClosure;
|
||||
_nextState = null;
|
||||
if (next != null) {
|
||||
newState = next;
|
||||
_nextClosure = null;
|
||||
if (nextState != null) {
|
||||
newState = nextState;
|
||||
newClosure = nextClosure!;
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
@ -38,8 +42,26 @@ class SingleStateProcessor<State> {
|
||||
}, onBusy: () {
|
||||
// Keep this state until we process again
|
||||
_nextState = newInputState;
|
||||
_nextClosure = closure;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> pause() => singleFuturePause(this);
|
||||
Future<void> resume() async {
|
||||
// Process any next state before resuming the singlefuture
|
||||
try {
|
||||
final nextState = _nextState;
|
||||
final nextClosure = _nextClosure;
|
||||
_nextState = null;
|
||||
_nextClosure = null;
|
||||
if (nextState != null) {
|
||||
await nextClosure!(nextState);
|
||||
}
|
||||
} finally {
|
||||
singleFutureResume(this);
|
||||
}
|
||||
}
|
||||
|
||||
State? _nextState;
|
||||
Future<void> Function(State)? _nextClosure;
|
||||
}
|
||||
|
18
packages/async_tools/lib/src/wait_set.dart
Normal file
18
packages/async_tools/lib/src/wait_set.dart
Normal 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 = [];
|
||||
}
|
@ -6,6 +6,6 @@ export 'src/bloc_busy_wrapper.dart';
|
||||
export 'src/bloc_map_cubit.dart';
|
||||
export 'src/bloc_tools_extension.dart';
|
||||
export 'src/future_cubit.dart';
|
||||
export 'src/state_follower.dart';
|
||||
export 'src/state_map_follower.dart';
|
||||
export 'src/stream_wrapper_cubit.dart';
|
||||
export 'src/transformer_cubit.dart';
|
||||
|
@ -14,6 +14,7 @@ class AsyncTransformerCubit<T, S> extends Cubit<AsyncValue<T>> {
|
||||
_asyncTransform(input.state);
|
||||
_subscription = input.stream.listen(_asyncTransform);
|
||||
}
|
||||
|
||||
void _asyncTransform(AsyncValue<S> newInputState) {
|
||||
_singleStateProcessor.updateState(newInputState, (newState) async {
|
||||
// Emit the transformed state
|
||||
|
@ -3,6 +3,9 @@ import 'dart:async';
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'state_map_follower.dart';
|
||||
|
||||
typedef BlocMapState<K, S> = IMap<K, S>;
|
||||
|
||||
@ -18,14 +21,15 @@ class _ItemEntry<S, B> {
|
||||
// cubits.
|
||||
//
|
||||
// K = Key type for the bloc map, used to look up some mapped cubit
|
||||
// S = State type for the value, keys will look up values of this type
|
||||
// V = State type for the value, keys will look up values of this type
|
||||
// B = Bloc/cubit type for the value, output states of type S
|
||||
abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
|
||||
extends Cubit<BlocMapState<K, S>> {
|
||||
abstract class BlocMapCubit<K, V, B extends BlocBase<V>>
|
||||
extends Cubit<BlocMapState<K, V>>
|
||||
with StateMapFollowable<BlocMapState<K, V>, K, V> {
|
||||
BlocMapCubit()
|
||||
: _entries = {},
|
||||
_tagLock = AsyncTagLock(),
|
||||
super(IMap<K, S>());
|
||||
super(IMap<K, V>());
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
@ -34,6 +38,13 @@ abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
|
||||
await super.close();
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
// ignore: unnecessary_overrides
|
||||
void emit(BlocMapState<K, V> state) {
|
||||
super.emit(state);
|
||||
}
|
||||
|
||||
Future<void> add(MapEntry<K, B> Function() create) {
|
||||
// Create new element
|
||||
final newElement = create();
|
||||
@ -56,7 +67,7 @@ abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addState(K key, S value) =>
|
||||
Future<void> addState(K key, V value) =>
|
||||
_tagLock.protect(key, closure: () async {
|
||||
// Remove entry with the same key if it exists
|
||||
await _internalRemove(key);
|
||||
@ -107,6 +118,10 @@ abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
|
||||
return closure(entry.bloc);
|
||||
});
|
||||
|
||||
final Map<K, _ItemEntry<S, B>> _entries;
|
||||
/// StateMapFollowable /////////////////////////
|
||||
@override
|
||||
IMap<K, V> getStateMap(BlocMapState<K, V> s) => s;
|
||||
|
||||
final Map<K, _ItemEntry<V, B>> _entries;
|
||||
final AsyncTagLock<K> _tagLock;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
125
packages/bloc_tools/lib/src/state_map_follower.dart
Normal file
125
packages/bloc_tools/lib/src/state_map_follower.dart
Normal 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 = [];
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export 'default_dht_record_cubit.dart';
|
||||
export 'dht_record_crypto.dart';
|
||||
export 'dht_record_cubit.dart';
|
||||
export 'dht_record_pool.dart';
|
||||
|
@ -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)]);
|
||||
}
|
||||
}
|
@ -16,12 +16,13 @@ class DHTRecordWatchChange extends Equatable {
|
||||
/////////////////////////////////////////////////
|
||||
|
||||
class DHTRecord {
|
||||
DHTRecord(
|
||||
DHTRecord._(
|
||||
{required VeilidRoutingContext routingContext,
|
||||
required SharedDHTRecordData sharedDHTRecordData,
|
||||
required int defaultSubkey,
|
||||
required KeyPair? writer,
|
||||
required DHTRecordCrypto crypto})
|
||||
required DHTRecordCrypto crypto,
|
||||
required this.debugName})
|
||||
: _crypto = crypto,
|
||||
_routingContext = routingContext,
|
||||
_defaultSubkey = defaultSubkey,
|
||||
@ -34,6 +35,7 @@ class DHTRecord {
|
||||
final int _defaultSubkey;
|
||||
final KeyPair? _writer;
|
||||
final DHTRecordCrypto _crypto;
|
||||
final String debugName;
|
||||
|
||||
bool _open;
|
||||
@internal
|
||||
|
@ -3,6 +3,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../../veilid_support.dart';
|
||||
|
||||
@ -20,7 +21,7 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
|
||||
}) : _wantsCloseRecord = false,
|
||||
_stateFunction = stateFunction,
|
||||
super(const AsyncValue.loading()) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
initWait.add(() async {
|
||||
// Do record open/create
|
||||
_record = await open();
|
||||
_wantsCloseRecord = true;
|
||||
@ -73,6 +74,7 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await initWait();
|
||||
await _record.cancelWatch();
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
@ -84,6 +86,8 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
|
||||
}
|
||||
|
||||
Future<void> refresh(List<ValueSubkeyRange> subkeys) async {
|
||||
await initWait();
|
||||
|
||||
var updateSubkeys = [...subkeys];
|
||||
|
||||
for (final skr in subkeys) {
|
||||
@ -107,69 +111,11 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
|
||||
|
||||
DHTRecord get record => _record;
|
||||
|
||||
@protected
|
||||
final WaitSet initWait = WaitSet();
|
||||
|
||||
StreamSubscription<DHTRecordWatchChange>? _subscription;
|
||||
late DHTRecord _record;
|
||||
bool _wantsCloseRecord;
|
||||
final StateFunction<T> _stateFunction;
|
||||
}
|
||||
|
||||
// Cubit that watches the default subkey value of a dhtrecord
|
||||
class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
|
||||
DefaultDHTRecordCubit({
|
||||
required super.open,
|
||||
required T Function(List<int> data) decodeState,
|
||||
}) : super(
|
||||
initialStateFunction: _makeInitialStateFunction(decodeState),
|
||||
stateFunction: _makeStateFunction(decodeState),
|
||||
watchFunction: _makeWatchFunction());
|
||||
|
||||
// DefaultDHTRecordCubit.value({
|
||||
// required super.record,
|
||||
// required T Function(List<int> data) decodeState,
|
||||
// }) : super.value(
|
||||
// initialStateFunction: _makeInitialStateFunction(decodeState),
|
||||
// stateFunction: _makeStateFunction(decodeState),
|
||||
// watchFunction: _makeWatchFunction());
|
||||
|
||||
static InitialStateFunction<T> _makeInitialStateFunction<T>(
|
||||
T Function(List<int> data) decodeState) =>
|
||||
(record) async {
|
||||
final initialData = await record.get();
|
||||
if (initialData == null) {
|
||||
return null;
|
||||
}
|
||||
return decodeState(initialData);
|
||||
};
|
||||
|
||||
static StateFunction<T> _makeStateFunction<T>(
|
||||
T Function(List<int> data) decodeState) =>
|
||||
(record, subkeys, updatedata) async {
|
||||
final defaultSubkey = record.subkeyOrDefault(-1);
|
||||
if (subkeys.containsSubkey(defaultSubkey)) {
|
||||
final Uint8List data;
|
||||
final firstSubkey = subkeys.firstOrNull!.low;
|
||||
if (firstSubkey != defaultSubkey || updatedata == null) {
|
||||
final maybeData = await record.get(forceRefresh: true);
|
||||
if (maybeData == null) {
|
||||
return null;
|
||||
}
|
||||
data = maybeData;
|
||||
} else {
|
||||
data = updatedata;
|
||||
}
|
||||
final newState = decodeState(data);
|
||||
return newState;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
static WatchFunction _makeWatchFunction() => (record) async {
|
||||
final defaultSubkey = record.subkeyOrDefault(-1);
|
||||
await record.watch(subkeys: [ValueSubkeyRange.single(defaultSubkey)]);
|
||||
};
|
||||
|
||||
Future<void> refreshDefault() async {
|
||||
final defaultSubkey = _record.subkeyOrDefault(-1);
|
||||
await refresh([ValueSubkeyRange(low: defaultSubkey, high: defaultSubkey)]);
|
||||
}
|
||||
}
|
||||
|
@ -18,14 +18,16 @@ const int watchBackoffMultiplier = 2;
|
||||
const int watchBackoffMax = 30;
|
||||
|
||||
/// Record pool that managed DHTRecords and allows for tagged deletion
|
||||
/// String versions of keys due to IMap<> json unsupported in key
|
||||
@freezed
|
||||
class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations {
|
||||
const factory DHTRecordPoolAllocations({
|
||||
required IMap<String, ISet<TypedKey>>
|
||||
childrenByParent, // String key due to IMap<> json unsupported in key
|
||||
required IMap<String, TypedKey>
|
||||
parentByChild, // String key due to IMap<> json unsupported in key
|
||||
required ISet<TypedKey> rootRecords,
|
||||
@Default(IMapConst<String, ISet<TypedKey>>({}))
|
||||
IMap<String, ISet<TypedKey>> childrenByParent,
|
||||
@Default(IMapConst<String, TypedKey>({}))
|
||||
IMap<String, TypedKey> parentByChild,
|
||||
@Default(ISetConst<TypedKey>({})) ISet<TypedKey> rootRecords,
|
||||
@Default(IMapConst<String, String>({})) IMap<String, String> debugNames,
|
||||
}) = _DHTRecordPoolAllocations;
|
||||
|
||||
factory DHTRecordPoolAllocations.fromJson(dynamic json) =>
|
||||
@ -92,10 +94,7 @@ class OpenedRecordInfo {
|
||||
|
||||
class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext)
|
||||
: _state = DHTRecordPoolAllocations(
|
||||
childrenByParent: IMap(),
|
||||
parentByChild: IMap(),
|
||||
rootRecords: ISet()),
|
||||
: _state = const DHTRecordPoolAllocations(),
|
||||
_mutex = Mutex(),
|
||||
_opened = <TypedKey, OpenedRecordInfo>{},
|
||||
_routingContext = routingContext,
|
||||
@ -129,8 +128,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
@override
|
||||
DHTRecordPoolAllocations valueFromJson(Object? obj) => obj != null
|
||||
? DHTRecordPoolAllocations.fromJson(obj)
|
||||
: DHTRecordPoolAllocations(
|
||||
childrenByParent: IMap(), parentByChild: IMap(), rootRecords: ISet());
|
||||
: const DHTRecordPoolAllocations();
|
||||
@override
|
||||
Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson();
|
||||
|
||||
@ -148,7 +146,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
Veilid get veilid => _veilid;
|
||||
|
||||
Future<OpenedRecordInfo> _recordCreateInner(
|
||||
{required VeilidRoutingContext dhtctx,
|
||||
{required String debugName,
|
||||
required VeilidRoutingContext dhtctx,
|
||||
required DHTSchema schema,
|
||||
KeyPair? writer,
|
||||
TypedKey? parent}) async {
|
||||
@ -169,13 +168,18 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
_opened[recordDescriptor.key] = openedRecordInfo;
|
||||
|
||||
// Register the dependency
|
||||
await _addDependencyInner(parent, recordDescriptor.key);
|
||||
await _addDependencyInner(
|
||||
parent,
|
||||
recordDescriptor.key,
|
||||
debugName: debugName,
|
||||
);
|
||||
|
||||
return openedRecordInfo;
|
||||
}
|
||||
|
||||
Future<OpenedRecordInfo> _recordOpenInner(
|
||||
{required VeilidRoutingContext dhtctx,
|
||||
{required String debugName,
|
||||
required VeilidRoutingContext dhtctx,
|
||||
required TypedKey recordKey,
|
||||
KeyPair? writer,
|
||||
TypedKey? parent}) async {
|
||||
@ -198,7 +202,11 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
_opened[recordDescriptor.key] = newOpenedRecordInfo;
|
||||
|
||||
// Register the dependency
|
||||
await _addDependencyInner(parent, recordKey);
|
||||
await _addDependencyInner(
|
||||
parent,
|
||||
recordKey,
|
||||
debugName: debugName,
|
||||
);
|
||||
|
||||
return newOpenedRecordInfo;
|
||||
}
|
||||
@ -218,7 +226,11 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
}
|
||||
|
||||
// Register the dependency
|
||||
await _addDependencyInner(parent, recordKey);
|
||||
await _addDependencyInner(
|
||||
parent,
|
||||
recordKey,
|
||||
debugName: debugName,
|
||||
);
|
||||
|
||||
return openedRecordInfo;
|
||||
}
|
||||
@ -259,6 +271,18 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
return allDeps.reversedView;
|
||||
}
|
||||
|
||||
void _debugPrintChildren(TypedKey recordKey, {List<TypedKey>? allDeps}) {
|
||||
allDeps ??= _collectChildrenInner(recordKey);
|
||||
// ignore: avoid_print
|
||||
print('Parent: $recordKey (${_state.debugNames[recordKey.toString()]})');
|
||||
for (final dep in allDeps) {
|
||||
if (dep != recordKey) {
|
||||
// ignore: avoid_print
|
||||
print(' Child: $dep (${_state.debugNames[dep.toString()]})');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteInner(TypedKey recordKey) async {
|
||||
// Remove this child from parents
|
||||
await _removeDependenciesInner([recordKey]);
|
||||
@ -269,7 +293,10 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
await _mutex.protect(() async {
|
||||
final allDeps = _collectChildrenInner(recordKey);
|
||||
|
||||
assert(allDeps.singleOrNull == recordKey, 'must delete children first');
|
||||
if (allDeps.singleOrNull != recordKey) {
|
||||
_debugPrintChildren(recordKey, allDeps: allDeps);
|
||||
assert(false, 'must delete children first');
|
||||
}
|
||||
|
||||
final ori = _opened[recordKey];
|
||||
if (ori != null) {
|
||||
@ -301,15 +328,17 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addDependencyInner(TypedKey? parent, TypedKey child) async {
|
||||
Future<void> _addDependencyInner(TypedKey? parent, TypedKey child,
|
||||
{required String debugName}) async {
|
||||
assert(_mutex.isLocked, 'should be locked here');
|
||||
if (parent == null) {
|
||||
if (_state.rootRecords.contains(child)) {
|
||||
// Dependency already added
|
||||
return;
|
||||
}
|
||||
_state = await store(
|
||||
_state.copyWith(rootRecords: _state.rootRecords.add(child)));
|
||||
_state = await store(_state.copyWith(
|
||||
rootRecords: _state.rootRecords.add(child),
|
||||
debugNames: _state.debugNames.add(child.toJson(), debugName)));
|
||||
} else {
|
||||
final childrenOfParent =
|
||||
_state.childrenByParent[parent.toJson()] ?? ISet<TypedKey>();
|
||||
@ -320,7 +349,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
_state = await store(_state.copyWith(
|
||||
childrenByParent: _state.childrenByParent
|
||||
.add(parent.toJson(), childrenOfParent.add(child)),
|
||||
parentByChild: _state.parentByChild.add(child.toJson(), parent)));
|
||||
parentByChild: _state.parentByChild.add(child.toJson(), parent),
|
||||
debugNames: _state.debugNames.add(child.toJson(), debugName)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,7 +361,9 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
|
||||
for (final child in childList) {
|
||||
if (_state.rootRecords.contains(child)) {
|
||||
state = state.copyWith(rootRecords: state.rootRecords.remove(child));
|
||||
state = state.copyWith(
|
||||
rootRecords: state.rootRecords.remove(child),
|
||||
debugNames: state.debugNames.remove(child.toJson()));
|
||||
} else {
|
||||
final parent = state.parentByChild[child.toJson()];
|
||||
if (parent == null) {
|
||||
@ -341,12 +373,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
if (children.isEmpty) {
|
||||
state = state.copyWith(
|
||||
childrenByParent: state.childrenByParent.remove(parent.toJson()),
|
||||
parentByChild: state.parentByChild.remove(child.toJson()));
|
||||
parentByChild: state.parentByChild.remove(child.toJson()),
|
||||
debugNames: state.debugNames.remove(child.toJson()));
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
childrenByParent:
|
||||
state.childrenByParent.add(parent.toJson(), children),
|
||||
parentByChild: state.parentByChild.remove(child.toJson()));
|
||||
parentByChild: state.parentByChild.remove(child.toJson()),
|
||||
debugNames: state.debugNames.remove(child.toJson()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -360,6 +394,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
|
||||
/// Create a root DHTRecord that has no dependent records
|
||||
Future<DHTRecord> create({
|
||||
required String debugName,
|
||||
VeilidRoutingContext? routingContext,
|
||||
TypedKey? parent,
|
||||
DHTSchema schema = const DHTSchema.dflt(oCnt: 1),
|
||||
@ -371,9 +406,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
final dhtctx = routingContext ?? _routingContext;
|
||||
|
||||
final openedRecordInfo = await _recordCreateInner(
|
||||
dhtctx: dhtctx, schema: schema, writer: writer, parent: parent);
|
||||
debugName: debugName,
|
||||
dhtctx: dhtctx,
|
||||
schema: schema,
|
||||
writer: writer,
|
||||
parent: parent);
|
||||
|
||||
final rec = DHTRecord(
|
||||
final rec = DHTRecord._(
|
||||
debugName: debugName,
|
||||
routingContext: dhtctx,
|
||||
defaultSubkey: defaultSubkey,
|
||||
sharedDHTRecordData: openedRecordInfo.shared,
|
||||
@ -391,7 +431,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
|
||||
/// Open a DHTRecord readonly
|
||||
Future<DHTRecord> openRead(TypedKey recordKey,
|
||||
{VeilidRoutingContext? routingContext,
|
||||
{required String debugName,
|
||||
VeilidRoutingContext? routingContext,
|
||||
TypedKey? parent,
|
||||
int defaultSubkey = 0,
|
||||
DHTRecordCrypto? crypto}) async =>
|
||||
@ -399,9 +440,13 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
final dhtctx = routingContext ?? _routingContext;
|
||||
|
||||
final openedRecordInfo = await _recordOpenInner(
|
||||
dhtctx: dhtctx, recordKey: recordKey, parent: parent);
|
||||
debugName: debugName,
|
||||
dhtctx: dhtctx,
|
||||
recordKey: recordKey,
|
||||
parent: parent);
|
||||
|
||||
final rec = DHTRecord(
|
||||
final rec = DHTRecord._(
|
||||
debugName: debugName,
|
||||
routingContext: dhtctx,
|
||||
defaultSubkey: defaultSubkey,
|
||||
sharedDHTRecordData: openedRecordInfo.shared,
|
||||
@ -417,6 +462,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
Future<DHTRecord> openWrite(
|
||||
TypedKey recordKey,
|
||||
KeyPair writer, {
|
||||
required String debugName,
|
||||
VeilidRoutingContext? routingContext,
|
||||
TypedKey? parent,
|
||||
int defaultSubkey = 0,
|
||||
@ -426,12 +472,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
final dhtctx = routingContext ?? _routingContext;
|
||||
|
||||
final openedRecordInfo = await _recordOpenInner(
|
||||
debugName: debugName,
|
||||
dhtctx: dhtctx,
|
||||
recordKey: recordKey,
|
||||
parent: parent,
|
||||
writer: writer);
|
||||
|
||||
final rec = DHTRecord(
|
||||
final rec = DHTRecord._(
|
||||
debugName: debugName,
|
||||
routingContext: dhtctx,
|
||||
defaultSubkey: defaultSubkey,
|
||||
writer: writer,
|
||||
@ -453,6 +501,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
/// parent must be specified.
|
||||
Future<DHTRecord> openOwned(
|
||||
OwnedDHTRecordPointer ownedDHTRecordPointer, {
|
||||
required String debugName,
|
||||
required TypedKey parent,
|
||||
VeilidRoutingContext? routingContext,
|
||||
int defaultSubkey = 0,
|
||||
@ -461,6 +510,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
|
||||
openWrite(
|
||||
ownedDHTRecordPointer.recordKey,
|
||||
ownedDHTRecordPointer.owner,
|
||||
debugName: debugName,
|
||||
routingContext: routingContext,
|
||||
parent: parent,
|
||||
defaultSubkey: defaultSubkey,
|
||||
|
@ -22,11 +22,12 @@ DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson(
|
||||
/// @nodoc
|
||||
mixin _$DHTRecordPoolAllocations {
|
||||
IMap<String, ISet<Typed<FixedEncodedString43>>> get childrenByParent =>
|
||||
throw _privateConstructorUsedError; // String key due to IMap<> json unsupported in key
|
||||
throw _privateConstructorUsedError;
|
||||
IMap<String, Typed<FixedEncodedString43>> get parentByChild =>
|
||||
throw _privateConstructorUsedError; // String key due to IMap<> json unsupported in key
|
||||
throw _privateConstructorUsedError;
|
||||
ISet<Typed<FixedEncodedString43>> get rootRecords =>
|
||||
throw _privateConstructorUsedError;
|
||||
IMap<String, String> get debugNames => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
@ -43,7 +44,8 @@ abstract class $DHTRecordPoolAllocationsCopyWith<$Res> {
|
||||
$Res call(
|
||||
{IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent,
|
||||
IMap<String, Typed<FixedEncodedString43>> parentByChild,
|
||||
ISet<Typed<FixedEncodedString43>> rootRecords});
|
||||
ISet<Typed<FixedEncodedString43>> rootRecords,
|
||||
IMap<String, String> debugNames});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -63,6 +65,7 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res,
|
||||
Object? childrenByParent = null,
|
||||
Object? parentByChild = null,
|
||||
Object? rootRecords = null,
|
||||
Object? debugNames = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
childrenByParent: null == childrenByParent
|
||||
@ -77,6 +80,10 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res,
|
||||
? _value.rootRecords
|
||||
: rootRecords // ignore: cast_nullable_to_non_nullable
|
||||
as ISet<Typed<FixedEncodedString43>>,
|
||||
debugNames: null == debugNames
|
||||
? _value.debugNames
|
||||
: debugNames // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<String, String>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
@ -93,7 +100,8 @@ abstract class _$$DHTRecordPoolAllocationsImplCopyWith<$Res>
|
||||
$Res call(
|
||||
{IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent,
|
||||
IMap<String, Typed<FixedEncodedString43>> parentByChild,
|
||||
ISet<Typed<FixedEncodedString43>> rootRecords});
|
||||
ISet<Typed<FixedEncodedString43>> rootRecords,
|
||||
IMap<String, String> debugNames});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -112,6 +120,7 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res>
|
||||
Object? childrenByParent = null,
|
||||
Object? parentByChild = null,
|
||||
Object? rootRecords = null,
|
||||
Object? debugNames = null,
|
||||
}) {
|
||||
return _then(_$DHTRecordPoolAllocationsImpl(
|
||||
childrenByParent: null == childrenByParent
|
||||
@ -126,6 +135,10 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res>
|
||||
? _value.rootRecords
|
||||
: rootRecords // ignore: cast_nullable_to_non_nullable
|
||||
as ISet<Typed<FixedEncodedString43>>,
|
||||
debugNames: null == debugNames
|
||||
? _value.debugNames
|
||||
: debugNames // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<String, String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -134,25 +147,30 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations {
|
||||
const _$DHTRecordPoolAllocationsImpl(
|
||||
{required this.childrenByParent,
|
||||
required this.parentByChild,
|
||||
required this.rootRecords});
|
||||
{this.childrenByParent = const IMapConst<String, ISet<TypedKey>>({}),
|
||||
this.parentByChild = const IMapConst<String, TypedKey>({}),
|
||||
this.rootRecords = const ISetConst<TypedKey>({}),
|
||||
this.debugNames = const IMapConst<String, String>({})});
|
||||
|
||||
factory _$DHTRecordPoolAllocationsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$DHTRecordPoolAllocationsImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent;
|
||||
// String key due to IMap<> json unsupported in key
|
||||
@override
|
||||
@JsonKey()
|
||||
final IMap<String, Typed<FixedEncodedString43>> parentByChild;
|
||||
// String key due to IMap<> json unsupported in key
|
||||
@override
|
||||
@JsonKey()
|
||||
final ISet<Typed<FixedEncodedString43>> rootRecords;
|
||||
@override
|
||||
@JsonKey()
|
||||
final IMap<String, String> debugNames;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords)';
|
||||
return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords, debugNames: $debugNames)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -165,13 +183,15 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations {
|
||||
(identical(other.parentByChild, parentByChild) ||
|
||||
other.parentByChild == parentByChild) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.rootRecords, rootRecords));
|
||||
.equals(other.rootRecords, rootRecords) &&
|
||||
(identical(other.debugNames, debugNames) ||
|
||||
other.debugNames == debugNames));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, childrenByParent, parentByChild,
|
||||
const DeepCollectionEquality().hash(rootRecords));
|
||||
const DeepCollectionEquality().hash(rootRecords), debugNames);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -190,22 +210,23 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations {
|
||||
|
||||
abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations {
|
||||
const factory _DHTRecordPoolAllocations(
|
||||
{required final IMap<String, ISet<Typed<FixedEncodedString43>>>
|
||||
childrenByParent,
|
||||
required final IMap<String, Typed<FixedEncodedString43>> parentByChild,
|
||||
required final ISet<Typed<FixedEncodedString43>>
|
||||
rootRecords}) = _$DHTRecordPoolAllocationsImpl;
|
||||
{final IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent,
|
||||
final IMap<String, Typed<FixedEncodedString43>> parentByChild,
|
||||
final ISet<Typed<FixedEncodedString43>> rootRecords,
|
||||
final IMap<String, String> debugNames}) = _$DHTRecordPoolAllocationsImpl;
|
||||
|
||||
factory _DHTRecordPoolAllocations.fromJson(Map<String, dynamic> json) =
|
||||
_$DHTRecordPoolAllocationsImpl.fromJson;
|
||||
|
||||
@override
|
||||
IMap<String, ISet<Typed<FixedEncodedString43>>> get childrenByParent;
|
||||
@override // String key due to IMap<> json unsupported in key
|
||||
@override
|
||||
IMap<String, Typed<FixedEncodedString43>> get parentByChild;
|
||||
@override // String key due to IMap<> json unsupported in key
|
||||
@override
|
||||
ISet<Typed<FixedEncodedString43>> get rootRecords;
|
||||
@override
|
||||
IMap<String, String> get debugNames;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$DHTRecordPoolAllocationsImplCopyWith<_$DHTRecordPoolAllocationsImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
|
@ -9,19 +9,29 @@ part of 'dht_record_pool.dart';
|
||||
_$DHTRecordPoolAllocationsImpl _$$DHTRecordPoolAllocationsImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$DHTRecordPoolAllocationsImpl(
|
||||
childrenByParent:
|
||||
IMap<String, ISet<Typed<FixedEncodedString43>>>.fromJson(
|
||||
childrenByParent: json['childrenByParent'] == null
|
||||
? const IMapConst<String, ISet<TypedKey>>({})
|
||||
: IMap<String, ISet<Typed<FixedEncodedString43>>>.fromJson(
|
||||
json['childrenByParent'] as Map<String, dynamic>,
|
||||
(value) => value as String,
|
||||
(value) => ISet<Typed<FixedEncodedString43>>.fromJson(value,
|
||||
(value) => Typed<FixedEncodedString43>.fromJson(value))),
|
||||
parentByChild: IMap<String, Typed<FixedEncodedString43>>.fromJson(
|
||||
json['parentByChild'] as Map<String, dynamic>,
|
||||
(value) => value as String,
|
||||
(value) => Typed<FixedEncodedString43>.fromJson(value)),
|
||||
rootRecords: ISet<Typed<FixedEncodedString43>>.fromJson(
|
||||
json['rootRecords'],
|
||||
(value) => Typed<FixedEncodedString43>.fromJson(value)),
|
||||
parentByChild: json['parentByChild'] == null
|
||||
? const IMapConst<String, TypedKey>({})
|
||||
: IMap<String, Typed<FixedEncodedString43>>.fromJson(
|
||||
json['parentByChild'] as Map<String, dynamic>,
|
||||
(value) => value as String,
|
||||
(value) => Typed<FixedEncodedString43>.fromJson(value)),
|
||||
rootRecords: json['rootRecords'] == null
|
||||
? const ISetConst<TypedKey>({})
|
||||
: ISet<Typed<FixedEncodedString43>>.fromJson(json['rootRecords'],
|
||||
(value) => Typed<FixedEncodedString43>.fromJson(value)),
|
||||
debugNames: json['debugNames'] == null
|
||||
? const IMapConst<String, String>({})
|
||||
: IMap<String, String>.fromJson(
|
||||
json['debugNames'] as Map<String, dynamic>,
|
||||
(value) => value as String,
|
||||
(value) => value as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$DHTRecordPoolAllocationsImplToJson(
|
||||
@ -40,6 +50,10 @@ Map<String, dynamic> _$$DHTRecordPoolAllocationsImplToJson(
|
||||
'rootRecords': instance.rootRecords.toJson(
|
||||
(value) => value,
|
||||
),
|
||||
'debugNames': instance.debugNames.toJson(
|
||||
(value) => value,
|
||||
(value) => value,
|
||||
),
|
||||
};
|
||||
|
||||
_$OwnedDHTRecordPointerImpl _$$OwnedDHTRecordPointerImplFromJson(
|
||||
|
@ -28,7 +28,8 @@ class DHTShortArray {
|
||||
// if smplWriter is specified, uses a SMPL schema with a single writer
|
||||
// rather than the key owner
|
||||
static Future<DHTShortArray> create(
|
||||
{int stride = maxElements,
|
||||
{required String debugName,
|
||||
int stride = maxElements,
|
||||
VeilidRoutingContext? routingContext,
|
||||
TypedKey? parent,
|
||||
DHTRecordCrypto? crypto,
|
||||
@ -42,6 +43,7 @@ class DHTShortArray {
|
||||
oCnt: 0,
|
||||
members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]);
|
||||
dhtRecord = await pool.create(
|
||||
debugName: debugName,
|
||||
parent: parent,
|
||||
routingContext: routingContext,
|
||||
schema: schema,
|
||||
@ -50,6 +52,7 @@ class DHTShortArray {
|
||||
} else {
|
||||
final schema = DHTSchema.dflt(oCnt: stride + 1);
|
||||
dhtRecord = await pool.create(
|
||||
debugName: debugName,
|
||||
parent: parent,
|
||||
routingContext: routingContext,
|
||||
schema: schema,
|
||||
@ -72,11 +75,15 @@ class DHTShortArray {
|
||||
}
|
||||
|
||||
static Future<DHTShortArray> openRead(TypedKey headRecordKey,
|
||||
{VeilidRoutingContext? routingContext,
|
||||
{required String debugName,
|
||||
VeilidRoutingContext? routingContext,
|
||||
TypedKey? parent,
|
||||
DHTRecordCrypto? crypto}) async {
|
||||
final dhtRecord = await DHTRecordPool.instance.openRead(headRecordKey,
|
||||
parent: parent, routingContext: routingContext, crypto: crypto);
|
||||
debugName: debugName,
|
||||
parent: parent,
|
||||
routingContext: routingContext,
|
||||
crypto: crypto);
|
||||
try {
|
||||
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
|
||||
await dhtShortArray._head.operate((head) => head._loadHead());
|
||||
@ -90,13 +97,17 @@ class DHTShortArray {
|
||||
static Future<DHTShortArray> openWrite(
|
||||
TypedKey headRecordKey,
|
||||
KeyPair writer, {
|
||||
required String debugName,
|
||||
VeilidRoutingContext? routingContext,
|
||||
TypedKey? parent,
|
||||
DHTRecordCrypto? crypto,
|
||||
}) async {
|
||||
final dhtRecord = await DHTRecordPool.instance.openWrite(
|
||||
headRecordKey, writer,
|
||||
parent: parent, routingContext: routingContext, crypto: crypto);
|
||||
debugName: debugName,
|
||||
parent: parent,
|
||||
routingContext: routingContext,
|
||||
crypto: crypto);
|
||||
try {
|
||||
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
|
||||
await dhtShortArray._head.operate((head) => head._loadHead());
|
||||
@ -109,6 +120,7 @@ class DHTShortArray {
|
||||
|
||||
static Future<DHTShortArray> openOwned(
|
||||
OwnedDHTRecordPointer ownedDHTRecordPointer, {
|
||||
required String debugName,
|
||||
required TypedKey parent,
|
||||
VeilidRoutingContext? routingContext,
|
||||
DHTRecordCrypto? crypto,
|
||||
@ -116,6 +128,7 @@ class DHTShortArray {
|
||||
openWrite(
|
||||
ownedDHTRecordPointer.recordKey,
|
||||
ownedDHTRecordPointer.owner,
|
||||
debugName: debugName,
|
||||
routingContext: routingContext,
|
||||
parent: parent,
|
||||
crypto: crypto,
|
||||
|
@ -17,13 +17,13 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
|
||||
required T Function(List<int> data) decodeElement,
|
||||
}) : _decodeElement = decodeElement,
|
||||
super(const BlocBusyState(AsyncValue.loading())) {
|
||||
_initFuture = Future(() async {
|
||||
_initWait.add(() async {
|
||||
// Open DHT record
|
||||
_shortArray = await open();
|
||||
_wantsCloseRecord = true;
|
||||
|
||||
// Make initial state update
|
||||
unawaited(_refreshNoWait());
|
||||
await _refreshNoWait();
|
||||
_subscription = await _shortArray.listen(_update);
|
||||
});
|
||||
}
|
||||
@ -42,7 +42,7 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
|
||||
// }
|
||||
|
||||
Future<void> refresh({bool forceRefresh = false}) async {
|
||||
await _initFuture;
|
||||
await _initWait();
|
||||
await _refreshNoWait(forceRefresh: forceRefresh);
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _initFuture;
|
||||
await _initWait();
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
if (_wantsCloseRecord) {
|
||||
@ -85,24 +85,24 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
|
||||
}
|
||||
|
||||
Future<R?> operate<R>(Future<R?> Function(DHTShortArrayRead) closure) async {
|
||||
await _initFuture;
|
||||
await _initWait();
|
||||
return _shortArray.operate(closure);
|
||||
}
|
||||
|
||||
Future<(R?, bool)> operateWrite<R>(
|
||||
Future<R?> Function(DHTShortArrayWrite) closure) async {
|
||||
await _initFuture;
|
||||
await _initWait();
|
||||
return _shortArray.operateWrite(closure);
|
||||
}
|
||||
|
||||
Future<void> operateWriteEventual(
|
||||
Future<bool> Function(DHTShortArrayWrite) closure,
|
||||
{Duration? timeout}) async {
|
||||
await _initFuture;
|
||||
await _initWait();
|
||||
return _shortArray.operateWriteEventual(closure, timeout: timeout);
|
||||
}
|
||||
|
||||
late final Future<void> _initFuture;
|
||||
final WaitSet _initWait = WaitSet();
|
||||
late final DHTShortArray _shortArray;
|
||||
final T Function(List<int> data) _decodeElement;
|
||||
StreamSubscription<void>? _subscription;
|
||||
|
@ -184,7 +184,7 @@ class _DHTShortArrayHead {
|
||||
final oldRecord = oldRecords[newKey];
|
||||
if (oldRecord == null) {
|
||||
// Open the new record
|
||||
final newRecord = await _openLinkedRecord(newKey);
|
||||
final newRecord = await _openLinkedRecord(newKey, n);
|
||||
newRecords[newKey] = newRecord;
|
||||
updatedLinkedRecords.add(newRecord);
|
||||
} else {
|
||||
@ -263,6 +263,7 @@ class _DHTShortArrayHead {
|
||||
oCnt: 0,
|
||||
members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]);
|
||||
final dhtRecord = await pool.create(
|
||||
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
||||
parent: parent,
|
||||
routingContext: routingContext,
|
||||
schema: schema,
|
||||
@ -279,17 +280,20 @@ class _DHTShortArrayHead {
|
||||
}
|
||||
|
||||
/// Open a linked record for reading or writing, same as the head record
|
||||
Future<DHTRecord> _openLinkedRecord(TypedKey recordKey) async {
|
||||
Future<DHTRecord> _openLinkedRecord(
|
||||
TypedKey recordKey, int recordNumber) async {
|
||||
final writer = _headRecord.writer;
|
||||
return (writer != null)
|
||||
? await DHTRecordPool.instance.openWrite(
|
||||
recordKey,
|
||||
writer,
|
||||
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
||||
parent: _headRecord.key,
|
||||
routingContext: _headRecord.routingContext,
|
||||
)
|
||||
: await DHTRecordPool.instance.openRead(
|
||||
recordKey,
|
||||
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
||||
parent: _headRecord.key,
|
||||
routingContext: _headRecord.routingContext,
|
||||
);
|
||||
|
@ -132,7 +132,10 @@ extension IdentityMasterExtension on IdentityMaster {
|
||||
|
||||
late final List<AccountRecordInfo> accountRecordInfo;
|
||||
await (await pool.openRead(identityRecordKey,
|
||||
parent: masterRecordKey, crypto: identityRecordCrypto))
|
||||
debugName:
|
||||
'IdentityMaster::readAccountsFromIdentity::IdentityRecord',
|
||||
parent: masterRecordKey,
|
||||
crypto: identityRecordCrypto))
|
||||
.scope((identityRec) async {
|
||||
final identity = await identityRec.getJson(Identity.fromJson);
|
||||
if (identity == null) {
|
||||
@ -161,14 +164,17 @@ extension IdentityMasterExtension on IdentityMaster {
|
||||
/////// Add account with profile to DHT
|
||||
|
||||
// Open identity key for writing
|
||||
veilidLoggy.debug('Opening master identity');
|
||||
veilidLoggy.debug('Opening identity record');
|
||||
return (await pool.openWrite(
|
||||
identityRecordKey, identityWriter(identitySecret),
|
||||
debugName: 'IdentityMaster::addAccountToIdentity::IdentityRecord',
|
||||
parent: masterRecordKey))
|
||||
.scope((identityRec) async {
|
||||
// Create new account to insert into identity
|
||||
veilidLoggy.debug('Creating new account');
|
||||
return (await pool.create(parent: identityRec.key))
|
||||
return (await pool.create(
|
||||
debugName: 'IdentityMaster::addAccountToIdentity::AccountRecord',
|
||||
parent: identityRec.key))
|
||||
.deleteScope((accountRec) async {
|
||||
final account = await createAccountCallback(accountRec.key);
|
||||
// Write account key
|
||||
@ -222,11 +228,16 @@ class IdentityMasterWithSecrets {
|
||||
|
||||
// IdentityMaster DHT record is public/unencrypted
|
||||
veilidLoggy.debug('Creating master identity record');
|
||||
return (await pool.create(crypto: const DHTRecordCryptoPublic()))
|
||||
return (await pool.create(
|
||||
debugName:
|
||||
'IdentityMasterWithSecrets::create::IdentityMasterRecord',
|
||||
crypto: const DHTRecordCryptoPublic()))
|
||||
.deleteScope((masterRec) async {
|
||||
veilidLoggy.debug('Creating identity record');
|
||||
// Identity record is private
|
||||
return (await pool.create(parent: masterRec.key))
|
||||
return (await pool.create(
|
||||
debugName: 'IdentityMasterWithSecrets::create::IdentityRecord',
|
||||
parent: masterRec.key))
|
||||
.scope((identityRec) async {
|
||||
// Make IdentityMaster
|
||||
final masterRecordKey = masterRec.key;
|
||||
@ -282,7 +293,9 @@ Future<IdentityMaster> openIdentityMaster(
|
||||
final pool = DHTRecordPool.instance;
|
||||
|
||||
// IdentityMaster DHT record is public/unencrypted
|
||||
return (await pool.openRead(identityMasterRecordKey))
|
||||
return (await pool.openRead(identityMasterRecordKey,
|
||||
debugName:
|
||||
'IdentityMaster::openIdentityMaster::IdentityMasterRecord'))
|
||||
.deleteScope((masterRec) async {
|
||||
final identityMaster =
|
||||
(await masterRec.getJson(IdentityMaster.fromJson, forceRefresh: true))!;
|
||||
|
Loading…
Reference in New Issue
Block a user