ui cleanup

This commit is contained in:
Christien Rioux 2024-06-21 22:44:35 -04:00
parent 7b400ed08b
commit 152c8bdff4
15 changed files with 827 additions and 772 deletions

View File

@ -4,7 +4,6 @@ import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
@ -82,6 +81,16 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
await _sentMessagesCubit?.close();
await _rcvdMessagesCubit?.close();
await _reconciledMessagesCubit?.close();
// If the local conversation record is gone, then delete the reconciled
// messages table as well
final conversationDead = await DHTRecordPool.instance
.isDeletedRecordKey(_localConversationRecordKey);
if (conversationDead) {
await SingleContactMessagesCubit.cleanupAndDeleteMessages(
localConversationRecordKey: _localConversationRecordKey);
}
await super.close();
}
@ -292,8 +301,14 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
previousMessage = message;
}
// _sendingMessages = messages;
// _renderState();
await _sentMessagesCubit!.operateAppendEventual((writer) =>
writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
// _sendingMessages = const IList.empty();
}
// Produce a state for this cubit from the input cubits and queues
@ -304,7 +319,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Get all sent messages
final sentMessages = _sentMessagesCubit?.state.state.asData?.value;
//Get all items in the unsent queue
final unsentMessages = _unsentMessagesQueue.queue;
//final unsentMessages = _unsentMessagesQueue.queue;
// If we aren't ready to render a state, say we're loading
if (reconciledMessages == null || sentMessages == null) {
@ -329,7 +344,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// );
final renderedElements = <RenderStateElement>[];
final renderedIds = <String>{};
for (final m in reconciledMessages.windowElements) {
final isLocal =
m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey;
@ -346,13 +361,22 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
sent: sent,
sentOffline: sentOffline,
));
renderedIds.add(m.content.authorUniqueIdString);
}
for (final m in unsentMessages) {
renderedElements.add(RenderStateElement(
message: (m.deepCopy())..id = m.timestamp.toBytes(),
isLocal: true,
));
}
// Render in-flight messages at the bottom
// for (final m in _sendingMessages) {
// if (renderedIds.contains(m.authorUniqueIdString)) {
// continue;
// }
// renderedElements.add(RenderStateElement(
// message: m,
// isLocal: true,
// sent: true,
// sentOffline: true,
// ));
// }
// Render the state
final messages = renderedElements
@ -426,7 +450,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
late final MessageReconciliation _reconciliation;
late final PersistentQueue<proto.Message> _unsentMessagesQueue;
// IList<proto.Message> _sendingMessages = const IList.empty();
StreamSubscription<DHTLogBusyState<proto.Message>>? _sentSubscription;
StreamSubscription<DHTLogBusyState<proto.Message>>? _rcvdSubscription;
StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>?

View File

