tabledb array work

This commit is contained in:
Christien Rioux 2024-05-25 22:46:43 -04:00
parent 83c8715742
commit 5d89de9bfe
45 changed files with 3022 additions and 1035 deletions

View File

@ -24,7 +24,7 @@ class ActiveAccountInfo {
return KeyPair(key: identityKey, secret: identitySecret.value);
}
Future<DHTRecordCrypto> makeConversationCrypto(
Future<VeilidCrypto> makeConversationCrypto(
TypedKey remoteIdentityPublicKey) async {
final identitySecret = userLogin.identitySecret;
final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
@ -33,8 +33,8 @@ class ActiveAccountInfo {
identitySecret.value,
utf8.encode('VeilidChat Conversation'));
final messagesCrypto = await DHTRecordCryptoPrivate.fromSecret(
identitySecret.kind, sharedSecret);
final messagesCrypto =
await VeilidCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret);
return messagesCrypto;
}

View File

@ -120,9 +120,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
Future<void> _initSentMessagesCubit() async {
final writer = _activeAccountInfo.conversationWriter;
_sentMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openWrite(
_localMessagesRecordKey, writer,
_sentMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer,
debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::'
'SentMessages',
parent: _localConversationRecordKey,
@ -135,8 +134,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Open remote messages key
Future<void> _initRcvdMessagesCubit() async {
_rcvdMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey,
_rcvdMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openRead(_remoteMessagesRecordKey,
debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::'
'RcvdMessages',
parent: _remoteConversationRecordKey,
@ -152,8 +151,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
_reconciledMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openOwned(_reconciledChatRecord,
_reconciledMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openOwned(_reconciledChatRecord,
debugName:
'SingleContactMessagesCubit::_initReconciledMessagesCubit::'
'ReconciledMessages',
@ -166,10 +165,24 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
////////////////////////////////////////////////////////////////////////////
// Set the tail position of the log for pagination.
// If tail is 0, the end of the log is used.
// If tail is negative, the position is subtracted from the current log
// length.
// If tail is positive, the position is absolute from the head of the log
// If follow is enabled, the tail offset will update when the log changes
Future<void> setWindow(
{int? tail, int? count, bool? follow, bool forceRefresh = false}) async {
await _initWait();
await _reconciledMessagesCubit!.setWindow(
tail: tail, count: count, follow: follow, forceRefresh: forceRefresh);
}
////////////////////////////////////////////////////////////////////////////
// Called when the sent messages cubit gets a change
// This will re-render when messages are sent from another machine
void _updateSentMessagesState(
DHTShortArrayBusyState<proto.Message> avmessages) {
void _updateSentMessagesState(DHTLogBusyState<proto.Message> avmessages) {
final sentMessages = avmessages.state.asData?.value;
if (sentMessages == null) {
return;
@ -182,27 +195,52 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
}
// Called when the received messages cubit gets a change
void _updateRcvdMessagesState(
DHTShortArrayBusyState<proto.Message> avmessages) {
void _updateRcvdMessagesState(DHTLogBusyState<proto.Message> avmessages) {
final rcvdMessages = avmessages.state.asData?.value;
if (rcvdMessages == null) {
return;
}
// Add remote messages updates to queue to process asynchronously
// Ignore offline state because remote messages are always fully delivered
// This may happen once per client but should be idempotent
_unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value));
singleFuture(_rcvdMessagesCubit!, () async {
// Get the timestamp of our most recent reconciled message
final lastReconciledMessageTs =
await _reconciledMessagesCubit!.operate((r) async {
final len = r.length;
if (len == 0) {
return null;
} else {
final lastMessage =
await r.getItemProtobuf(proto.Message.fromBuffer, len - 1);
if (lastMessage == null) {
throw StateError('should have gotten last message');
}
return lastMessage.timestamp;
}
});
// Update the view
_renderState();
// Find oldest message we have not yet reconciled
// // Go through all the ones from the cubit state first since we've already
// // gotten them from the DHT
// for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) {
// //
// }
// // Add remote messages updates to queue to process asynchronously
// // Ignore offline state because remote messages are always fully delivered
// // This may happen once per client but should be idempotent
// _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value));
// Update the view
_renderState();
});
}
// Called when the reconciled messages list gets a change
// This can happen when multiple clients for the same identity are
// reading and reconciling the same remote chat
void _updateReconciledMessagesState(
DHTShortArrayBusyState<proto.Message> avmessages) {
DHTLogBusyState<proto.Message> avmessages) {
// Update the view
_renderState();
}
@ -210,85 +248,85 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Async process to reconcile messages sent or received in the background
Future<void> _processUnreconciledMessages(
IList<proto.Message> messages) async {
await _reconciledMessagesCubit!
.operateWrite((reconciledMessagesWriter) async {
await _reconcileMessagesInner(
reconciledMessagesWriter: reconciledMessagesWriter,
messages: messages);
});
// await _reconciledMessagesCubit!
// .operateAppendEventual((reconciledMessagesWriter) async {
// await _reconcileMessagesInner(
// reconciledMessagesWriter: reconciledMessagesWriter,
// messages: messages);
// });
}
// Async process to send messages in the background
Future<void> _processSendingMessages(IList<proto.Message> messages) async {
for (final message in messages) {
await _sentMessagesCubit!.operateWriteEventual(
(writer) => writer.tryAddItem(message.writeToBuffer()));
}
await _sentMessagesCubit!.operateAppendEventual((writer) =>
writer.tryAddItems(messages.map((m) => m.writeToBuffer()).toList()));
}
Future<void> _reconcileMessagesInner(
{required DHTRandomReadWrite reconciledMessagesWriter,
{required DHTLogWriteOperations reconciledMessagesWriter,
required IList<proto.Message> messages}) async {
// Ensure remoteMessages is sorted by timestamp
final newMessages = messages
.sort((a, b) => a.timestamp.compareTo(b.timestamp))
.removeDuplicates();
// // Ensure remoteMessages is sorted by timestamp
// final newMessages = messages
// .sort((a, b) => a.timestamp.compareTo(b.timestamp))
// .removeDuplicates();
// Existing messages will always be sorted by timestamp so merging is easy
final existingMessages = await reconciledMessagesWriter
.getItemRangeProtobuf(proto.Message.fromBuffer, 0);
if (existingMessages == null) {
throw Exception(
'Could not load existing reconciled messages at this time');
}
// // Existing messages will always be sorted by timestamp so merging is easy
// final existingMessages = await reconciledMessagesWriter
// .getItemRangeProtobuf(proto.Message.fromBuffer, 0);
// if (existingMessages == null) {
// throw Exception(
// 'Could not load existing reconciled messages at this time');
// }
var ePos = 0;
var nPos = 0;
while (ePos < existingMessages.length && nPos < newMessages.length) {
final existingMessage = existingMessages[ePos];
final newMessage = newMessages[nPos];
// var ePos = 0;
// var nPos = 0;
// while (ePos < existingMessages.length && nPos < newMessages.length) {
// final existingMessage = existingMessages[ePos];
// final newMessage = newMessages[nPos];
// If timestamp to insert is less than
// the current position, insert it here
final newTs = Timestamp.fromInt64(newMessage.timestamp);
final existingTs = Timestamp.fromInt64(existingMessage.timestamp);
final cmp = newTs.compareTo(existingTs);
if (cmp < 0) {
// New message belongs here
// // If timestamp to insert is less than
// // the current position, insert it here
// final newTs = Timestamp.fromInt64(newMessage.timestamp);
// final existingTs = Timestamp.fromInt64(existingMessage.timestamp);
// final cmp = newTs.compareTo(existingTs);
// if (cmp < 0) {
// // New message belongs here
// Insert into dht backing array
await reconciledMessagesWriter.tryInsertItem(
ePos, newMessage.writeToBuffer());
// Insert into local copy as well for this operation
existingMessages.insert(ePos, newMessage);
// // Insert into dht backing array
// await reconciledMessagesWriter.tryInsertItem(
// ePos, newMessage.writeToBuffer());
// // Insert into local copy as well for this operation
// existingMessages.insert(ePos, newMessage);
// Next message
nPos++;
ePos++;
} else if (cmp == 0) {
// Duplicate, skip
nPos++;
ePos++;
} else if (cmp > 0) {
// New message belongs later
ePos++;
}
}
// If there are any new messages left, append them all
while (nPos < newMessages.length) {
final newMessage = newMessages[nPos];
// // Next message
// nPos++;
// ePos++;
// } else if (cmp == 0) {
// // Duplicate, skip
// nPos++;
// ePos++;
// } else if (cmp > 0) {
// // New message belongs later
// ePos++;
// }
// }
// // If there are any new messages left, append them all
// while (nPos < newMessages.length) {
// final newMessage = newMessages[nPos];
// Append to dht backing array
await reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer());
// Insert into local copy as well for this operation
existingMessages.add(newMessage);
// // Append to dht backing array
// await reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer());
// // Insert into local copy as well for this operation
// existingMessages.add(newMessage);
nPos++;
}
// nPos++;
// }
}
// Produce a state for this cubit from the input cubits and queues
void _renderState() {
// xxx move into a singlefuture
// Get all reconciled messages
final reconciledMessages =
_reconciledMessagesCubit?.state.state.asData?.value;
@ -307,14 +345,14 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Generate state for each message
final sentMessagesMap =
IMap<Int64, DHTShortArrayElementState<proto.Message>>.fromValues(
IMap<Int64, DHTLogElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.timestamp,
values: sentMessages,
values: sentMessages.elements,
);
final reconciledMessagesMap =
IMap<Int64, DHTShortArrayElementState<proto.Message>>.fromValues(
IMap<Int64, DHTLogElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.timestamp,
values: reconciledMessages,
values: reconciledMessages.elements,
);
final sendingMessagesMap = IMap<Int64, proto.Message>.fromValues(
keyMapper: (x) => x.timestamp,
@ -372,9 +410,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
.sort((x, y) => x.key.compareTo(y.key));
final renderedState = messageKeys
.map((x) => MessageState(
author: x.value.message.author.toVeilid(),
content: x.value.message,
timestamp: Timestamp.fromInt64(x.key),
text: x.value.message.text,
sendState: x.value.sendState))
.toIList();
@ -400,17 +437,16 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
final TypedKey _remoteMessagesRecordKey;
final OwnedDHTRecordPointer _reconciledChatRecord;
late final DHTRecordCrypto _messagesCrypto;
late final VeilidCrypto _messagesCrypto;
DHTShortArrayCubit<proto.Message>? _sentMessagesCubit;
DHTShortArrayCubit<proto.Message>? _rcvdMessagesCubit;
DHTShortArrayCubit<proto.Message>? _reconciledMessagesCubit;
DHTLogCubit<proto.Message>? _sentMessagesCubit;
DHTLogCubit<proto.Message>? _rcvdMessagesCubit;
DHTLogCubit<proto.Message>? _reconciledMessagesCubit;
late final PersistentQueue<proto.Message> _unreconciledMessagesQueue;
late final PersistentQueue<proto.Message> _sendingMessagesQueue;
StreamSubscription<DHTShortArrayBusyState<proto.Message>>? _sentSubscription;
StreamSubscription<DHTShortArrayBusyState<proto.Message>>? _rcvdSubscription;
StreamSubscription<DHTShortArrayBusyState<proto.Message>>?
_reconciledSubscription;
StreamSubscription<DHTLogBusyState<proto.Message>>? _sentSubscription;
StreamSubscription<DHTLogBusyState<proto.Message>>? _rcvdSubscription;
StreamSubscription<DHTLogBusyState<proto.Message>>? _reconciledSubscription;
}

View File

@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../proto/proto.dart' as proto;
part 'message_state.freezed.dart';
part 'message_state.g.dart';
@ -23,9 +25,12 @@ enum MessageSendState {
@freezed
class MessageState with _$MessageState {
const factory MessageState({
required TypedKey author,
// Content of the message
@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson)
required proto.Message content,
// Received or delivered timestamp
required Timestamp timestamp,
required String text,
// The state of the mssage
required MessageSendState? sendState,
}) = _MessageState;

View File

@ -20,9 +20,12 @@ MessageState _$MessageStateFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$MessageState {
Typed<FixedEncodedString43> get author => throw _privateConstructorUsedError;
Timestamp get timestamp => throw _privateConstructorUsedError;
String get text => throw _privateConstructorUsedError;
// Content of the message
@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson)
proto.Message get content =>
throw _privateConstructorUsedError; // Received or delivered timestamp
Timestamp get timestamp =>
throw _privateConstructorUsedError; // The state of the mssage
MessageSendState? get sendState => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -38,9 +41,9 @@ abstract class $MessageStateCopyWith<$Res> {
_$MessageStateCopyWithImpl<$Res, MessageState>;
@useResult
$Res call(
{Typed<FixedEncodedString43> author,
{@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson)
proto.Message content,
Timestamp timestamp,
String text,
MessageSendState? sendState});
}
@ -57,24 +60,19 @@ class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? author = null,
Object? content = null,
Object? timestamp = null,
Object? text = null,
Object? sendState = freezed,
}) {
return _then(_value.copyWith(
author: null == author
? _value.author
: author // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as proto.Message,
timestamp: null == timestamp
? _value.timestamp
: timestamp // ignore: cast_nullable_to_non_nullable
as Timestamp,
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
sendState: freezed == sendState
? _value.sendState
: sendState // ignore: cast_nullable_to_non_nullable
@ -92,9 +90,9 @@ abstract class _$$MessageStateImplCopyWith<$Res>
@override
@useResult
$Res call(
{Typed<FixedEncodedString43> author,
{@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson)
proto.Message content,
Timestamp timestamp,
String text,
MessageSendState? sendState});
}
@ -109,24 +107,19 @@ class __$$MessageStateImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? author = null,
Object? content = null,
Object? timestamp = null,
Object? text = null,
Object? sendState = freezed,
}) {
return _then(_$MessageStateImpl(
author: null == author
? _value.author
: author // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as proto.Message,
timestamp: null == timestamp
? _value.timestamp
: timestamp // ignore: cast_nullable_to_non_nullable
as Timestamp,
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
sendState: freezed == sendState
? _value.sendState
: sendState // ignore: cast_nullable_to_non_nullable
@ -139,26 +132,28 @@ class __$$MessageStateImplCopyWithImpl<$Res>
@JsonSerializable()
class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState {
const _$MessageStateImpl(
{required this.author,
{@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson)
required this.content,
required this.timestamp,
required this.text,
required this.sendState});
factory _$MessageStateImpl.fromJson(Map<String, dynamic> json) =>
_$$MessageStateImplFromJson(json);
// Content of the message
@override
final Typed<FixedEncodedString43> author;
@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson)
final proto.Message content;
// Received or delivered timestamp
@override
final Timestamp timestamp;
@override
final String text;
// The state of the mssage
@override
final MessageSendState? sendState;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'MessageState(author: $author, timestamp: $timestamp, text: $text, sendState: $sendState)';
return 'MessageState(content: $content, timestamp: $timestamp, sendState: $sendState)';
}
@override
@ -166,9 +161,8 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'MessageState'))
..add(DiagnosticsProperty('author', author))
..add(DiagnosticsProperty('content', content))
..add(DiagnosticsProperty('timestamp', timestamp))
..add(DiagnosticsProperty('text', text))
..add(DiagnosticsProperty('sendState', sendState));
}
@ -177,18 +171,16 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MessageStateImpl &&
(identical(other.author, author) || other.author == author) &&
(identical(other.content, content) || other.content == content) &&
(identical(other.timestamp, timestamp) ||
other.timestamp == timestamp) &&
(identical(other.text, text) || other.text == text) &&
(identical(other.sendState, sendState) ||
other.sendState == sendState));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, author, timestamp, text, sendState);
int get hashCode => Object.hash(runtimeType, content, timestamp, sendState);
@JsonKey(ignore: true)
@override
@ -206,21 +198,20 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState {
abstract class _MessageState implements MessageState {
const factory _MessageState(
{required final Typed<FixedEncodedString43> author,
{@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson)
required final proto.Message content,
required final Timestamp timestamp,
required final String text,
required final MessageSendState? sendState}) = _$MessageStateImpl;
factory _MessageState.fromJson(Map<String, dynamic> json) =
_$MessageStateImpl.fromJson;
@override
Typed<FixedEncodedString43> get author;
@override
@override // Content of the message
@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson)
proto.Message get content;
@override // Received or delivered timestamp
Timestamp get timestamp;
@override
String get text;
@override
@override // The state of the mssage
MessageSendState? get sendState;
@override
@JsonKey(ignore: true)

View File

@ -8,9 +8,8 @@ part of 'message_state.dart';
_$MessageStateImpl _$$MessageStateImplFromJson(Map<String, dynamic> json) =>
_$MessageStateImpl(
author: Typed<FixedEncodedString43>.fromJson(json['author']),
content: messageFromJson(json['content'] as Map<String, dynamic>),
timestamp: Timestamp.fromJson(json['timestamp']),
text: json['text'] as String,
sendState: json['send_state'] == null
? null
: MessageSendState.fromJson(json['send_state']),
@ -18,8 +17,7 @@ _$MessageStateImpl _$$MessageStateImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$MessageStateImplToJson(_$MessageStateImpl instance) =>
<String, dynamic>{
'author': instance.author.toJson(),
'content': messageToJson(instance.content),
'timestamp': instance.timestamp.toJson(),
'text': instance.text,
'send_state': instance.sendState?.toJson(),
};

View File

@ -65,7 +65,7 @@ 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(
final reconciledChatRecord = await (await DHTLog.create(
debugName:
'ChatListCubit::getOrCreateChatSingleContact::ReconciledChat',
parent: accountRecordKey))

View File

@ -121,7 +121,7 @@ class ContactInvitationListCubit
schema: DHTSchema.smpl(oCnt: 1, members: [
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
]),
crypto: const DHTRecordCryptoPublic()))
crypto: const VeilidCryptoPublic()))
.deleteScope((contactRequestInbox) async {
// Store ContactRequest in owner subkey
await contactRequestInbox.eventualWriteProtobuf(creq);
@ -129,7 +129,7 @@ class ContactInvitationListCubit
await contactRequestInbox.eventualWriteBytes(Uint8List(0),
subkey: 1,
writer: contactRequestWriter,
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(
crypto: await VeilidCryptoPrivate.fromTypedKeyPair(
TypedKeyPair.fromKeyPair(
contactRequestInbox.key.kind, contactRequestWriter)));

View File

@ -37,7 +37,7 @@ class ContactRequestInboxCubit
return pool.openRecordRead(recordKey,
debugName: 'ContactRequestInboxCubit::_open::'
'ContactRequestInbox',
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
crypto: await VeilidCryptoPrivate.fromTypedKeyPair(writer),
parent: accountRecordKey,
defaultSubkey: 1);
}

View File

@ -285,13 +285,13 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationKey,
required FutureOr<T> Function(DHTShortArray) callback,
required FutureOr<T> Function(DHTLog) callback,
}) async {
final crypto =
await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey);
final writer = activeAccountInfo.conversationWriter;
return (await DHTShortArray.create(
return (await DHTLog.create(
debugName: 'ConversationCubit::initLocalMessages::LocalMessages',
parent: localConversationKey,
crypto: crypto,
@ -327,7 +327,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
return update;
}
Future<DHTRecordCrypto> _cachedConversationCrypto() async {
Future<VeilidCrypto> _cachedConversationCrypto() async {
var conversationCrypto = _conversationCrypto;
if (conversationCrypto != null) {
return conversationCrypto;
@ -350,6 +350,6 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
ConversationState _incrementalState = const ConversationState(
localConversation: null, remoteConversation: null);
//
DHTRecordCrypto? _conversationCrypto;
VeilidCrypto? _conversationCrypto;
final WaitSet<void> _initWait = WaitSet();
}

12
lib/proto/extensions.dart Normal file
View File

@ -0,0 +1,12 @@
import 'proto.dart' as proto;
proto.Message messageFromJson(Map<String, dynamic> j) =>
proto.Message.create()..mergeFromJsonMap(j);
Map<String, dynamic> messageToJson(proto.Message m) => m.writeToJsonMap();
proto.ReconciledMessage reconciledMessageFromJson(Map<String, dynamic> j) =>
proto.ReconciledMessage.create()..mergeFromJsonMap(j);
Map<String, dynamic> reconciledMessageToJson(proto.ReconciledMessage m) =>
m.writeToJsonMap();

View File

@ -1,5 +1,7 @@
export 'package:veilid_support/dht_support/proto/proto.dart';
export 'package:veilid_support/proto/proto.dart';
export 'extensions.dart';
export 'veilidchat.pb.dart';
export 'veilidchat.pbenum.dart';
export 'veilidchat.pbjson.dart';

File diff suppressed because it is too large Load Diff

View File

@ -13,23 +13,6 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class AttachmentKind extends $pb.ProtobufEnum {
static const AttachmentKind ATTACHMENT_KIND_UNSPECIFIED = AttachmentKind._(0, _omitEnumNames ? '' : 'ATTACHMENT_KIND_UNSPECIFIED');
static const AttachmentKind ATTACHMENT_KIND_FILE = AttachmentKind._(1, _omitEnumNames ? '' : 'ATTACHMENT_KIND_FILE');
static const AttachmentKind ATTACHMENT_KIND_IMAGE = AttachmentKind._(2, _omitEnumNames ? '' : 'ATTACHMENT_KIND_IMAGE');
static const $core.List<AttachmentKind> values = <AttachmentKind> [
ATTACHMENT_KIND_UNSPECIFIED,
ATTACHMENT_KIND_FILE,
ATTACHMENT_KIND_IMAGE,
];
static final $core.Map<$core.int, AttachmentKind> _byValue = $pb.ProtobufEnum.initByValue(values);
static AttachmentKind? valueOf($core.int value) => _byValue[value];
const AttachmentKind._($core.int v, $core.String n) : super(v, n);
}
class Availability extends $pb.ProtobufEnum {
static const Availability AVAILABILITY_UNSPECIFIED = Availability._(0, _omitEnumNames ? '' : 'AVAILABILITY_UNSPECIFIED');
static const Availability AVAILABILITY_OFFLINE = Availability._(1, _omitEnumNames ? '' : 'AVAILABILITY_OFFLINE');
@ -51,23 +34,6 @@ class Availability extends $pb.ProtobufEnum {
const Availability._($core.int v, $core.String n) : super(v, n);
}
class ChatType extends $pb.ProtobufEnum {
static const ChatType CHAT_TYPE_UNSPECIFIED = ChatType._(0, _omitEnumNames ? '' : 'CHAT_TYPE_UNSPECIFIED');
static const ChatType SINGLE_CONTACT = ChatType._(1, _omitEnumNames ? '' : 'SINGLE_CONTACT');
static const ChatType GROUP = ChatType._(2, _omitEnumNames ? '' : 'GROUP');
static const $core.List<ChatType> values = <ChatType> [
CHAT_TYPE_UNSPECIFIED,
SINGLE_CONTACT,
GROUP,
];
static final $core.Map<$core.int, ChatType> _byValue = $pb.ProtobufEnum.initByValue(values);
static ChatType? valueOf($core.int value) => _byValue[value];
const ChatType._($core.int v, $core.String n) : super(v, n);
}
class EncryptionKeyType extends $pb.ProtobufEnum {
static const EncryptionKeyType ENCRYPTION_KEY_TYPE_UNSPECIFIED = EncryptionKeyType._(0, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_UNSPECIFIED');
static const EncryptionKeyType ENCRYPTION_KEY_TYPE_NONE = EncryptionKeyType._(1, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_NONE');
@ -87,5 +53,26 @@ class EncryptionKeyType extends $pb.ProtobufEnum {
const EncryptionKeyType._($core.int v, $core.String n) : super(v, n);
}
class Scope extends $pb.ProtobufEnum {
static const Scope WATCHERS = Scope._(0, _omitEnumNames ? '' : 'WATCHERS');
static const Scope MODERATED = Scope._(1, _omitEnumNames ? '' : 'MODERATED');
static const Scope TALKERS = Scope._(2, _omitEnumNames ? '' : 'TALKERS');
static const Scope MODERATORS = Scope._(3, _omitEnumNames ? '' : 'MODERATORS');
static const Scope ADMINS = Scope._(4, _omitEnumNames ? '' : 'ADMINS');
static const $core.List<Scope> values = <Scope> [
WATCHERS,
MODERATED,
TALKERS,
MODERATORS,
ADMINS,
];
static final $core.Map<$core.int, Scope> _byValue = $pb.ProtobufEnum.initByValue(values);
static Scope? valueOf($core.int value) => _byValue[value];
const Scope._($core.int v, $core.String n) : super(v, n);
}
const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');

View File

@ -13,21 +13,6 @@ import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use attachmentKindDescriptor instead')
const AttachmentKind$json = {
'1': 'AttachmentKind',
'2': [
{'1': 'ATTACHMENT_KIND_UNSPECIFIED', '2': 0},
{'1': 'ATTACHMENT_KIND_FILE', '2': 1},
{'1': 'ATTACHMENT_KIND_IMAGE', '2': 2},
],
};
/// Descriptor for `AttachmentKind`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List attachmentKindDescriptor = $convert.base64Decode(
'Cg5BdHRhY2htZW50S2luZBIfChtBVFRBQ0hNRU5UX0tJTkRfVU5TUEVDSUZJRUQQABIYChRBVF'
'RBQ0hNRU5UX0tJTkRfRklMRRABEhkKFUFUVEFDSE1FTlRfS0lORF9JTUFHRRAC');
@$core.Deprecated('Use availabilityDescriptor instead')
const Availability$json = {
'1': 'Availability',
@ -46,21 +31,6 @@ final $typed_data.Uint8List availabilityDescriptor = $convert.base64Decode(
'lMSVRZX09GRkxJTkUQARIVChFBVkFJTEFCSUxJVFlfRlJFRRACEhUKEUFWQUlMQUJJTElUWV9C'
'VVNZEAMSFQoRQVZBSUxBQklMSVRZX0FXQVkQBA==');
@$core.Deprecated('Use chatTypeDescriptor instead')
const ChatType$json = {
'1': 'ChatType',
'2': [
{'1': 'CHAT_TYPE_UNSPECIFIED', '2': 0},
{'1': 'SINGLE_CONTACT', '2': 1},
{'1': 'GROUP', '2': 2},
],
};
/// Descriptor for `ChatType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List chatTypeDescriptor = $convert.base64Decode(
'CghDaGF0VHlwZRIZChVDSEFUX1RZUEVfVU5TUEVDSUZJRUQQABISCg5TSU5HTEVfQ09OVEFDVB'
'ABEgkKBUdST1VQEAI=');
@$core.Deprecated('Use encryptionKeyTypeDescriptor instead')
const EncryptionKeyType$json = {
'1': 'EncryptionKeyType',
@ -78,43 +48,249 @@ final $typed_data.Uint8List encryptionKeyTypeDescriptor = $convert.base64Decode(
'ASHAoYRU5DUllQVElPTl9LRVlfVFlQRV9OT05FEAESGwoXRU5DUllQVElPTl9LRVlfVFlQRV9Q'
'SU4QAhIgChxFTkNSWVBUSU9OX0tFWV9UWVBFX1BBU1NXT1JEEAM=');
@$core.Deprecated('Use scopeDescriptor instead')
const Scope$json = {
'1': 'Scope',
'2': [
{'1': 'WATCHERS', '2': 0},
{'1': 'MODERATED', '2': 1},
{'1': 'TALKERS', '2': 2},
{'1': 'MODERATORS', '2': 3},
{'1': 'ADMINS', '2': 4},
],
};
/// Descriptor for `Scope`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List scopeDescriptor = $convert.base64Decode(
'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0'
'RFUkFUT1JTEAMSCgoGQURNSU5TEAQ=');
@$core.Deprecated('Use attachmentDescriptor instead')
const Attachment$json = {
'1': 'Attachment',
'2': [
{'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.AttachmentKind', '10': 'kind'},
{'1': 'mime', '3': 2, '4': 1, '5': 9, '10': 'mime'},
{'1': 'name', '3': 3, '4': 1, '5': 9, '10': 'name'},
{'1': 'content', '3': 4, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'},
{'1': 'signature', '3': 5, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'},
{'1': 'media', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.AttachmentMedia', '9': 0, '10': 'media'},
{'1': 'signature', '3': 2, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'},
],
'8': [
{'1': 'kind'},
],
};
/// Descriptor for `Attachment`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List attachmentDescriptor = $convert.base64Decode(
'CgpBdHRhY2htZW50Ei4KBGtpbmQYASABKA4yGi52ZWlsaWRjaGF0LkF0dGFjaG1lbnRLaW5kUg'
'RraW5kEhIKBG1pbWUYAiABKAlSBG1pbWUSEgoEbmFtZRgDIAEoCVIEbmFtZRIsCgdjb250ZW50'
'GAQgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VSB2NvbnRlbnQSLwoJc2lnbmF0dXJlGAUgASgLMh'
'EudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0dXJl');
'CgpBdHRhY2htZW50EjMKBW1lZGlhGAEgASgLMhsudmVpbGlkY2hhdC5BdHRhY2htZW50TWVkaW'
'FIAFIFbWVkaWESLwoJc2lnbmF0dXJlGAIgASgLMhEudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0'
'dXJlQgYKBGtpbmQ=');
@$core.Deprecated('Use attachmentMediaDescriptor instead')
const AttachmentMedia$json = {
'1': 'AttachmentMedia',
'2': [
{'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'},
{'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
{'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'},
],
};
/// Descriptor for `AttachmentMedia`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode(
'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW'
'1lEiwKB2NvbnRlbnQYAyABKAsyEi5kaHQuRGF0YVJlZmVyZW5jZVIHY29udGVudA==');
@$core.Deprecated('Use permissionsDescriptor instead')
const Permissions$json = {
'1': 'Permissions',
'2': [
{'1': 'can_add_members', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.Scope', '10': 'canAddMembers'},
{'1': 'can_edit_info', '3': 2, '4': 1, '5': 14, '6': '.veilidchat.Scope', '10': 'canEditInfo'},
{'1': 'moderated', '3': 3, '4': 1, '5': 8, '10': 'moderated'},
],
};
/// Descriptor for `Permissions`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List permissionsDescriptor = $convert.base64Decode(
'CgtQZXJtaXNzaW9ucxI5Cg9jYW5fYWRkX21lbWJlcnMYASABKA4yES52ZWlsaWRjaGF0LlNjb3'
'BlUg1jYW5BZGRNZW1iZXJzEjUKDWNhbl9lZGl0X2luZm8YAiABKA4yES52ZWlsaWRjaGF0LlNj'
'b3BlUgtjYW5FZGl0SW5mbxIcCgltb2RlcmF0ZWQYAyABKAhSCW1vZGVyYXRlZA==');
@$core.Deprecated('Use membershipDescriptor instead')
const Membership$json = {
'1': 'Membership',
'2': [
{'1': 'watchers', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'watchers'},
{'1': 'moderated', '3': 2, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'moderated'},
{'1': 'talkers', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'talkers'},
{'1': 'moderators', '3': 4, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'moderators'},
{'1': 'admins', '3': 5, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'admins'},
],
};
/// Descriptor for `Membership`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List membershipDescriptor = $convert.base64Decode(
'CgpNZW1iZXJzaGlwEiwKCHdhdGNoZXJzGAEgAygLMhAudmVpbGlkLlR5cGVkS2V5Ugh3YXRjaG'
'VycxIuCgltb2RlcmF0ZWQYAiADKAsyEC52ZWlsaWQuVHlwZWRLZXlSCW1vZGVyYXRlZBIqCgd0'
'YWxrZXJzGAMgAygLMhAudmVpbGlkLlR5cGVkS2V5Ugd0YWxrZXJzEjAKCm1vZGVyYXRvcnMYBC'
'ADKAsyEC52ZWlsaWQuVHlwZWRLZXlSCm1vZGVyYXRvcnMSKAoGYWRtaW5zGAUgAygLMhAudmVp'
'bGlkLlR5cGVkS2V5UgZhZG1pbnM=');
@$core.Deprecated('Use chatSettingsDescriptor instead')
const ChatSettings$json = {
'1': 'ChatSettings',
'2': [
{'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'},
{'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'},
{'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '9': 0, '10': 'icon', '17': true},
{'1': 'default_expiration', '3': 4, '4': 1, '5': 4, '10': 'defaultExpiration'},
],
'8': [
{'1': '_icon'},
],
};
/// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode(
'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS'
'gJUgtkZXNjcmlwdGlvbhIrCgRpY29uGAMgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VIAFIEaWNv'
'bogBARItChJkZWZhdWx0X2V4cGlyYXRpb24YBCABKARSEWRlZmF1bHRFeHBpcmF0aW9uQgcKBV'
'9pY29u');
@$core.Deprecated('Use messageDescriptor instead')
const Message$json = {
'1': 'Message',
'2': [
{'1': 'author', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'author'},
{'1': 'timestamp', '3': 2, '4': 1, '5': 4, '10': 'timestamp'},
{'1': 'text', '3': 3, '4': 1, '5': 9, '10': 'text'},
{'1': 'signature', '3': 4, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'},
{'1': 'attachments', '3': 5, '4': 3, '5': 11, '6': '.veilidchat.Attachment', '10': 'attachments'},
{'1': 'id', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'id'},
{'1': 'author', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'author'},
{'1': 'timestamp', '3': 3, '4': 1, '5': 4, '10': 'timestamp'},
{'1': 'text', '3': 4, '4': 1, '5': 11, '6': '.veilidchat.Message.Text', '9': 0, '10': 'text'},
{'1': 'secret', '3': 5, '4': 1, '5': 11, '6': '.veilidchat.Message.Secret', '9': 0, '10': 'secret'},
{'1': 'delete', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlDelete', '9': 0, '10': 'delete'},
{'1': 'clear', '3': 7, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlClear', '9': 0, '10': 'clear'},
{'1': 'settings', '3': 8, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlSettings', '9': 0, '10': 'settings'},
{'1': 'permissions', '3': 9, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlPermissions', '9': 0, '10': 'permissions'},
{'1': 'membership', '3': 10, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlMembership', '9': 0, '10': 'membership'},
{'1': 'moderation', '3': 11, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlModeration', '9': 0, '10': 'moderation'},
{'1': 'signature', '3': 12, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'},
],
'3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlClear$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json],
'8': [
{'1': 'kind'},
],
};
@$core.Deprecated('Use messageDescriptor instead')
const Message_Text$json = {
'1': 'Text',
'2': [
{'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},
{'1': 'topic', '3': 2, '4': 1, '5': 9, '10': 'topic'},
{'1': 'reply_id', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'replyId'},
{'1': 'expiration', '3': 4, '4': 1, '5': 4, '10': 'expiration'},
{'1': 'view_limit', '3': 5, '4': 1, '5': 4, '10': 'viewLimit'},
{'1': 'attachments', '3': 6, '4': 3, '5': 11, '6': '.veilidchat.Attachment', '10': 'attachments'},
],
};
@$core.Deprecated('Use messageDescriptor instead')
const Message_Secret$json = {
'1': 'Secret',
'2': [
{'1': 'ciphertext', '3': 1, '4': 1, '5': 12, '10': 'ciphertext'},
{'1': 'expiration', '3': 2, '4': 1, '5': 4, '10': 'expiration'},
],
};
@$core.Deprecated('Use messageDescriptor instead')
const Message_ControlDelete$json = {
'1': 'ControlDelete',
'2': [
{'1': 'ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'ids'},
],
};
@$core.Deprecated('Use messageDescriptor instead')
const Message_ControlClear$json = {
'1': 'ControlClear',
'2': [
{'1': 'timestamp', '3': 1, '4': 1, '5': 4, '10': 'timestamp'},
],
};
@$core.Deprecated('Use messageDescriptor instead')
const Message_ControlSettings$json = {
'1': 'ControlSettings',
'2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
],
};
@$core.Deprecated('Use messageDescriptor instead')
const Message_ControlPermissions$json = {
'1': 'ControlPermissions',
'2': [
{'1': 'permissions', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Permissions', '10': 'permissions'},
],
};
@$core.Deprecated('Use messageDescriptor instead')
const Message_ControlMembership$json = {
'1': 'ControlMembership',
'2': [
{'1': 'membership', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'},
],
};
@$core.Deprecated('Use messageDescriptor instead')
const Message_ControlModeration$json = {
'1': 'ControlModeration',
'2': [
{'1': 'accepted_ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'acceptedIds'},
{'1': 'rejected_ids', '3': 2, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'rejectedIds'},
],
};
/// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List messageDescriptor = $convert.base64Decode(
'CgdNZXNzYWdlEigKBmF1dGhvchgBIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIGYXV0aG9yEhwKCX'
'RpbWVzdGFtcBgCIAEoBFIJdGltZXN0YW1wEhIKBHRleHQYAyABKAlSBHRleHQSLwoJc2lnbmF0'
'dXJlGAQgASgLMhEudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0dXJlEjgKC2F0dGFjaG1lbnRzGA'
'UgAygLMhYudmVpbGlkY2hhdC5BdHRhY2htZW50UgthdHRhY2htZW50cw==');
'CgdNZXNzYWdlEiAKAmlkGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5UgJpZBIoCgZhdXRob3IYAi'
'ABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBmF1dGhvchIcCgl0aW1lc3RhbXAYAyABKARSCXRpbWVz'
'dGFtcBIuCgR0ZXh0GAQgASgLMhgudmVpbGlkY2hhdC5NZXNzYWdlLlRleHRIAFIEdGV4dBI0Cg'
'ZzZWNyZXQYBSABKAsyGi52ZWlsaWRjaGF0Lk1lc3NhZ2UuU2VjcmV0SABSBnNlY3JldBI7CgZk'
'ZWxldGUYBiABKAsyIS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbERlbGV0ZUgAUgZkZWxldG'
'USOAoFY2xlYXIYByABKAsyIC52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbENsZWFySABSBWNs'
'ZWFyEkEKCHNldHRpbmdzGAggASgLMiMudmVpbGlkY2hhdC5NZXNzYWdlLkNvbnRyb2xTZXR0aW'
'5nc0gAUghzZXR0aW5ncxJKCgtwZXJtaXNzaW9ucxgJIAEoCzImLnZlaWxpZGNoYXQuTWVzc2Fn'
'ZS5Db250cm9sUGVybWlzc2lvbnNIAFILcGVybWlzc2lvbnMSRwoKbWVtYmVyc2hpcBgKIAEoCz'
'IlLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sTWVtYmVyc2hpcEgAUgptZW1iZXJzaGlwEkcK'
'Cm1vZGVyYXRpb24YCyABKAsyJS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb2'
'5IAFIKbW9kZXJhdGlvbhIvCglzaWduYXR1cmUYDCABKAsyES52ZWlsaWQuU2lnbmF0dXJlUglz'
'aWduYXR1cmUa1gEKBFRleHQSEgoEdGV4dBgBIAEoCVIEdGV4dBIUCgV0b3BpYxgCIAEoCVIFdG'
'9waWMSKwoIcmVwbHlfaWQYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSB3JlcGx5SWQSHgoKZXhw'
'aXJhdGlvbhgEIAEoBFIKZXhwaXJhdGlvbhIdCgp2aWV3X2xpbWl0GAUgASgEUgl2aWV3TGltaX'
'QSOAoLYXR0YWNobWVudHMYBiADKAsyFi52ZWlsaWRjaGF0LkF0dGFjaG1lbnRSC2F0dGFjaG1l'
'bnRzGkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYX'
'Rpb24YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52'
'ZWlsaWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sQ2xlYXISHAoJdGltZXN0YW1wGAEgASgEUg'
'l0aW1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlk'
'Y2hhdC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZX'
'JtaXNzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksK'
'EUNvbnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbW'
'JlcnNoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRz'
'GAEgAygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAi'
'ADKAsyEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ=');
@$core.Deprecated('Use reconciledMessageDescriptor instead')
const ReconciledMessage$json = {
'1': 'ReconciledMessage',
'2': [
{'1': 'content', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Message', '10': 'content'},
{'1': 'reconciled_time', '3': 2, '4': 1, '5': 4, '10': 'reconciledTime'},
],
};
/// Descriptor for `ReconciledMessage`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List reconciledMessageDescriptor = $convert.base64Decode(
'ChFSZWNvbmNpbGVkTWVzc2FnZRItCgdjb250ZW50GAEgASgLMhMudmVpbGlkY2hhdC5NZXNzYW'
'dlUgdjb250ZW50EicKD3JlY29uY2lsZWRfdGltZRgCIAEoBFIOcmVjb25jaWxlZFRpbWU=');
@$core.Deprecated('Use conversationDescriptor instead')
const Conversation$json = {
@ -132,6 +308,91 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode(
'JvZmlsZRIwChRpZGVudGl0eV9tYXN0ZXJfanNvbhgCIAEoCVISaWRlbnRpdHlNYXN0ZXJKc29u'
'EiwKCG1lc3NhZ2VzGAMgASgLMhAudmVpbGlkLlR5cGVkS2V5UghtZXNzYWdlcw==');
@$core.Deprecated('Use chatDescriptor instead')
const Chat$json = {
'1': 'Chat',
'2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
],
};
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode(
'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH'
'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5'
'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW'
'9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv'
'blJlY29yZEtleQ==');
@$core.Deprecated('Use groupChatDescriptor instead')
const GroupChat$json = {
'1': 'GroupChat',
'2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'remote_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'},
],
};
/// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode(
'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1'
'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls'
'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX'
'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl'
'cnNhdGlvblJlY29yZEtleXM=');
@$core.Deprecated('Use profileDescriptor instead')
const Profile$json = {
'1': 'Profile',
'2': [
{'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
{'1': 'pronouns', '3': 2, '4': 1, '5': 9, '10': 'pronouns'},
{'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'},
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
{'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'},
{'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true},
],
'8': [
{'1': '_avatar'},
],
};
/// Descriptor for `Profile`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ei'
'0KBmF2YXRhchgGIAEoCzIQLnZlaWxpZC5UeXBlZEtleUgAUgZhdmF0YXKIAQFCCQoHX2F2YXRh'
'cg==');
@$core.Deprecated('Use accountDescriptor instead')
const Account$json = {
'1': 'Account',
'2': [
{'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'},
{'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'},
{'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'},
{'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactList'},
{'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'},
{'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'chatList'},
{'1': 'group_chat_list', '3': 7, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'groupChatList'},
],
};
/// Descriptor for `Account`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
'CgdBY2NvdW50Ei0KB3Byb2ZpbGUYASABKAsyEy52ZWlsaWRjaGF0LlByb2ZpbGVSB3Byb2ZpbG'
'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfc2Vj'
'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRTZWMSPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk'
'93bmVkREhUUmVjb3JkUG9pbnRlclILY29udGFjdExpc3QSWAoaY29udGFjdF9pbnZpdGF0aW9u'
'X3JlY29yZHMYBSABKAsyGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhhjb250YWN0SW52aX'
'RhdGlvblJlY29yZHMSNwoJY2hhdF9saXN0GAYgASgLMhouZGh0Lk93bmVkREhUUmVjb3JkUG9p'
'bnRlclIIY2hhdExpc3QSQgoPZ3JvdXBfY2hhdF9saXN0GAcgASgLMhouZGh0Lk93bmVkREhUUm'
'Vjb3JkUG9pbnRlclINZ3JvdXBDaGF0TGlzdA==');
@$core.Deprecated('Use contactDescriptor instead')
const Contact$json = {
'1': 'Contact',
@ -158,68 +419,6 @@ final $typed_data.Uint8List contactDescriptor = $convert.base64Decode(
'lSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5EisKEXNob3dfYXZhaWxhYmlsaXR5GAcgASgI'
'UhBzaG93QXZhaWxhYmlsaXR5');
@$core.Deprecated('Use profileDescriptor instead')
const Profile$json = {
'1': 'Profile',
'2': [
{'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
{'1': 'pronouns', '3': 2, '4': 1, '5': 9, '10': 'pronouns'},
{'1': 'status', '3': 3, '4': 1, '5': 9, '10': 'status'},
{'1': 'availability', '3': 4, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'},
{'1': 'avatar', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true},
],
'8': [
{'1': '_avatar'},
],
};
/// Descriptor for `Profile`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
'5zEhYKBnN0YXR1cxgDIAEoCVIGc3RhdHVzEjwKDGF2YWlsYWJpbGl0eRgEIAEoDjIYLnZlaWxp'
'ZGNoYXQuQXZhaWxhYmlsaXR5UgxhdmFpbGFiaWxpdHkSLQoGYXZhdGFyGAUgASgLMhAudmVpbG'
'lkLlR5cGVkS2V5SABSBmF2YXRhcogBAUIJCgdfYXZhdGFy');
@$core.Deprecated('Use chatDescriptor instead')
const Chat$json = {
'1': 'Chat',
'2': [
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.ChatType', '10': 'type'},
{'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
{'1': 'reconciled_chat_record', '3': 3, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'reconciledChatRecord'},
],
};
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode(
'CgRDaGF0EigKBHR5cGUYASABKA4yFC52ZWlsaWRjaGF0LkNoYXRUeXBlUgR0eXBlElUKHnJlbW'
'90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleRgCIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVt'
'b3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElAKFnJlY29uY2lsZWRfY2hhdF9yZWNvcmQYAyABKA'
'syGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhRyZWNvbmNpbGVkQ2hhdFJlY29yZA==');
@$core.Deprecated('Use accountDescriptor instead')
const Account$json = {
'1': 'Account',
'2': [
{'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'},
{'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'},
{'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'},
{'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactList'},
{'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'},
{'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'chatList'},
],
};
/// Descriptor for `Account`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
'CgdBY2NvdW50Ei0KB3Byb2ZpbGUYASABKAsyEy52ZWlsaWRjaGF0LlByb2ZpbGVSB3Byb2ZpbG'
'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfc2Vj'
'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRTZWMSPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk'
'93bmVkREhUUmVjb3JkUG9pbnRlclILY29udGFjdExpc3QSWAoaY29udGFjdF9pbnZpdGF0aW9u'
'X3JlY29yZHMYBSABKAsyGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhhjb250YWN0SW52aX'
'RhdGlvblJlY29yZHMSNwoJY2hhdF9saXN0GAYgASgLMhouZGh0Lk93bmVkREhUUmVjb3JkUG9p'
'bnRlclIIY2hhdExpc3Q=');
@$core.Deprecated('Use contactInvitationDescriptor instead')
const ContactInvitation$json = {
'1': 'ContactInvitation',

View File

@ -1,51 +1,230 @@
////////////////////////////////////////////////////////////////////////////////////
// VeilidChat Protocol Buffer Definitions
//
// * Timestamps are in microseconds (us) since epoch
// * Durations are in microseconds (us)
////////////////////////////////////////////////////////////////////////////////////
syntax = "proto3";
package veilidchat;
import "veilid.proto";
import "dht.proto";
// AttachmentKind
// Enumeration of well-known attachment types
enum AttachmentKind {
ATTACHMENT_KIND_UNSPECIFIED = 0;
ATTACHMENT_KIND_FILE = 1;
ATTACHMENT_KIND_IMAGE = 2;
////////////////////////////////////////////////////////////////////////////////////
// Enumerations
////////////////////////////////////////////////////////////////////////////////////
// Contact availability
enum Availability {
AVAILABILITY_UNSPECIFIED = 0;
AVAILABILITY_OFFLINE = 1;
AVAILABILITY_FREE = 2;
AVAILABILITY_BUSY = 3;
AVAILABILITY_AWAY = 4;
}
// Encryption used on secret keys
enum EncryptionKeyType {
ENCRYPTION_KEY_TYPE_UNSPECIFIED = 0;
ENCRYPTION_KEY_TYPE_NONE = 1;
ENCRYPTION_KEY_TYPE_PIN = 2;
ENCRYPTION_KEY_TYPE_PASSWORD = 3;
}
// Scope of a chat
enum Scope {
// Can read chats but not send messages
WATCHERS = 0;
// Can send messages subject to moderation
// If moderation is disabled, this is equivalent to WATCHERS
MODERATED = 1;
// Can send messages without moderation
TALKERS = 2;
// Can moderate messages sent my members if moderation is enabled
MODERATORS = 3;
// Can perform all actions
ADMINS = 4;
}
////////////////////////////////////////////////////////////////////////////////////
// Attachments
////////////////////////////////////////////////////////////////////////////////////
// A single attachment
message Attachment {
// Type of the data
AttachmentKind kind = 1;
// MIME type of the data
string mime = 2;
// Title or filename
string name = 3;
// Pointer to the data content
dht.DataReference content = 4;
oneof kind {
AttachmentMedia media = 1;
}
// Author signature over all attachment fields and content fields and bytes
veilid.Signature signature = 5;
veilid.Signature signature = 2;
}
// A file, audio, image, or video attachment
message AttachmentMedia {
// MIME type of the data
string mime = 1;
// Title or filename
string name = 2;
// Pointer to the data content
dht.DataReference content = 3;
}
////////////////////////////////////////////////////////////////////////////////////
// Chat room controls
////////////////////////////////////////////////////////////////////////////////////
// Permissions of a chat
message Permissions {
// Parties in this scope or higher can add members to their own group or lower
Scope can_add_members = 1;
// Parties in this scope or higher can change the 'info' of a group
Scope can_edit_info = 2;
// If moderation is enabled or not.
bool moderated = 3;
}
// The membership of a chat
message Membership {
// Conversation keys for parties in the 'watchers' group
repeated veilid.TypedKey watchers = 1;
// Conversation keys for parties in the 'moderated' group
repeated veilid.TypedKey moderated = 2;
// Conversation keys for parties in the 'talkers' group
repeated veilid.TypedKey talkers = 3;
// Conversation keys for parties in the 'moderators' group
repeated veilid.TypedKey moderators = 4;
// Conversation keys for parties in the 'admins' group
repeated veilid.TypedKey admins = 5;
}
// The chat settings
message ChatSettings {
// Title for the chat
string title = 1;
// Description for the chat
string description = 2;
// Icon for the chat
optional dht.DataReference icon = 3;
// Default message expiration duration (in us)
uint64 default_expiration = 4;
}
////////////////////////////////////////////////////////////////////////////////////
// Messages
////////////////////////////////////////////////////////////////////////////////////
// A single message as part of a series of messages
message Message {
// Author of the message
veilid.TypedKey author = 1;
// Time the message was sent (us since epoch)
uint64 timestamp = 2;
// Text of the message
string text = 3;
// A text message
message Text {
// Text of the message
string text = 1;
// Topic of the message / Content warning
string topic = 2;
// Message id replied to
veilid.TypedKey reply_id = 3;
// Message expiration timestamp
uint64 expiration = 4;
// Message view limit before deletion
uint64 view_limit = 5;
// Attachments on the message
repeated Attachment attachments = 6;
}
// A secret message
message Secret {
// Text message protobuf encrypted by a key
bytes ciphertext = 1;
// Secret expiration timestamp
// This is the time after which an un-revealed secret will get deleted
uint64 expiration = 2;
}
// A 'delete' control message
// Deletes a set of messages by their ids
message ControlDelete {
repeated veilid.TypedKey ids = 1;
}
// A 'clear' control message
// Deletes a set of messages from before some timestamp
message ControlClear {
// The latest timestamp to delete messages before
// If this is zero then all messages are cleared
uint64 timestamp = 1;
}
// A 'change settings' control message
message ControlSettings {
ChatSettings settings = 1;
}
// A 'change permissions' control message
// Changes the permissions of a chat
message ControlPermissions {
Permissions permissions = 1;
}
// A 'change membership' control message
// Changes the
message ControlMembership {
Membership membership = 1;
}
// A 'moderation' control message
// Accepts or rejects a set of messages
message ControlModeration {
repeated veilid.TypedKey accepted_ids = 1;
repeated veilid.TypedKey rejected_ids = 2;
}
//////////////////////////////////////////////////////////////////////////
// Hash of previous message from the same author,
// including its previous hash.
// Also serves as a unique key for the message.
veilid.TypedKey id = 1;
// Author of the message (identity public key)
veilid.TypedKey author = 2;
// Time the message was sent according to sender
uint64 timestamp = 3;
// Message kind
oneof kind {
Text text = 4;
Secret secret = 5;
ControlDelete delete = 6;
ControlClear clear = 7;
ControlSettings settings = 8;
ControlPermissions permissions = 9;
ControlMembership membership = 10;
ControlModeration moderation = 11;
}
// Author signature over all of the fields and attachment signatures
veilid.Signature signature = 4;
// Attachments on the message
repeated Attachment attachments = 5;
veilid.Signature signature = 12;
}
// Locally stored messages for chats
message ReconciledMessage {
// The message as sent
Message content = 1;
// The timestamp the message was reconciled
uint64 reconciled_time = 2;
}
////////////////////////////////////////////////////////////////////////////////////
// Chats
////////////////////////////////////////////////////////////////////////////////////
// The means of direct communications that is synchronized between
// two users. Visible and encrypted for the other party.
// Includes communications for:
// * Profile changes
// * Identity changes
// * 1-1 chat messages
// * Group chat messages
//
// DHT Schema: SMPL(0,1,[identityPublicKey])
// DHT Key (UnicastOutbox): localConversation
@ -54,12 +233,84 @@ message Message {
message Conversation {
// Profile to publish to friend
Profile profile = 1;
// Identity master (JSON) to publish to friend
// Identity master (JSON) to publish to friend or chat room
string identity_master_json = 2;
// Messages DHTLog (xxx for now DHTShortArray)
// Messages DHTLog
veilid.TypedKey messages = 3;
}
// Either a 1-1 conversation or a group chat
// Privately encrypted, this is the local user's copy of the chat
message Chat {
// Settings
ChatSettings settings = 1;
// Conversation key for this user
veilid.TypedKey local_conversation_record_key = 2;
// Conversation key for the other party
veilid.TypedKey remote_conversation_record_key = 3;
}
// A group chat
// Privately encrypted, this is the local user's copy of the chat
message GroupChat {
// Settings
ChatSettings settings = 1;
// Conversation key for this user
veilid.TypedKey local_conversation_record_key = 2;
// Conversation keys for the other parties
repeated veilid.TypedKey remote_conversation_record_keys = 3;
}
////////////////////////////////////////////////////////////////////////////////////
// Accounts
////////////////////////////////////////////////////////////////////////////////////
// Publicly shared profile information for both contacts and accounts
// Contains:
// Name - Friendly name
// Pronouns - Pronouns of user
// Icon - Little picture to represent user in contact list
message Profile {
// Friendy name
string name = 1;
// Pronouns of user
string pronouns = 2;
// Description of the user
string about = 3;
// Status/away message
string status = 4;
// Availability
Availability availability = 5;
// Avatar DHTData
optional veilid.TypedKey avatar = 6;
}
// A record of an individual account
// Pointed to by the identity account map in the identity key
//
// DHT Schema: DFLT(1)
// DHT Private: accountSecretKey
message Account {
// The user's profile that gets shared with contacts
Profile profile = 1;
// Invisibility makes you always look 'Offline'
bool invisible = 2;
// Auto-away sets 'away' mode after an inactivity time
uint32 auto_away_timeout_sec = 3;
// The contacts DHTList for this account
// DHT Private
dht.OwnedDHTRecordPointer contact_list = 4;
// The ContactInvitationRecord DHTShortArray for this account
// DHT Private
dht.OwnedDHTRecordPointer contact_invitation_records = 5;
// The Chats DHTList for this account
// DHT Private
dht.OwnedDHTRecordPointer chat_list = 6;
// The GroupChats DHTList for this account
// DHT Private
dht.OwnedDHTRecordPointer group_chat_list = 7;
}
// A record of a contact that has accepted a contact invitation
// Contains a copy of the most recent remote profile as well as
// a locally edited profile.
@ -80,87 +331,13 @@ message Contact {
veilid.TypedKey remote_conversation_record_key = 5;
// Our conversation key for friend to sync
veilid.TypedKey local_conversation_record_key = 6;
// Show availability
// Show availability to this contact
bool show_availability = 7;
}
// Contact availability
enum Availability {
AVAILABILITY_UNSPECIFIED = 0;
AVAILABILITY_OFFLINE = 1;
AVAILABILITY_FREE = 2;
AVAILABILITY_BUSY = 3;
AVAILABILITY_AWAY = 4;
}
// Publicly shared profile information for both contacts and accounts
// Contains:
// Name - Friendly name
// Pronouns - Pronouns of user
// Icon - Little picture to represent user in contact list
message Profile {
// Friendy name
string name = 1;
// Pronouns of user
string pronouns = 2;
// Status/away message
string status = 3;
// Availability
Availability availability = 4;
// Avatar DHTData
optional veilid.TypedKey avatar = 5;
}
enum ChatType {
CHAT_TYPE_UNSPECIFIED = 0;
SINGLE_CONTACT = 1;
GROUP = 2;
}
// Either a 1-1 conversation or a group chat (eventually)
// Privately encrypted, this is the local user's copy of the chat
message Chat {
// What kind of chat is this
ChatType type = 1;
// Conversation key for the other party
veilid.TypedKey remote_conversation_record_key = 2;
// Reconciled chat record DHTLog (xxx for now DHTShortArray)
dht.OwnedDHTRecordPointer reconciled_chat_record = 3;
}
// A record of an individual account
// Pointed to by the identity account map in the identity key
//
// DHT Schema: DFLT(1)
// DHT Private: accountSecretKey
message Account {
// The user's profile that gets shared with contacts
Profile profile = 1;
// Invisibility makes you always look 'Offline'
bool invisible = 2;
// Auto-away sets 'away' mode after an inactivity time
uint32 auto_away_timeout_sec = 3;
// The contacts DHTList for this account
// DHT Private
dht.OwnedDHTRecordPointer contact_list = 4;
// The ContactInvitationRecord DHTShortArray for this account
// DHT Private
dht.OwnedDHTRecordPointer contact_invitation_records = 5;
// The chats DHTList for this account
// DHT Private
dht.OwnedDHTRecordPointer chat_list = 6;
}
// EncryptionKeyType
// Encryption of secret
enum EncryptionKeyType {
ENCRYPTION_KEY_TYPE_UNSPECIFIED = 0;
ENCRYPTION_KEY_TYPE_NONE = 1;
ENCRYPTION_KEY_TYPE_PIN = 2;
ENCRYPTION_KEY_TYPE_PASSWORD = 3;
}
////////////////////////////////////////////////////////////////////////////////////
// Invitations
////////////////////////////////////////////////////////////////////////////////////
// Invitation that is shared for VeilidChat contact connections
// serialized to QR code or data blob, not send over DHT, out of band.

View File

@ -44,7 +44,7 @@ Widget waitingPage({String? text}) => Builder(builder: (context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return ColoredBox(
color: scale.tertiaryScale.primaryText,
color: scale.tertiaryScale.appBackground,
child: Center(
child: Column(children: [
buildProgressIndicator().expanded(),

View File

@ -7,6 +7,7 @@ import 'fixtures/fixtures.dart';
import 'test_dht_log.dart';
import 'test_dht_record_pool.dart';
import 'test_dht_short_array.dart';
import 'test_table_db_array.dart';
void main() {
final startTime = DateTime.now();
@ -34,6 +35,17 @@ void main() {
setUpAll(veilidFixture.attach);
tearDownAll(veilidFixture.detach);
group('TableDB Tests', () {
group('TableDBArray Tests', () {
test('create TableDBArray', makeTestTableDBArrayCreateDelete());
test(
timeout: const Timeout(Duration(seconds: 480)),
'add/truncate TableDBArray',
makeTestDHTLogAddTruncate(),
);
});
});
group('DHT Support Tests', () {
setUpAll(updateProcessorFixture.setUp);
setUpAll(tickerFixture.setUp);

View File

@ -64,8 +64,7 @@ Future<void> Function() makeTestDHTLogAddTruncate({required int stride}) =>
const chunk = 25;
for (var n = 0; n < dataset.length; n += chunk) {
print('$n-${n + chunk - 1} ');
final success =
await w.tryAppendItems(dataset.sublist(n, n + chunk));
final success = await w.tryAddItems(dataset.sublist(n, n + chunk));
expect(success, isTrue);
}
});
@ -94,7 +93,7 @@ Future<void> Function() makeTestDHTLogAddTruncate({required int stride}) =>
}
print('truncate\n');
{
await dlog.operateAppend((w) async => w.truncate(5));
await dlog.operateAppend((w) async => w.truncate(w.length - 5));
}
{
final dataset6 = await dlog
@ -103,7 +102,7 @@ Future<void> Function() makeTestDHTLogAddTruncate({required int stride}) =>
}
print('truncate 2\n');
{
await dlog.operateAppend((w) async => w.truncate(251));
await dlog.operateAppend((w) async => w.truncate(w.length - 251));
}
{
final dataset7 = await dlog

View File

@ -0,0 +1,134 @@
import 'dart:convert';
import 'package:test/test.dart';
import 'package:veilid_support/veilid_support.dart';
Future<void> Function() makeTestTableDBArrayCreateDelete() => () async {
// Close before delete
{
final arr = await TableDBArray(
table: 'test', crypto: const VeilidCryptoPublic());
expect(await arr.operate((r) async => r.length), isZero);
expect(arr.isOpen, isTrue);
await arr.close();
expect(arr.isOpen, isFalse);
await arr.delete();
// Operate should fail
await expectLater(() async => arr.operate((r) async => r.length),
throwsA(isA<StateError>()));
}
// Close after delete
{
final arr = await DHTShortArray.create(
debugName: 'sa_create_delete 2 stride $stride', stride: stride);
await arr.delete();
// Operate should still succeed because things aren't closed
expect(await arr.operate((r) async => r.length), isZero);
await arr.close();
// Operate should fail
await expectLater(() async => arr.operate((r) async => r.length),
throwsA(isA<StateError>()));
}
// Close after delete multiple
// Okay to request delete multiple times before close
{
final arr = await DHTShortArray.create(
debugName: 'sa_create_delete 3 stride $stride', stride: stride);
await arr.delete();
await arr.delete();
// Operate should still succeed because things aren't closed
expect(await arr.operate((r) async => r.length), isZero);
await arr.close();
await expectLater(() async => arr.close(), throwsA(isA<StateError>()));
// Operate should fail
await expectLater(() async => arr.operate((r) async => r.length),
throwsA(isA<StateError>()));
}
};
Future<void> Function() makeTestTableDBArrayAdd({required int stride}) =>
() async {
final arr = await DHTShortArray.create(
debugName: 'sa_add 1 stride $stride', stride: stride);
final dataset = Iterable<int>.generate(256)
.map((n) => utf8.encode('elem $n'))
.toList();
print('adding singles\n');
{
final res = await arr.operateWrite((w) async {
for (var n = 4; n < 8; n++) {
print('$n ');
final success = await w.tryAddItem(dataset[n]);
expect(success, isTrue);
}
});
expect(res, isNull);
}
print('adding batch\n');
{
final res = await arr.operateWrite((w) async {
print('${dataset.length ~/ 2}-${dataset.length}');
final success = await w.tryAddItems(
dataset.sublist(dataset.length ~/ 2, dataset.length));
expect(success, isTrue);
});
expect(res, isNull);
}
print('inserting singles\n');
{
final res = await arr.operateWrite((w) async {
for (var n = 0; n < 4; n++) {
print('$n ');
final success = await w.tryInsertItem(n, dataset[n]);
expect(success, isTrue);
}
});
expect(res, isNull);
}
print('inserting batch\n');
{
final res = await arr.operateWrite((w) async {
print('8-${dataset.length ~/ 2}');
final success = await w.tryInsertItems(
8, dataset.sublist(8, dataset.length ~/ 2));
expect(success, isTrue);
});
expect(res, isNull);
}
//print('get all\n');
{
final dataset2 = await arr.operate((r) async => r.getItemRange(0));
expect(dataset2, equals(dataset));
}
{
final dataset3 =
await arr.operate((r) async => r.getItemRange(64, length: 128));
expect(dataset3, equals(dataset.sublist(64, 64 + 128)));
}
//print('clear\n');
{
await arr.operateWriteEventual((w) async {
await w.clear();
return true;
});
}
//print('get all\n');
{
final dataset4 = await arr.operate((r) async => r.getItemRange(0));
expect(dataset4, isEmpty);
}
await arr.delete();
await arr.close();
};

View File

@ -9,11 +9,11 @@ import 'package:meta/meta.dart';
import '../../../veilid_support.dart';
import '../../proto/proto.dart' as proto;
import '../interfaces/dht_append_truncate.dart';
import '../interfaces/dht_append.dart';
part 'dht_log_spine.dart';
part 'dht_log_read.dart';
part 'dht_log_append.dart';
part 'dht_log_write.dart';
///////////////////////////////////////////////////////////////////////
@ -60,7 +60,7 @@ class DHTLog implements DHTDeleteable<DHTLog, DHTLog> {
int stride = DHTShortArray.maxElements,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer}) async {
assert(stride <= DHTShortArray.maxElements, 'stride too long');
final pool = DHTRecordPool.instance;
@ -102,7 +102,7 @@ class DHTLog implements DHTDeleteable<DHTLog, DHTLog> {
{required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto}) async {
VeilidCrypto? crypto}) async {
final spineRecord = await DHTRecordPool.instance.openRecordRead(
logRecordKey,
debugName: debugName,
@ -125,7 +125,7 @@ class DHTLog implements DHTDeleteable<DHTLog, DHTLog> {
required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
}) async {
final spineRecord = await DHTRecordPool.instance.openRecordWrite(
logRecordKey, writer,
@ -148,7 +148,7 @@ class DHTLog implements DHTDeleteable<DHTLog, DHTLog> {
required String debugName,
required TypedKey parent,
VeilidRoutingContext? routingContext,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
}) =>
openWrite(
ownedLogRecordPointer.recordKey,
@ -209,7 +209,8 @@ class DHTLog implements DHTDeleteable<DHTLog, DHTLog> {
OwnedDHTRecordPointer get recordPointer => _spine.recordPointer;
/// Runs a closure allowing read-only access to the log
Future<T?> operate<T>(Future<T?> Function(DHTRandomRead) closure) async {
Future<T?> operate<T>(
Future<T?> Function(DHTLogReadOperations) closure) async {
if (!isOpen) {
throw StateError('log is not open"');
}
@ -226,13 +227,13 @@ class DHTLog implements DHTDeleteable<DHTLog, DHTLog> {
/// Throws DHTOperateException if the write could not be performed
/// at this time
Future<T> operateAppend<T>(
Future<T> Function(DHTAppendTruncateRandomRead) closure) async {
Future<T> Function(DHTLogWriteOperations) closure) async {
if (!isOpen) {
throw StateError('log is not open"');
}
return _spine.operateAppend((spine) async {
final writer = _DHTLogAppend._(spine);
final writer = _DHTLogWrite._(spine);
return closure(writer);
});
}
@ -244,14 +245,14 @@ class DHTLog implements DHTDeleteable<DHTLog, DHTLog> {
/// succeeded, returning false will trigger another eventual consistency
/// attempt.
Future<void> operateAppendEventual(
Future<bool> Function(DHTAppendTruncateRandomRead) closure,
Future<bool> Function(DHTLogWriteOperations) closure,
{Duration? timeout}) async {
if (!isOpen) {
throw StateError('log is not open"');
}
return _spine.operateAppendEventual((spine) async {
final writer = _DHTLogAppend._(spine);
final writer = _DHTLogWrite._(spine);
return closure(writer);
}, timeout: timeout);
}

View File

@ -8,7 +8,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart';
import '../../../veilid_support.dart';
import '../interfaces/dht_append_truncate.dart';
@immutable
class DHTLogElementState<T> extends Equatable {
@ -184,19 +183,20 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
await super.close();
}
Future<R?> operate<R>(Future<R?> Function(DHTRandomRead) closure) async {
Future<R?> operate<R>(
Future<R?> Function(DHTLogReadOperations) closure) async {
await _initWait();
return _log.operate(closure);
}
Future<R> operateAppend<R>(
Future<R> Function(DHTAppendTruncateRandomRead) closure) async {
Future<R> Function(DHTLogWriteOperations) closure) async {
await _initWait();
return _log.operateAppend(closure);
}
Future<void> operateAppendEventual(
Future<bool> Function(DHTAppendTruncateRandomRead) closure,
Future<bool> Function(DHTLogWriteOperations) closure,
{Duration? timeout}) async {
await _initWait();
return _log.operateAppendEventual(closure, timeout: timeout);

View File

@ -3,7 +3,9 @@ part of 'dht_log.dart';
////////////////////////////////////////////////////////////////////////////
// Reader-only implementation
class _DHTLogRead implements DHTRandomRead {
abstract class DHTLogReadOperations implements DHTRandomRead {}
class _DHTLogRead implements DHTLogReadOperations {
_DHTLogRead._(_DHTLogSpine spine) : _spine = spine;
@override

View File

@ -1,13 +1,32 @@
part of 'dht_log.dart';
////////////////////////////////////////////////////////////////////////////
// Append/truncate implementation
// Writer implementation
class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead {
_DHTLogAppend._(super.spine) : super._();
abstract class DHTLogWriteOperations
implements DHTRandomRead, DHTRandomWrite, DHTAdd, DHTTruncate, DHTClear {}
class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
_DHTLogWrite._(super.spine) : super._();
@override
Future<bool> tryAppendItem(Uint8List value) async {
Future<bool> tryWriteItem(int pos, Uint8List newValue,
{Output<Uint8List>? output}) async {
if (pos < 0 || pos >= _spine.length) {
throw IndexError.withLength(pos, _spine.length);
}
final lookup = await _spine.lookupPosition(pos);
if (lookup == null) {
throw StateError("can't write to dht log");
}
// Write item to the segment
return lookup.scope((sa) => sa.operateWrite((write) async =>
write.tryWriteItem(lookup.pos, newValue, output: output)));
}
@override
Future<bool> tryAddItem(Uint8List value) async {
// Allocate empty index at the end of the list
final insertPos = _spine.length;
_spine.allocateTail(1);
@ -30,7 +49,7 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead {
}
@override
Future<bool> tryAppendItems(List<Uint8List> values) async {
Future<bool> tryAddItems(List<Uint8List> values) async {
// Allocate empty index at the end of the list
final insertPos = _spine.length;
_spine.allocateTail(values.length);
@ -76,15 +95,14 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead {
}
@override
Future<void> truncate(int count) async {
count = min(count, _spine.length);
if (count == 0) {
Future<void> truncate(int newLength) async {
if (newLength < 0) {
throw StateError('can not truncate to negative length');
}
if (newLength >= _spine.length) {
return;
}
if (count < 0) {
throw StateError('can not remove negative items');
}
await _spine.releaseHead(count);
await _spine.releaseHead(_spine.length - newLength);
}
@override

View File

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

View File

@ -42,7 +42,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
required SharedDHTRecordData sharedDHTRecordData,
required int defaultSubkey,
required KeyPair? writer,
required DHTRecordCrypto crypto,
required VeilidCrypto crypto,
required this.debugName})
: _crypto = crypto,
_routingContext = routingContext,
@ -104,7 +104,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
int get subkeyCount =>
_sharedDHTRecordData.recordDescriptor.schema.subkeyCount();
KeyPair? get writer => _writer;
DHTRecordCrypto get crypto => _crypto;
VeilidCrypto get crypto => _crypto;
OwnedDHTRecordPointer get ownedDHTRecordPointer =>
OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!);
int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey;
@ -118,7 +118,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
/// returned if one was returned.
Future<Uint8List?> get(
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached,
Output<int>? outSeqNum}) async {
subkey = subkeyOrDefault(subkey);
@ -146,7 +146,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
return null;
}
// If we're returning a value, decrypt it
final out = (crypto ?? _crypto).decrypt(valueData.data, subkey);
final out = (crypto ?? _crypto).decrypt(valueData.data);
if (outSeqNum != null) {
outSeqNum.save(valueData.seq);
}
@ -163,7 +163,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
/// returned if one was returned.
Future<T?> getJson<T>(T Function(dynamic) fromJson,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached,
Output<int>? outSeqNum}) async {
final data = await get(
@ -189,7 +189,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
Future<T?> getProtobuf<T extends GeneratedMessage>(
T Function(List<int> i) fromBuffer,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached,
Output<int>? outSeqNum}) async {
final data = await get(
@ -208,13 +208,12 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
/// If the value was succesfully written, null is returned
Future<Uint8List?> tryWriteBytes(Uint8List newValue,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) async {
subkey = subkeyOrDefault(subkey);
final lastSeq = await _localSubkeySeq(subkey);
final encryptedNewValue =
await (crypto ?? _crypto).encrypt(newValue, subkey);
final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue);
// Set the new data if possible
var newValueData = await _routingContext
@ -246,7 +245,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
// Decrypt value to return it
final decryptedNewValue =
await (crypto ?? _crypto).decrypt(newValueData.data, subkey);
await (crypto ?? _crypto).decrypt(newValueData.data);
if (isUpdated) {
DHTRecordPool.instance
.processLocalValueChange(key, decryptedNewValue, subkey);
@ -259,13 +258,12 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
/// will be made to write the subkey until this succeeds
Future<void> eventualWriteBytes(Uint8List newValue,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) async {
subkey = subkeyOrDefault(subkey);
final lastSeq = await _localSubkeySeq(subkey);
final encryptedNewValue =
await (crypto ?? _crypto).encrypt(newValue, subkey);
final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue);
ValueData? newValueData;
do {
@ -309,7 +307,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
Future<void> eventualUpdateBytes(
Future<Uint8List> Function(Uint8List? oldValue) update,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) async {
subkey = subkeyOrDefault(subkey);
@ -334,7 +332,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
/// Like 'tryWriteBytes' but with JSON marshal/unmarshal of the value
Future<T?> tryWriteJson<T>(T Function(dynamic) fromJson, T newValue,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) =>
tryWriteBytes(jsonEncodeBytes(newValue),
@ -353,7 +351,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
Future<T?> tryWriteProtobuf<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, T newValue,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) =>
tryWriteBytes(newValue.writeToBuffer(),
@ -371,7 +369,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
/// Like 'eventualWriteBytes' but with JSON marshal/unmarshal of the value
Future<void> eventualWriteJson<T>(T newValue,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) =>
eventualWriteBytes(jsonEncodeBytes(newValue),
@ -380,7 +378,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
/// Like 'eventualWriteBytes' but with protobuf marshal/unmarshal of the value
Future<void> eventualWriteProtobuf<T extends GeneratedMessage>(T newValue,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) =>
eventualWriteBytes(newValue.writeToBuffer(),
@ -390,7 +388,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
Future<void> eventualUpdateJson<T>(
T Function(dynamic) fromJson, Future<T> Function(T?) update,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) =>
eventualUpdateBytes(jsonUpdate(fromJson, update),
@ -400,7 +398,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, Future<T> Function(T?) update,
{int subkey = -1,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
Output<int>? outSeqNum}) =>
eventualUpdateBytes(protobufUpdate(fromBuffer, update),
@ -433,7 +431,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
DHTRecord record, Uint8List? data, List<ValueSubkeyRange> subkeys)
onUpdate, {
bool localChanges = true,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
}) async {
// Set up watch requirements
_watchController ??=
@ -457,8 +455,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
final changeData = change.data;
data = changeData == null
? null
: await (crypto ?? _crypto)
.decrypt(changeData, change.subkeys.first.low);
: await (crypto ?? _crypto).decrypt(changeData);
}
await onUpdate(this, data, change.subkeys);
});
@ -544,7 +541,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord, DHTRecord> {
final VeilidRoutingContext _routingContext;
final int _defaultSubkey;
final KeyPair? _writer;
final DHTRecordCrypto _crypto;
final VeilidCrypto _crypto;
final String debugName;
final _mutex = Mutex();
int _openCount;

View File

@ -1,53 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import '../../../../../veilid_support.dart';
abstract class DHTRecordCrypto {
Future<Uint8List> encrypt(Uint8List data, int subkey);
Future<Uint8List> decrypt(Uint8List data, int subkey);
}
////////////////////////////////////
/// Private DHT Record: Encrypted for a specific symmetric key
class DHTRecordCryptoPrivate implements DHTRecordCrypto {
DHTRecordCryptoPrivate._(
VeilidCryptoSystem cryptoSystem, SharedSecret secretKey)
: _cryptoSystem = cryptoSystem,
_secretKey = secretKey;
final VeilidCryptoSystem _cryptoSystem;
final SharedSecret _secretKey;
static Future<DHTRecordCryptoPrivate> fromTypedKeyPair(
TypedKeyPair typedKeyPair) async {
final cryptoSystem =
await Veilid.instance.getCryptoSystem(typedKeyPair.kind);
final secretKey = typedKeyPair.secret;
return DHTRecordCryptoPrivate._(cryptoSystem, secretKey);
}
static Future<DHTRecordCryptoPrivate> fromSecret(
CryptoKind kind, SharedSecret secretKey) async {
final cryptoSystem = await Veilid.instance.getCryptoSystem(kind);
return DHTRecordCryptoPrivate._(cryptoSystem, secretKey);
}
@override
Future<Uint8List> encrypt(Uint8List data, int subkey) =>
_cryptoSystem.encryptNoAuthWithNonce(data, _secretKey);
@override
Future<Uint8List> decrypt(Uint8List data, int subkey) =>
_cryptoSystem.decryptNoAuthWithNonce(data, _secretKey);
}
////////////////////////////////////
/// Public DHT Record: No encryption
class DHTRecordCryptoPublic implements DHTRecordCrypto {
const DHTRecordCryptoPublic();
@override
Future<Uint8List> encrypt(Uint8List data, int subkey) async => data;
@override
Future<Uint8List> decrypt(Uint8List data, int subkey) async => data;
}

View File

@ -526,7 +526,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
TypedKey? parent,
DHTSchema schema = const DHTSchema.dflt(oCnt: 1),
int defaultSubkey = 0,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer,
}) async =>
_mutex.protect(() async {
@ -547,7 +547,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
writer: writer ??
openedRecordInfo.shared.recordDescriptor.ownerKeyPair(),
crypto: crypto ??
await DHTRecordCryptoPrivate.fromTypedKeyPair(openedRecordInfo
await VeilidCryptoPrivate.fromTypedKeyPair(openedRecordInfo
.shared.recordDescriptor
.ownerTypedKeyPair()!));
@ -562,7 +562,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
VeilidRoutingContext? routingContext,
TypedKey? parent,
int defaultSubkey = 0,
DHTRecordCrypto? crypto}) async =>
VeilidCrypto? crypto}) async =>
_mutex.protect(() async {
final dhtctx = routingContext ?? _routingContext;
@ -578,7 +578,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
defaultSubkey: defaultSubkey,
sharedDHTRecordData: openedRecordInfo.shared,
writer: null,
crypto: crypto ?? const DHTRecordCryptoPublic());
crypto: crypto ?? const VeilidCryptoPublic());
openedRecordInfo.records.add(rec);
@ -593,7 +593,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
VeilidRoutingContext? routingContext,
TypedKey? parent,
int defaultSubkey = 0,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
}) async =>
_mutex.protect(() async {
final dhtctx = routingContext ?? _routingContext;
@ -612,7 +612,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
writer: writer,
sharedDHTRecordData: openedRecordInfo.shared,
crypto: crypto ??
await DHTRecordCryptoPrivate.fromTypedKeyPair(
await VeilidCryptoPrivate.fromTypedKeyPair(
TypedKeyPair.fromKeyPair(recordKey.kind, writer)));
openedRecordInfo.records.add(rec);
@ -632,7 +632,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
required TypedKey parent,
VeilidRoutingContext? routingContext,
int defaultSubkey = 0,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
}) =>
openRecordWrite(
ownedDHTRecordPointer.recordKey,

View File

@ -33,7 +33,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray, DHTShortArray> {
int stride = maxElements,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
KeyPair? writer}) async {
assert(stride <= maxElements, 'stride too long');
final pool = DHTRecordPool.instance;
@ -79,7 +79,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray, DHTShortArray> {
{required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto}) async {
VeilidCrypto? crypto}) async {
final dhtRecord = await DHTRecordPool.instance.openRecordRead(headRecordKey,
debugName: debugName,
parent: parent,
@ -101,7 +101,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray, DHTShortArray> {
required String debugName,
VeilidRoutingContext? routingContext,
TypedKey? parent,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
}) async {
final dhtRecord = await DHTRecordPool.instance.openRecordWrite(
headRecordKey, writer,
@ -124,7 +124,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray, DHTShortArray> {
required String debugName,
required TypedKey parent,
VeilidRoutingContext? routingContext,
DHTRecordCrypto? crypto,
VeilidCrypto? crypto,
}) =>
openWrite(
ownedShortArrayRecordPointer.recordKey,
@ -186,7 +186,8 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray, DHTShortArray> {
OwnedDHTRecordPointer get recordPointer => _head.recordPointer;
/// Runs a closure allowing read-only access to the shortarray
Future<T> operate<T>(Future<T> Function(DHTRandomRead) closure) async {
Future<T> operate<T>(
Future<T> Function(DHTShortArrayReadOperations) closure) async {
if (!isOpen) {
throw StateError('short array is not open"');
}
@ -203,7 +204,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray, DHTShortArray> {
/// Throws DHTOperateException if the write could not be performed
/// at this time
Future<T> operateWrite<T>(
Future<T> Function(DHTRandomReadWrite) closure) async {
Future<T> Function(DHTShortArrayWriteOperations) closure) async {
if (!isOpen) {
throw StateError('short array is not open"');
}
@ -221,7 +222,7 @@ class DHTShortArray implements DHTDeleteable<DHTShortArray, DHTShortArray> {
/// succeeded, returning false will trigger another eventual consistency
/// attempt.
Future<void> operateWriteEventual(
Future<bool> Function(DHTRandomReadWrite) closure,
Future<bool> Function(DHTShortArrayWriteOperations) closure,
{Duration? timeout}) async {
if (!isOpen) {
throw StateError('short array is not open"');

View File

@ -91,19 +91,20 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
await super.close();
}
Future<R> operate<R>(Future<R> Function(DHTRandomRead) closure) async {
Future<R> operate<R>(
Future<R> Function(DHTShortArrayReadOperations) closure) async {
await _initWait();
return _shortArray.operate(closure);
}
Future<R> operateWrite<R>(
Future<R> Function(DHTRandomReadWrite) closure) async {
Future<R> Function(DHTShortArrayWriteOperations) closure) async {
await _initWait();
return _shortArray.operateWrite(closure);
}
Future<void> operateWriteEventual(
Future<bool> Function(DHTRandomReadWrite) closure,
Future<bool> Function(DHTShortArrayWriteOperations) closure,
{Duration? timeout}) async {
await _initWait();
return _shortArray.operateWriteEventual(closure, timeout: timeout);

View File

@ -3,7 +3,9 @@ part of 'dht_short_array.dart';
////////////////////////////////////////////////////////////////////////////
// Reader-only implementation
class _DHTShortArrayRead implements DHTRandomRead {
abstract class DHTShortArrayReadOperations implements DHTRandomRead {}
class _DHTShortArrayRead implements DHTShortArrayReadOperations {
_DHTShortArrayRead._(_DHTShortArrayHead head) : _head = head;
@override

View File

@ -3,8 +3,16 @@ part of 'dht_short_array.dart';
////////////////////////////////////////////////////////////////////////////
// Writer implementation
abstract class DHTShortArrayWriteOperations
implements
DHTRandomRead,
DHTRandomWrite,
DHTInsertRemove,
DHTAdd,
DHTClear {}
class _DHTShortArrayWrite extends _DHTShortArrayRead
implements DHTRandomReadWrite {
implements DHTShortArrayWriteOperations {
_DHTShortArrayWrite._(super.head) : super._();
@override

View File

@ -0,0 +1,41 @@
import 'dart:typed_data';
import 'package:protobuf/protobuf.dart';
import '../../../veilid_support.dart';
////////////////////////////////////////////////////////////////////////////
// Add
abstract class DHTAdd {
/// Try to add an item to the DHT container.
/// Return true if the element was successfully added, and false if the state
/// changed before the element could be added or a newer value was found on
/// the network.
/// Throws a StateError if the container exceeds its maximum size.
Future<bool> tryAddItem(Uint8List value);
/// Try to add a list of items to the DHT container.
/// Return true if the elements were successfully added, and false if the
/// state changed before the element could be added or a newer value was found
/// on the network.
/// Throws a StateError if the container exceeds its maximum size.
Future<bool> tryAddItems(List<Uint8List> values);
}
extension DHTAddExt on DHTAdd {
/// Convenience function:
/// Like tryAddItem but also encodes the input value as JSON and parses the
/// returned element as JSON
Future<bool> tryAppendItemJson<T>(
T newValue,
) =>
tryAddItem(jsonEncodeBytes(newValue));
/// Convenience function:
/// Like tryAddItem but also encodes the input value as a protobuf object
/// and parses the returned element as a protobuf object
Future<bool> tryAddItemProtobuf<T extends GeneratedMessage>(
T newValue,
) =>
tryAddItem(newValue.writeToBuffer());
}

View File

@ -1,51 +0,0 @@
import 'dart:typed_data';
import 'package:protobuf/protobuf.dart';
import '../../../veilid_support.dart';
////////////////////////////////////////////////////////////////////////////
// Append/truncate interface
abstract class DHTAppendTruncate {
/// Try to add an item to the end of the DHT data structure.
/// Return true if the element was successfully added, and false if the state
/// changed before the element could be added or a newer value was found on
/// the network.
/// This may throw an exception if the number elements added exceeds limits.
Future<bool> tryAppendItem(Uint8List value);
/// Try to add a list of items to the end of the DHT data structure.
/// Return true if the elements were successfully added, and false if the
/// state changed before the element could be added or a newer value was found
/// on the network.
/// This may throw an exception if the number elements added exceeds limits.
Future<bool> tryAppendItems(List<Uint8List> values);
/// Try to remove a number of items from the head of the DHT data structure.
/// Throws StateError if count < 0
Future<void> truncate(int count);
/// Remove all items in the DHT data structure.
Future<void> clear();
}
abstract class DHTAppendTruncateRandomRead
implements DHTAppendTruncate, DHTRandomRead {}
extension DHTAppendTruncateExt on DHTAppendTruncate {
/// Convenience function:
/// Like tryAppendItem but also encodes the input value as JSON and parses the
/// returned element as JSON
Future<bool> tryAppendItemJson<T>(
T newValue,
) =>
tryAppendItem(jsonEncodeBytes(newValue));
/// Convenience function:
/// Like tryAppendItem but also encodes the input value as a protobuf object
/// and parses the returned element as a protobuf object
Future<bool> tryAppendItemProtobuf<T extends GeneratedMessage>(
T newValue,
) =>
tryAppendItem(newValue.writeToBuffer());
}

View File

@ -0,0 +1,7 @@
////////////////////////////////////////////////////////////////////////////
// Clear interface
// ignore: one_member_abstracts
abstract class DHTClear {
/// Remove all items in the DHT container.
Future<void> clear();
}

View File

@ -0,0 +1,60 @@
import 'dart:typed_data';
import 'package:protobuf/protobuf.dart';
import '../../../veilid_support.dart';
////////////////////////////////////////////////////////////////////////////
// Insert/Remove interface
abstract class DHTInsertRemove {
/// Try to insert an item as position 'pos' of the DHT container.
/// Return true if the element was successfully inserted, and false if the
/// state changed before the element could be inserted or a newer value was
/// found on the network.
/// Throws an IndexError if the position removed exceeds the length of
/// the container.
/// Throws a StateError if the container exceeds its maximum size.
Future<bool> tryInsertItem(int pos, Uint8List value);
/// Try to insert items at position 'pos' of the DHT container.
/// Return true if the elements were successfully inserted, and false if the
/// state changed before the elements could be inserted or a newer value was
/// found on the network.
/// Throws an IndexError if the position removed exceeds the length of
/// the container.
/// Throws a StateError if the container exceeds its maximum size.
Future<bool> tryInsertItems(int pos, List<Uint8List> values);
/// Swap items at position 'aPos' and 'bPos' in the DHTArray.
/// Throws an IndexError if either of the positions swapped exceeds the length
/// of the container
Future<void> swapItem(int aPos, int bPos);
/// Remove an item at position 'pos' in the DHT container.
/// If the remove was successful this returns:
/// * outValue will return the prior contents of the element
/// Throws an IndexError if the position removed exceeds the length of
/// the container.
Future<void> removeItem(int pos, {Output<Uint8List>? output});
}
extension DHTInsertRemoveExt on DHTInsertRemove {
/// Convenience function:
/// Like removeItem but also parses the returned element as JSON
Future<void> removeItemJson<T>(T Function(dynamic) fromJson, int pos,
{Output<T>? output}) async {
final outValueBytes = output == null ? null : Output<Uint8List>();
await removeItem(pos, output: outValueBytes);
output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b));
}
/// Convenience function:
/// Like removeItem but also parses the returned element as JSON
Future<void> removeItemProtobuf<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, int pos,
{Output<T>? output}) async {
final outValueBytes = output == null ? null : Output<Uint8List>();
await removeItem(pos, output: outValueBytes);
output.mapSave(outValueBytes, fromBuffer);
}
}

View File

@ -7,22 +7,21 @@ import '../../../veilid_support.dart';
////////////////////////////////////////////////////////////////////////////
// Reader interface
abstract class DHTRandomRead {
/// Returns the number of elements in the DHTArray
/// This number will be >= 0 and <= DHTShortArray.maxElements (256)
/// Returns the number of elements in the DHT container
int get length;
/// Return the item at position 'pos' in the DHTArray. If 'forceRefresh'
/// Return the item at position 'pos' in the DHT container. If 'forceRefresh'
/// is specified, the network will always be checked for newer values
/// rather than returning the existing locally stored copy of the elements.
/// * 'pos' must be >= 0 and < 'length'
/// Throws an IndexError if the 'pos' is not within the length
/// of the container.
Future<Uint8List?> getItem(int pos, {bool forceRefresh = false});
/// Return a list of a range of items in the DHTArray. If 'forceRefresh'
/// is specified, the network will always be checked for newer values
/// rather than returning the existing locally stored copy of the elements.
/// * 'start' must be >= 0
/// * 'len' must be >= 0 and <= DHTShortArray.maxElements (256) and defaults
/// to the maximum length
/// Throws an IndexError if either 'start' or '(start+length)' is not within
/// the length of the container.
Future<List<Uint8List>?> getItemRange(int start,
{int? length, bool forceRefresh = false});

View File

@ -6,8 +6,9 @@ import '../../../veilid_support.dart';
////////////////////////////////////////////////////////////////////////////
// Writer interface
// ignore: one_member_abstracts
abstract class DHTRandomWrite {
/// Try to set an item at position 'pos' of the DHTArray.
/// Try to set an item at position 'pos' of the DHT container.
/// If the set was successful this returns:
/// * A boolean true
/// * outValue will return the prior contents of the element,
@ -18,55 +19,10 @@ abstract class DHTRandomWrite {
/// * outValue will return the newer value of the element,
/// or null if the head record changed.
///
/// This may throw an exception if the position exceeds the built-in limit of
/// 'maxElements = 256' entries.
/// Throws an IndexError if the position is not within the length
/// of the container.
Future<bool> tryWriteItem(int pos, Uint8List newValue,
{Output<Uint8List>? output});
/// Try to add an item to the end of the DHTArray. Return true if the
/// element was successfully added, and false if the state changed before
/// the element could be added or a newer value was found on the network.
/// This may throw an exception if the number elements added exceeds the
/// built-in limit of 'maxElements = 256' entries.
Future<bool> tryAddItem(Uint8List value);
/// Try to add a list of items to the end of the DHTArray. Return true if the
/// elements were successfully added, and false if the state changed before
/// the elements could be added or a newer value was found on the network.
/// This may throw an exception if the number elements added exceeds the
/// built-in limit of 'maxElements = 256' entries.
Future<bool> tryAddItems(List<Uint8List> values);
/// Try to insert an item as position 'pos' of the DHTArray.
/// Return true if the element was successfully inserted, and false if the
/// state changed before the element could be inserted or a newer value was
/// found on the network.
/// This may throw an exception if the number elements added exceeds the
/// built-in limit of 'maxElements = 256' entries.
Future<bool> tryInsertItem(int pos, Uint8List value);
/// Try to insert items at position 'pos' of the DHTArray.
/// Return true if the elements were successfully inserted, and false if the
/// state changed before the elements could be inserted or a newer value was
/// found on the network.
/// This may throw an exception if the number elements added exceeds the
/// built-in limit of 'maxElements = 256' entries.
Future<bool> tryInsertItems(int pos, List<Uint8List> values);
/// Swap items at position 'aPos' and 'bPos' in the DHTArray.
/// Throws IndexError if either of the positions swapped exceed
/// the length of the list
Future<void> swapItem(int aPos, int bPos);
/// Remove an item at position 'pos' in the DHTArray.
/// If the remove was successful this returns:
/// * outValue will return the prior contents of the element
/// Throws IndexError if the position removed exceeds the length of
/// the list.
Future<void> removeItem(int pos, {Output<Uint8List>? output});
/// Remove all items in the DHTShortArray.
Future<void> clear();
}
extension DHTRandomWriteExt on DHTRandomWrite {
@ -95,25 +51,4 @@ extension DHTRandomWriteExt on DHTRandomWrite {
output.mapSave(outValueBytes, fromBuffer);
return out;
}
/// Convenience function:
/// Like removeItem but also parses the returned element as JSON
Future<void> removeItemJson<T>(T Function(dynamic) fromJson, int pos,
{Output<T>? output}) async {
final outValueBytes = output == null ? null : Output<Uint8List>();
await removeItem(pos, output: outValueBytes);
output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b));
}
/// Convenience function:
/// Like removeItem but also parses the returned element as JSON
Future<void> removeItemProtobuf<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, int pos,
{Output<T>? output}) async {
final outValueBytes = output == null ? null : Output<Uint8List>();
await removeItem(pos, output: outValueBytes);
output.mapSave(outValueBytes, fromBuffer);
}
}
abstract class DHTRandomReadWrite implements DHTRandomRead, DHTRandomWrite {}

View File

@ -0,0 +1,8 @@
////////////////////////////////////////////////////////////////////////////
// Truncate interface
// ignore: one_member_abstracts
abstract class DHTTruncate {
/// Remove items from the DHT container to shrink its size to 'newLength'
/// Throws StateError if newLength < 0
Future<void> truncate(int newLength);
}

View File

@ -1,4 +1,8 @@
export 'dht_append.dart';
export 'dht_clear.dart';
export 'dht_closeable.dart';
export 'dht_insert_remove.dart';
export 'dht_random_read.dart';
export 'dht_random_write.dart';
export 'dht_truncate.dart';
export 'exceptions.dart';

View File

@ -130,7 +130,7 @@ extension IdentityMasterExtension on IdentityMaster {
// Read the identity key to get the account keys
final pool = DHTRecordPool.instance;
final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret(
final identityRecordCrypto = await VeilidCryptoPrivate.fromSecret(
identityRecordKey.kind, identitySecret);
late final List<AccountRecordInfo> accountRecordInfo;
@ -234,7 +234,7 @@ class IdentityMasterWithSecrets {
return (await pool.createRecord(
debugName:
'IdentityMasterWithSecrets::create::IdentityMasterRecord',
crypto: const DHTRecordCryptoPublic()))
crypto: const VeilidCryptoPublic()))
.deleteScope((masterRec) async {
veilidLoggy.debug('Creating identity record');
// Identity record is private

View File

@ -0,0 +1,517 @@
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'package:async_tools/async_tools.dart';
import 'package:charcode/charcode.dart';
import '../veilid_support.dart';
class TableDBArray {
TableDBArray({
required String table,
required VeilidCrypto crypto,
}) : _table = table,
_crypto = crypto {
_initWait.add(_init);
}
Future<void> _init() async {
// Load the array details
await _mutex.protect(() async {
_tableDB = await Veilid.instance.openTableDB(_table, 1);
});
}
Future<void> close({bool delete = false}) async {
// Ensure the init finished
await _initWait();
await _mutex.acquire();
await _changeStream.close();
_tableDB.close();
if (delete) {
await Veilid.instance.deleteTableDB(_table);
}
}
Future<StreamSubscription<void>> listen(void Function() onChanged) async =>
_changeStream.stream.listen((_) => onChanged());
////////////////////////////////////////////////////////////
// Public interface
int get length => _length;
Future<void> add(Uint8List value) async {
await _initWait();
return _writeTransaction((t) async => _addInner(t, value));
}
Future<void> addAll(List<Uint8List> values) async {
await _initWait();
return _writeTransaction((t) async => _addAllInner(t, values));
}
Future<void> insert(int pos, Uint8List value) async {
await _initWait();
return _writeTransaction((t) async => _insertInner(t, pos, value));
}
Future<void> insertAll(int pos, List<Uint8List> values) async {
await _initWait();
return _writeTransaction((t) async => _insertAllInner(t, pos, values));
}
Future<Uint8List> get(int pos) async {
await _initWait();
return _mutex.protect(() async => _getInner(pos));
}
Future<List<Uint8List>> getAll(int start, int length) async {
await _initWait();
return _mutex.protect(() async => _getAllInner(start, length));
}
Future<void> remove(int pos, {Output<Uint8List>? out}) async {
await _initWait();
return _writeTransaction((t) async => _removeInner(t, pos, out: out));
}
Future<void> removeRange(int start, int length,
{Output<List<Uint8List>>? out}) async {
await _initWait();
return _writeTransaction(
(t) async => _removeRangeInner(t, start, length, out: out));
}
Future<void> clear() async {
await _initWait();
return _writeTransaction((t) async {
final keys = await _tableDB.getKeys(0);
for (final key in keys) {
await t.delete(0, key);
}
_length = 0;
_nextFree = 0;
_dirtyChunks.clear();
_chunkCache.clear();
});
}
////////////////////////////////////////////////////////////
// Inner interface
Future<void> _addInner(VeilidTableDBTransaction t, Uint8List value) async {
// Allocate an entry to store the value
final entry = await _allocateEntry();
await _storeEntry(t, entry, value);
// Put the entry in the index
final pos = _length;
_length++;
await _setIndexEntry(pos, entry);
}
Future<void> _addAllInner(
VeilidTableDBTransaction t, List<Uint8List> values) async {
var pos = _length;
_length += values.length;
for (final value in values) {
// Allocate an entry to store the value
final entry = await _allocateEntry();
await _storeEntry(t, entry, value);
// Put the entry in the index
await _setIndexEntry(pos, entry);
pos++;
}
}
Future<void> _insertInner(
VeilidTableDBTransaction t, int pos, Uint8List value) async {
if (pos == _length) {
return _addInner(t, value);
}
if (pos < 0 || pos >= _length) {
throw IndexError.withLength(pos, _length);
}
// Allocate an entry to store the value
final entry = await _allocateEntry();
await _storeEntry(t, entry, value);
// Put the entry in the index
await _insertIndexEntry(pos);
await _setIndexEntry(pos, entry);
}
Future<void> _insertAllInner(
VeilidTableDBTransaction t, int pos, List<Uint8List> values) async {
if (pos == _length) {
return _addAllInner(t, values);
}
if (pos < 0 || pos >= _length) {
throw IndexError.withLength(pos, _length);
}
await _insertIndexEntries(pos, values.length);
for (final value in values) {
// Allocate an entry to store the value
final entry = await _allocateEntry();
await _storeEntry(t, entry, value);
// Put the entry in the index
await _setIndexEntry(pos, entry);
pos++;
}
}
Future<Uint8List> _getInner(int pos) async {
if (pos < 0 || pos >= _length) {
throw IndexError.withLength(pos, _length);
}
final entry = await _getIndexEntry(pos);
return (await _loadEntry(entry))!;
}
Future<List<Uint8List>> _getAllInner(int start, int length) async {
if (length < 0) {
throw StateError('length should not be negative');
}
if (start < 0 || start >= _length) {
throw IndexError.withLength(start, _length);
}
if ((start + length) > _length) {
throw IndexError.withLength(start + length, _length);
}
final out = <Uint8List>[];
for (var pos = start; pos < (start + length); pos++) {
final entry = await _getIndexEntry(pos);
final value = (await _loadEntry(entry))!;
out.add(value);
}
return out;
}
Future<void> _removeInner(VeilidTableDBTransaction t, int pos,
{Output<Uint8List>? out}) async {
if (pos < 0 || pos >= _length) {
throw IndexError.withLength(pos, _length);
}
final entry = await _getIndexEntry(pos);
if (out != null) {
final value = (await _loadEntry(entry))!;
out.save(value);
}
await _freeEntry(t, entry);
await _removeIndexEntry(pos);
}
Future<void> _removeRangeInner(
VeilidTableDBTransaction t, int start, int length,
{Output<List<Uint8List>>? out}) async {
if (length < 0) {
throw StateError('length should not be negative');
}
if (start < 0 || start >= _length) {
throw IndexError.withLength(start, _length);
}
if ((start + length) > _length) {
throw IndexError.withLength(start + length, _length);
}
final outList = <Uint8List>[];
for (var pos = start; pos < (start + length); pos++) {
final entry = await _getIndexEntry(pos);
if (out != null) {
final value = (await _loadEntry(entry))!;
outList.add(value);
}
await _freeEntry(t, entry);
}
if (out != null) {
out.save(outList);
}
await _removeIndexEntries(start, length);
}
////////////////////////////////////////////////////////////
// Private implementation
static final Uint8List _headKey = Uint8List.fromList([$_, $H, $E, $A, $D]);
static Uint8List _entryKey(int k) =>
(ByteData(4)..setUint32(0, k)).buffer.asUint8List();
static Uint8List _chunkKey(int n) =>
(ByteData(2)..setUint16(0, n)).buffer.asUint8List();
Future<T> _writeTransaction<T>(
Future<T> Function(VeilidTableDBTransaction) closure) async =>
_mutex.protect(() async {
final _oldLength = _length;
final _oldNextFree = _nextFree;
try {
final out = await transactionScope(_tableDB, (t) async {
final out = closure(t);
await _saveHead(t);
await _flushDirtyChunks(t);
return out;
});
return out;
} on Exception {
// restore head
_length = _oldLength;
_nextFree = _oldNextFree;
// invalidate caches because they could have been written to
_chunkCache.clear();
_dirtyChunks.clear();
// propagate exception
rethrow;
}
});
Future<void> _storeEntry(
VeilidTableDBTransaction t, int entry, Uint8List value) async =>
t.store(0, _entryKey(entry), await _crypto.encrypt(value));
Future<Uint8List?> _loadEntry(int entry) async {
final encryptedValue = await _tableDB.load(0, _entryKey(entry));
return (encryptedValue == null) ? null : _crypto.decrypt(encryptedValue);
}
Future<int> _getIndexEntry(int pos) async {
if (pos < 0 || pos >= _length) {
throw IndexError.withLength(pos, _length);
}
final chunkNumber = pos ~/ _indexStride;
final chunkOffset = pos % _indexStride;
final chunk = await _loadIndexChunk(chunkNumber);
return chunk.buffer.asByteData().getUint32(chunkOffset * 4);
}
Future<void> _setIndexEntry(int pos, int entry) async {
if (pos < 0 || pos >= _length) {
throw IndexError.withLength(pos, _length);
}
final chunkNumber = pos ~/ _indexStride;
final chunkOffset = pos % _indexStride;
final chunk = await _loadIndexChunk(chunkNumber);
chunk.buffer.asByteData().setUint32(chunkOffset * 4, entry);
_dirtyChunks[chunkNumber] = chunk;
}
Future<void> _insertIndexEntry(int pos) async => _insertIndexEntries(pos, 1);
Future<void> _insertIndexEntries(int start, int length) async {
if (length == 0) {
return;
}
if (length < 0) {
throw StateError('length should not be negative');
}
if (start < 0 || start >= _length) {
throw IndexError.withLength(start, _length);
}
final end = start + length - 1;
// Slide everything over in reverse
final toCopyTotal = _length - start;
var dest = end + toCopyTotal;
var src = _length - 1;
(int, Uint8List)? lastSrcChunk;
(int, Uint8List)? lastDestChunk;
while (src >= start) {
final srcChunkNumber = src ~/ _indexStride;
final srcIndex = src % _indexStride;
final srcLength = srcIndex + 1;
final srcChunk =
(lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber))
? lastSrcChunk.$2
: await _loadIndexChunk(srcChunkNumber);
lastSrcChunk = (srcChunkNumber, srcChunk);
final destChunkNumber = dest ~/ _indexStride;
final destIndex = dest % _indexStride;
final destLength = destIndex + 1;
final destChunk =
(lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber))
? lastDestChunk.$2
: await _loadIndexChunk(destChunkNumber);
lastDestChunk = (destChunkNumber, destChunk);
final toCopy = min(srcLength, destLength);
destChunk.setRange((destIndex - (toCopy - 1)) * 4, (destIndex + 1) * 4,
srcChunk, (srcIndex - (toCopy - 1)) * 4);
dest -= toCopy;
src -= toCopy;
}
// Then add to length
_length += length;
}
Future<void> _removeIndexEntry(int pos) async => _removeIndexEntries(pos, 1);
Future<void> _removeIndexEntries(int start, int length) async {
if (length == 0) {
return;
}
if (length < 0) {
throw StateError('length should not be negative');
}
if (start < 0 || start >= _length) {
throw IndexError.withLength(start, _length);
}
final end = start + length - 1;
if (end < 0 || end >= _length) {
throw IndexError.withLength(end, _length);
}
// Slide everything over
var dest = start;
var src = end + 1;
(int, Uint8List)? lastSrcChunk;
(int, Uint8List)? lastDestChunk;
while (src < _length) {
final srcChunkNumber = src ~/ _indexStride;
final srcIndex = src % _indexStride;
final srcLength = _indexStride - srcIndex;
final srcChunk =
(lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber))
? lastSrcChunk.$2
: await _loadIndexChunk(srcChunkNumber);
lastSrcChunk = (srcChunkNumber, srcChunk);
final destChunkNumber = dest ~/ _indexStride;
final destIndex = dest % _indexStride;
final destLength = _indexStride - destIndex;
final destChunk =
(lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber))
? lastDestChunk.$2
: await _loadIndexChunk(destChunkNumber);
lastDestChunk = (destChunkNumber, destChunk);
final toCopy = min(srcLength, destLength);
destChunk.setRange(
destIndex * 4, (destIndex + toCopy) * 4, srcChunk, srcIndex * 4);
dest += toCopy;
src += toCopy;
}
// Then truncate
_length -= length;
}
Future<Uint8List> _loadIndexChunk(int chunkNumber) async {
// Get it from the dirty chunks if we have it
final dirtyChunk = _dirtyChunks[chunkNumber];
if (dirtyChunk != null) {
return dirtyChunk;
}
// Get from cache if we have it
for (var i = 0; i < _chunkCache.length; i++) {
if (_chunkCache[i].$1 == chunkNumber) {
// Touch the element
final x = _chunkCache.removeAt(i);
_chunkCache.add(x);
// Return the chunk for this position
return x.$2;
}
}
// Get chunk from disk
var chunk = await _tableDB.load(0, _chunkKey(chunkNumber));
chunk ??= Uint8List(_indexStride * 4);
// Cache the chunk
_chunkCache.add((chunkNumber, chunk));
if (_chunkCache.length > _chunkCacheLength) {
// Trim the LRU cache
final (_, _) = _chunkCache.removeAt(0);
}
return chunk;
}
Future<void> _flushDirtyChunks(VeilidTableDBTransaction t) async {
for (final ec in _dirtyChunks.entries) {
await _tableDB.store(0, _chunkKey(ec.key), ec.value);
}
_dirtyChunks.clear();
}
Future<void> _loadHead() async {
assert(_mutex.isLocked, 'should be locked');
final headBytes = await _tableDB.load(0, _headKey);
if (headBytes == null) {
_length = 0;
_nextFree = 0;
} else {
final b = headBytes.buffer.asByteData();
_length = b.getUint32(0);
_nextFree = b.getUint32(4);
}
}
Future<void> _saveHead(VeilidTableDBTransaction t) async {
assert(_mutex.isLocked, 'should be locked');
final b = ByteData(8)
..setUint32(0, _length)
..setUint32(4, _nextFree);
await t.store(0, _headKey, b.buffer.asUint8List());
}
Future<int> _allocateEntry() async {
assert(_mutex.isLocked, 'should be locked');
if (_nextFree == 0) {
return _length;
}
// pop endogenous free list
final free = _nextFree;
final nextFreeBytes = await _tableDB.load(0, _entryKey(free));
_nextFree = nextFreeBytes!.buffer.asByteData().getUint8(0);
return free;
}
Future<void> _freeEntry(VeilidTableDBTransaction t, int entry) async {
assert(_mutex.isLocked, 'should be locked');
// push endogenous free list
final b = ByteData(4)..setUint32(0, _nextFree);
await t.store(0, _entryKey(entry), b.buffer.asUint8List());
_nextFree = entry;
}
final String _table;
late final VeilidTableDB _tableDB;
final VeilidCrypto _crypto;
final WaitSet<void> _initWait = WaitSet();
final Mutex _mutex = Mutex();
// Head state
int _length = 0;
int _nextFree = 0;
static const int _indexStride = 16384;
final List<(int, Uint8List)> _chunkCache = [];
final Map<int, Uint8List> _dirtyChunks = {};
static const int _chunkCacheLength = 3;
final StreamController<void> _changeStream = StreamController.broadcast();
}

View File

@ -0,0 +1,52 @@
import 'dart:async';
import 'dart:typed_data';
import '../../../veilid_support.dart';
abstract class VeilidCrypto {
Future<Uint8List> encrypt(Uint8List data);
Future<Uint8List> decrypt(Uint8List data);
}
////////////////////////////////////
/// Encrypted for a specific symmetric key
class VeilidCryptoPrivate implements VeilidCrypto {
VeilidCryptoPrivate._(VeilidCryptoSystem cryptoSystem, SharedSecret secretKey)
: _cryptoSystem = cryptoSystem,
_secretKey = secretKey;
final VeilidCryptoSystem _cryptoSystem;
final SharedSecret _secretKey;
static Future<VeilidCryptoPrivate> fromTypedKeyPair(
TypedKeyPair typedKeyPair) async {
final cryptoSystem =
await Veilid.instance.getCryptoSystem(typedKeyPair.kind);
final secretKey = typedKeyPair.secret;
return VeilidCryptoPrivate._(cryptoSystem, secretKey);
}
static Future<VeilidCryptoPrivate> fromSecret(
CryptoKind kind, SharedSecret secretKey) async {
final cryptoSystem = await Veilid.instance.getCryptoSystem(kind);
return VeilidCryptoPrivate._(cryptoSystem, secretKey);
}
@override
Future<Uint8List> encrypt(Uint8List data) =>
_cryptoSystem.encryptNoAuthWithNonce(data, _secretKey);
@override
Future<Uint8List> decrypt(Uint8List data) =>
_cryptoSystem.decryptNoAuthWithNonce(data, _secretKey);
}
////////////////////////////////////
/// No encryption
class VeilidCryptoPublic implements VeilidCrypto {
const VeilidCryptoPublic();
@override
Future<Uint8List> encrypt(Uint8List data) async => data;
@override
Future<Uint8List> decrypt(Uint8List data) async => data;
}

View File

@ -14,4 +14,6 @@ export 'src/output.dart';
export 'src/persistent_queue.dart';
export 'src/protobuf_tools.dart';
export 'src/table_db.dart';
export 'src/table_db_array.dart';
export 'src/veilid_crypto.dart';
export 'src/veilid_log.dart' hide veilidLoggy;

View File

@ -146,7 +146,7 @@ packages:
source: hosted
version: "1.3.0"
charcode:
dependency: transitive
dependency: "direct main"
description:
name: charcode
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306

View File

@ -10,6 +10,7 @@ dependencies:
async_tools: ^0.1.1
bloc: ^8.1.4
bloc_advanced_tools: ^0.1.1
charcode: ^1.3.1
collection: ^1.18.0
equatable: ^2.0.5
fast_immutable_collections: ^10.2.3