@ -8,7 +8,6 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../chat/chat.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
//////////////////////////////////////////////////
@ -58,9 +57,20 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
final remoteConversationRecordKey =
contact.remoteConversationRecordKey.toVeilid();
// Create 1:1 conversation type Chat
final chatMember = proto.ChatMember()
..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto();
final directChat = proto.DirectChat()
..settings = await getDefaultChatSettings(contact)
..localConversationRecordKey = localConversationRecordKey.toProto()
..remoteMember = chatMember;
final chat = proto.Chat()..direct = directChat;
// Add Chat to account's list
// if this fails, don't keep retrying, user can try again later
await operateWrite((writer) async {
await operateWriteEventual((writer) async {
// See if we have added this chat already
for (var i = 0; i < writer.length; i++) {
final cbuf = await writer.get(i);
@ -89,18 +99,6 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
}
}
// Create 1:1 conversation type Chat
final chatMember = proto.ChatMember()
..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto();
final directChat = proto.DirectChat()
..settings = await getDefaultChatSettings(contact)
..localConversationRecordKey = localConversationRecordKey.toProto()
..remoteMember = chatMember;
final chat = proto.Chat()..direct = directChat;
// Add chat
await writer.add(chat.writeToBuffer());
});
@ -110,10 +108,7 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
Future<void> deleteChat(
{required TypedKey localConversationRecordKey}) async {
// Remove Chat from account's list
// if this fails, don't keep retrying, user can try again later
final deletedItem =
// Ensure followers get their changes before we return
await syncFollowers(() => operateWrite((writer) async {
await operateWriteEventual((writer) async {
if (_activeChatCubit.state == localConversationRecordKey) {
_activeChatCubit.setActiveChat(null);
}
@ -123,24 +118,12 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
throw Exception('Failed to get chat');
}
if (c.localConversationRecordKey ==
localConversationRecordKey) {
if (c.localConversationRecordKey == localConversationRecordKey) {
await writer.remove(i);
return c;
}
}
return null;
}));
// Since followers are synced, we can safetly remove the reconciled
// chat record now
if (deletedItem != null) {
try {
await SingleContactMessagesCubit.cleanupAndDeleteMessages(
localConversationRecordKey: localConversationRecordKey);
} on Exception catch (e) {
log.debug('error removing reconciled chat table: $e', e);
return;
}
}
});
}
/// StateMapFollowable /////////////////////////

View File

@ -152,8 +152,7 @@ class ContactInvitationListCubit
..message = message;
// Add ContactInvitationRecord to account's list
// if this fails, don't keep retrying, user can try again later
await operateWrite((writer) async {
await operateWriteEventual((writer) async {
await writer.add(cinvrec.writeToBuffer());
});
});

View File

@ -46,7 +46,6 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
final signedContactInvitationBytesV =
@ -58,6 +57,9 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
return PopControl(
dismissible: !signedContactInvitationBytesV.isLoading,
child: Dialog(
shape: RoundedRectangleBorder(
side: const BorderSide(width: 2),
borderRadius: BorderRadius.circular(16)),
backgroundColor: Colors.white,
child: ConstrainedBox(
constraints: BoxConstraints(
@ -90,6 +92,10 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
.paddingAll(8),
ElevatedButton.icon(
icon: const Icon(Icons.copy),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white,
side: const BorderSide()),
label: Text(translate(
'create_invitation_dialog.copy_invitation')),
onPressed: () async {

View File

@ -6,7 +6,6 @@ import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
@ -17,8 +16,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
ContactListCubit({
required AccountInfo accountInfo,
required OwnedDHTRecordPointer contactListRecordPointer,
}) : _accountInfo = accountInfo,
super(
}) : super(
open: () =>
_open(accountInfo.accountRecordKey, contactListRecordPointer),
decodeElement: proto.Contact.fromBuffer);
@ -98,8 +96,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
..showAvailability = false;
// Add Contact to account's list
// if this fails, don't keep retrying, user can try again later
await operateWrite((writer) async {
await operateWriteEventual((writer) async {
await writer.add(contact.writeToBuffer());
});
}
@ -107,7 +104,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
Future<void> deleteContact(
{required TypedKey localConversationRecordKey}) async {
// Remove Contact from account's list
final deletedItem = await operateWrite((writer) async {
final deletedItem = await operateWriteEventual((writer) async {
for (var i = 0; i < writer.length; i++) {
final item = await writer.getProtobuf(proto.Contact.fromBuffer, i);
if (item == null) {
@ -124,18 +121,11 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (deletedItem != null) {
try {
// Make a conversation cubit to manipulate the conversation
final conversationCubit = ConversationCubit(
accountInfo: _accountInfo,
remoteIdentityPublicKey: deletedItem.identityPublicKey.toVeilid(),
localConversationRecordKey:
deletedItem.localConversationRecordKey.toVeilid(),
remoteConversationRecordKey:
deletedItem.remoteConversationRecordKey.toVeilid(),
);
// Delete the local and remote conversation records
await conversationCubit.delete();
// Mark the conversation records for deletion
await DHTRecordPool.instance
.deleteRecord(deletedItem.localConversationRecordKey.toVeilid());
await DHTRecordPool.instance
.deleteRecord(deletedItem.remoteConversationRecordKey.toVeilid());
} on Exception catch (e) {
log.debug('error deleting conversation records: $e', e);
}
@ -144,5 +134,4 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
final _contactProfileUpdateMap =
SingleStateProcessorMap<TypedKey, proto.Profile?>();
final AccountInfo _accountInfo;
}

View File

@ -74,13 +74,13 @@ class ContactItemWidget extends StatelessWidget {
final contactListCubit = context.read<ContactListCubit>();
final chatListCubit = context.read<ChatListCubit>();
// Remove any chats for this contact
await chatListCubit.deleteChat(
localConversationRecordKey: localConversationRecordKey);
// Delete the contact itself
await contactListCubit.deleteContact(
localConversationRecordKey: localConversationRecordKey);
// Remove any chats for this contact
await chatListCubit.deleteChat(
localConversationRecordKey: localConversationRecordKey);
})
],
);

View File

@ -28,7 +28,6 @@ class _SingleContactChatState extends Equatable {
final TypedKey remoteMessagesRecordKey;
@override
// TODO: implement props
List<Object?> get props => [
remoteIdentityPublicKey,
localConversationRecordKey,

View File

@ -14,7 +14,6 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
const _sfUpdateAccountChange = 'updateAccountChange';
@ -116,7 +115,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
final accountRecordKey = _accountInfo.accountRecordKey;
final writer = _accountInfo.identityWriter;
// Open with SMPL scheme for identity writer
// Open with SMPL schema for identity writer
late final DHTRecord localConversationRecord;
if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openRecordWrite(
@ -171,57 +170,6 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
return out;
}
/// Delete the conversation keys associated with this conversation
Future<bool> delete() async {
final pool = DHTRecordPool.instance;
await _initWait();
final localConversationCubit = _localConversationCubit;
final remoteConversationCubit = _remoteConversationCubit;
final deleteSet = DelayedWaitSet<void>();
if (localConversationCubit != null) {
final data = localConversationCubit.state.asData;
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.deleteRecord(messagesKey);
await pool.deleteRecord(_localConversationRecordKey!);
_localConversationRecordKey = null;
});
}
if (remoteConversationCubit != null) {
final data = remoteConversationCubit.state.asData;
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.deleteRecord(messagesKey);
await pool.deleteRecord(_remoteConversationRecordKey!);
});
}
// Commit the delete futures
await deleteSet();
return true;
}
/// Force refresh of conversation keys
Future<void> refresh() async {
await _initWait();

View File

@ -71,11 +71,22 @@ class _DrawerMenuState extends State<DrawerMenu> {
shortname = abbrev;
}
final avatar = AvatarImage(
size: 32,
final avatar = Container(
height: 34,
width: 34,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: loggedIn ? scale.border : scale.subtleBorder,
width: 2,
strokeAlign: BorderSide.strokeAlignOutside),
color: Colors.blue,
),
child: AvatarImage(
//size: 32,
backgroundColor: loggedIn ? scale.primary : scale.elementBackground,
foregroundColor: loggedIn ? scale.primaryText : scale.subtleText,
child: Text(shortname, style: theme.textTheme.titleLarge));
child: Text(shortname, style: theme.textTheme.titleLarge)));
return AnimatedPadding(
padding: EdgeInsets.fromLTRB(selected ? 0 : 0, 0, selected ? 0 : 8, 0),
@ -234,6 +245,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
//final textTheme = theme.textTheme;
final localAccounts = context.watch<LocalAccountsCubit>().state;
final perAccountCollectionBlocMapState =
@ -271,11 +283,15 @@ class _DrawerMenuState extends State<DrawerMenu> {
SvgPicture.asset(
height: 48,
'assets/images/icon.svg',
).paddingLTRB(0, 0, 16, 0),
colorFilter: scaleConfig.useVisualIndicators
? grayColorFilter
: null)
.paddingLTRB(0, 0, 16, 0),
SvgPicture.asset(
height: 48,
'assets/images/title.svg',
),
colorFilter:
scaleConfig.useVisualIndicators ? grayColorFilter : null),
])),
const Spacer(),
_getAccountList(

View File

@ -89,11 +89,9 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
onError: errorScale.primaryText,
// errorContainer: errorScale.hoverElementBackground,
// onErrorContainer: errorScale.subtleText,
background: grayScale.appBackground, // reviewed
onBackground: grayScale.appText, // reviewed
surface: primaryScale.primary, // reviewed
onSurface: primaryScale.primaryText, // reviewed
surfaceVariant: secondaryScale.primary,
surfaceContainerHighest: secondaryScale.primary,
onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little
outline: primaryScale.border,
outlineVariant: secondaryScale.border,

View File

@ -5,7 +5,6 @@ import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:motion_toast/motion_toast.dart';
import 'package:quickalert/quickalert.dart';
@ -122,36 +121,45 @@ Future<void> showErrorModal(
}
void showErrorToast(BuildContext context, String message) {
MotionToast.error(
title: Text(translate('toast.error')),
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
//title: Text(translate('toast.error')),
description: Text(message),
constraints: BoxConstraints.loose(const Size(400, 100)),
contentPadding: const EdgeInsets.all(16),
primaryColor: scale.errorScale.elementBackground,
secondaryColor: scale.errorScale.calloutBackground,
borderRadius: 16,
toastDuration: const Duration(seconds: 4),
animationDuration: const Duration(milliseconds: 1000),
displayBorder: scaleConfig.useVisualIndicators,
icon: Icons.error,
).show(context);
}
void showInfoToast(BuildContext context, String message) {
MotionToast.info(
title: Text(translate('toast.info')),
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
//title: Text(translate('toast.info')),
description: Text(message),
constraints: BoxConstraints.loose(const Size(400, 100)),
contentPadding: const EdgeInsets.all(16),
primaryColor: scale.tertiaryScale.elementBackground,
secondaryColor: scale.tertiaryScale.calloutBackground,
borderRadius: 16,
toastDuration: const Duration(seconds: 2),
animationDuration: const Duration(milliseconds: 500),
displayBorder: scaleConfig.useVisualIndicators,
icon: Icons.info,
).show(context);
}
// Widget insetBorder(
// {required BuildContext context,
// required bool enabled,
// required Color color,
// required Widget child}) {
// if (!enabled) {
// return child;
// }
// return Stack({
// children: [] {
// DecoratedBox(decoration: BoxDecoration()
// child,
// }
// })
// }
Widget styledTitleContainer({
required BuildContext context,
required String title,
@ -230,3 +238,26 @@ Widget styledBottomSheet({
bool get isPlatformDark =>
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark;
const grayColorFilter = ColorFilter.matrix(<double>[
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0,
0,
0,
1,
0,
]);

View File

@ -1,7 +1,5 @@
part of 'dht_record_pool.dart';
const _sfListen = 'listen';
@immutable
class DHTRecordWatchChange extends Equatable {
const DHTRecordWatchChange(
@ -41,7 +39,7 @@ enum DHTRecordRefreshMode {
class DHTRecord implements DHTDeleteable<DHTRecord> {
DHTRecord._(
{required VeilidRoutingContext routingContext,
required SharedDHTRecordData sharedDHTRecordData,
required _SharedDHTRecordData sharedDHTRecordData,
required int defaultSubkey,
required KeyPair? writer,
required VeilidCrypto crypto,
@ -241,7 +239,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
// if so, shortcut and don't bother decrypting it
if (newValueData.data.equals(encryptedNewValue)) {
if (isUpdated) {
DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey);
DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey);
}
return null;
}
@ -251,7 +249,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
await (crypto ?? _crypto).decrypt(newValueData.data);
if (isUpdated) {
DHTRecordPool.instance
.processLocalValueChange(key, decryptedNewValue, subkey);
._processLocalValueChange(key, decryptedNewValue, subkey);
}
return decryptedNewValue;
}
@ -298,7 +296,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
final isUpdated = newValueData.seq != lastSeq;
if (isUpdated) {
DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey);
DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey);
}
}
@ -419,7 +417,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
// Set up watch requirements which will get picked up by the next tick
final oldWatchState = watchState;
watchState =
WatchState(subkeys: subkeys, expiration: expiration, count: count);
_WatchState(subkeys: subkeys, expiration: expiration, count: count);
if (oldWatchState != watchState) {
_sharedDHTRecordData.needsWatchStateUpdate = true;
}
@ -544,7 +542,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
//////////////////////////////////////////////////////////////
final SharedDHTRecordData _sharedDHTRecordData;
final _SharedDHTRecordData _sharedDHTRecordData;
final VeilidRoutingContext _routingContext;
final int _defaultSubkey;
final KeyPair? _writer;
@ -554,5 +552,5 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
int _openCount;
StreamController<DHTRecordWatchChange>? _watchController;
@internal
WatchState? watchState;
_WatchState? watchState;
}

View File

@ -0,0 +1,77 @@
part of 'dht_record_pool.dart';
const int _watchBackoffMultiplier = 2;
const int _watchBackoffMax = 30;
const int? _defaultWatchDurationSecs = null; // 600
const int _watchRenewalNumerator = 4;
const int _watchRenewalDenominator = 5;
// DHT crypto domain
const String _cryptoDomainDHT = 'dht';
// Singlefuture keys
const _sfPollWatch = '_pollWatch';
const _sfListen = 'listen';
/// Watch state
@immutable
class _WatchState extends Equatable {
const _WatchState(
{required this.subkeys,
required this.expiration,
required this.count,
this.realExpiration,
this.renewalTime});
final List<ValueSubkeyRange>? subkeys;
final Timestamp? expiration;
final int? count;
final Timestamp? realExpiration;
final Timestamp? renewalTime;
@override
List<Object?> get props =>
[subkeys, expiration, count, realExpiration, renewalTime];
}
/// Data shared amongst all DHTRecord instances
class _SharedDHTRecordData {
_SharedDHTRecordData(
{required this.recordDescriptor,
required this.defaultWriter,
required this.defaultRoutingContext});
DHTRecordDescriptor recordDescriptor;
KeyPair? defaultWriter;
VeilidRoutingContext defaultRoutingContext;
bool needsWatchStateUpdate = false;
_WatchState? unionWatchState;
}
// Per opened record data
class _OpenedRecordInfo {
_OpenedRecordInfo(
{required DHTRecordDescriptor recordDescriptor,
required KeyPair? defaultWriter,
required VeilidRoutingContext defaultRoutingContext})
: shared = _SharedDHTRecordData(
recordDescriptor: recordDescriptor,
defaultWriter: defaultWriter,
defaultRoutingContext: defaultRoutingContext);
_SharedDHTRecordData shared;
Set<DHTRecord> records = {};
String get debugNames {
final r = records.toList()
..sort((a, b) => a.key.toString().compareTo(b.key.toString()));
return '[${r.map((x) => x.debugName).join(',')}]';
}
String get details {
final r = records.toList()
..sort((a, b) => a.key.toString().compareTo(b.key.toString()));
return '[${r.map((x) => "writer=${x._writer} "
"defaultSubkey=${x._defaultSubkey}").join(',')}]';
}
String get sharedDetails => shared.toString();
}

View File

@ -8,8 +8,7 @@ import 'package:protobuf/protobuf.dart';
import 'table_db.dart';
class PersistentQueue<T extends GeneratedMessage>
/*extends Cubit<AsyncValue<IList<T>>>*/ with
TableDBBackedFromBuffer<IList<T>> {
with TableDBBackedFromBuffer<IList<T>> {
//
PersistentQueue(
{required String table,