message integrity

This commit is contained in:
Christien Rioux 2024-05-31 18:27:50 -04:00
parent 490051a650
commit fd63a0d5e0
11 changed files with 370 additions and 237 deletions

View File

@ -1,145 +1,191 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'dart:math';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../../proto/proto.dart' as proto; import '../../../proto/proto.dart' as proto;
import 'author_input_source.dart'; import 'author_input_source.dart';
import 'message_integrity.dart';
import 'output_position.dart'; import 'output_position.dart';
class AuthorInputQueue { class AuthorInputQueue {
AuthorInputQueue({ AuthorInputQueue._({
required this.author, required TypedKey author,
required this.inputSource, required AuthorInputSource inputSource,
required this.lastOutputPosition, required OutputPosition? outputPosition,
required this.onError, required void Function(Object, StackTrace?) onError,
}): required MessageIntegrity messageIntegrity,
assert(inputSource.messages.count>0, 'no input source window length'), }) : _author = author,
assert(inputSource.messages.elements.isNotEmpty, 'no input source elements'), _onError = onError,
assert(inputSource.messages.tail >= inputSource.messages.elements.length, 'tail is before initial messages end'), _inputSource = inputSource,
assert(inputSource.messages.tail > 0, 'tail is not greater than zero'), _outputPosition = outputPosition,
currentPosition = inputSource.messages.tail, _lastMessage = outputPosition?.message,
currentWindow = inputSource.messages.elements, _messageIntegrity = messageIntegrity,
windowLength = inputSource.messages.count, _currentPosition = inputSource.currentWindow.last;
windowFirst = inputSource.messages.tail - inputSource.messages.elements.length,
windowLast = inputSource.messages.tail - 1;
//////////////////////////////////////////////////////////////////////////// static Future<AuthorInputQueue?> create({
required TypedKey author,
bool get isEmpty => toReconcile.isEmpty; required AuthorInputSource inputSource,
required OutputPosition? outputPosition,
proto.Message? get current => toReconcile.firstOrNull; required void Function(Object, StackTrace?) onError,
}) async {
bool consume() { final queue = AuthorInputQueue._(
toReconcile.removeFirst(); author: author,
return toReconcile.isNotEmpty; inputSource: inputSource,
outputPosition: outputPosition,
onError: onError,
messageIntegrity: await MessageIntegrity.create(author: author));
if (!await queue._findStartOfWork()) {
return null;
}
return queue;
} }
Future<bool> prepareInputQueue() async { ////////////////////////////////////////////////////////////////////////////
// Go through batches of the input dhtlog starting with // Public interface
// the current cubit state which is at the tail of the log
// Find the last reconciled message for this author
outer: // Check if there are no messages in this queue to reconcile
bool get isEmpty => _currentMessage == null;
// Get the current message that needs reconciliation
proto.Message? get current => _currentMessage;
// Get the earliest output position to start inserting
OutputPosition? get outputPosition => _outputPosition;
// Get the author of this queue
TypedKey get author => _author;
// Remove a reconciled message and move to the next message
// Returns true if there is more work to do
Future<bool> consume() async {
while (true) { while (true) {
for (var rn = currentWindow.length; _lastMessage = _currentMessage;
rn >= 0 && currentPosition >= 0;
rn--, currentPosition--) {
final elem = currentWindow[rn];
// If we've found an input element that is older than our last _currentPosition++;
// reconciled message for this author, then we stop
if (lastOutputPosition != null) {
if (elem.value.timestamp < lastOutputPosition!.message.timestamp) {
break outer;
}
}
// Drop the 'offline' elements because we don't reconcile // Get more window if we need to
// anything until it has been confirmed to be committed to the DHT if (!await _updateWindow()) {
if (elem.isOffline) { // Window is not available so this queue can't work right now
return false;
}
final nextMessage = _inputSource.currentWindow
.elements[_currentPosition - _inputSource.currentWindow.first];
// Drop the 'offline' elements because we don't reconcile
// anything until it has been confirmed to be committed to the DHT
if (nextMessage.isOffline) {
continue;
}
if (_lastMessage != null) {
// Ensure the timestamp is not moving backward
if (nextMessage.value.timestamp < _lastMessage!.timestamp) {
continue; continue;
} }
// Add to head of reconciliation queue
toReconcile.addFirst(elem.value);
if (toReconcile.length > _maxQueueChunk) {
toReconcile.removeLast();
}
}
if (currentPosition < 0) {
break;
} }
xxx update window here and make this and other methods work // Verify the id chain for the message
final matchId = await _messageIntegrity.generateMessageId(_lastMessage);
if (matchId.compare(nextMessage.value.idBytes) != 0) {
continue;
}
// Verify the signature for the message
if (!await _messageIntegrity.verifyMessage(nextMessage.value)) {
continue;
}
_currentMessage = nextMessage.value;
break;
} }
return true; return true;
} }
////////////////////////////////////////////////////////////////////////////
// Internal implementation
// Walk backward from the tail of the input queue to find the first
// message newer than our last reconcicled message from this author
// Returns false if no work is needed
Future<bool> _findStartOfWork() async {
// Iterate windows over the inputSource
outer:
while (true) {
// Iterate through current window backward
for (var i = _inputSource.currentWindow.elements.length;
i >= 0 && _currentPosition >= 0;
i--, _currentPosition--) {
final elem = _inputSource.currentWindow.elements[i];
// If we've found an input element that is older than our last
// reconciled message for this author, then we stop
if (_lastMessage != null) {
if (elem.value.timestamp < _lastMessage!.timestamp) {
break outer;
}
}
}
// If we're at the beginning of the inputSource then we stop
if (_currentPosition < 0) {
break;
}
// Get more window if we need to
if (!await _updateWindow()) {
// Window is not available or things are empty so this
// queue can't work right now
return false;
}
}
// The current position should be equal to the first message to process
// and the current window to process should not be empty
return _inputSource.currentWindow.elements.isNotEmpty;
}
// Slide the window toward the current position and load the batch around it // Slide the window toward the current position and load the batch around it
Future<bool> updateWindow() async { Future<bool> _updateWindow() async {
// Check if we are still in the window
// Check if we are still in the window if (_currentPosition >= _inputSource.currentWindow.first &&
if (currentPosition>=windowFirst && currentPosition <= windowLast) { _currentPosition <= _inputSource.currentWindow.last) {
return true;
}
// Get the length of the cubit
final inputLength = await inputSource.cubit.operate((r) async => r.length);
// If not, slide the window
if (currentPosition<windowFirst) {
// Slide it backward, current position is now windowLast
windowFirst = max((currentPosition - windowLength) + 1, 0);
windowLast = currentPosition;
} else {
// Slide it forward, current position is now windowFirst
windowFirst = currentPosition;
windowLast = min((currentPosition + windowLength) - 1, inputLength - 1);
}
// Get another input batch futher back
final nextWindow =
await inputSource.cubit.loadElements(windowLast + 1, (windowLast + 1) - windowFirst);
final asErr = nextWindow.asError;
if (asErr != null) {
onError(asErr.error, asErr.stackTrace);
return false;
}
final asLoading = nextWindow.asLoading;
if (asLoading != null) {
// xxx: no need to block the cubit here for this
// xxx: might want to switch to a 'busy' state though
// xxx: to let the messages view show a spinner at the bottom
// xxx: while we reconcile...
// emit(const AsyncValue.loading());
return false;
}
currentWindow = nextWindow.asData!.value;
return true; return true;
}
// Get another input batch futher back
final avOk =
await _inputSource.updateWindow(_currentPosition, _maxWindowLength);
final asErr = avOk.asError;
if (asErr != null) {
_onError(asErr.error, asErr.stackTrace);
return false;
}
final asLoading = avOk.asLoading;
if (asLoading != null) {
// xxx: no need to block the cubit here for this
// xxx: might want to switch to a 'busy' state though
// xxx: to let the messages view show a spinner at the bottom
// xxx: while we reconcile...
// emit(const AsyncValue.loading());
return false;
}
return avOk.asData!.value;
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
final TypedKey author; final TypedKey _author;
final ListQueue<proto.Message> toReconcile = ListQueue<proto.Message>(); final AuthorInputSource _inputSource;
final AuthorInputSource inputSource; final OutputPosition? _outputPosition;
final OutputPosition? lastOutputPosition; final void Function(Object, StackTrace?) _onError;
final void Function(Object, StackTrace?) onError; final MessageIntegrity _messageIntegrity;
// The last message we've consumed
proto.Message? _lastMessage;
// The current position in the input log that we are looking at // The current position in the input log that we are looking at
int currentPosition; int _currentPosition;
// The current input window elements // The current message we're looking at
IList<DHTLogElementState<proto.Message>> currentWindow; proto.Message? _currentMessage;
// The first position of the sliding input window
int windowFirst;
// The last position of the sliding input window
int windowLast;
// Desired maximum window length // Desired maximum window length
int windowLength; static const int _maxWindowLength = 256;
static const int _maxQueueChunk = 256;
} }

View File

@ -1,10 +1,76 @@
import 'dart:math';
import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../../proto/proto.dart' as proto; import '../../../proto/proto.dart' as proto;
class AuthorInputSource { @immutable
AuthorInputSource({required this.messages, required this.cubit}); class InputWindow {
const InputWindow(
final DHTLogStateData<proto.Message> messages; {required this.elements, required this.first, required this.last});
final DHTLogCubit<proto.Message> cubit; final IList<OnlineElementState<proto.Message>> elements;
final int first;
final int last;
}
class AuthorInputSource {
AuthorInputSource.fromCubit(
{required DHTLogStateData<proto.Message> cubitState,
required this.cubit}) {
_currentWindow = InputWindow(
elements: cubitState.elements,
first: cubitState.tail - cubitState.elements.length,
last: cubitState.tail - 1);
}
////////////////////////////////////////////////////////////////////////////
InputWindow get currentWindow => _currentWindow;
Future<AsyncValue<bool>> updateWindow(
int currentPosition, int windowLength) async =>
cubit.operate((reader) async {
// See if we're beyond the input source
if (currentPosition < 0 || currentPosition >= reader.length) {
return const AsyncValue.data(false);
}
// Slide the window if we need to
var first = _currentWindow.first;
var last = _currentWindow.last;
if (currentPosition < first) {
// Slide it backward, current position is now last
first = max((currentPosition - windowLength) + 1, 0);
last = currentPosition;
} else if (currentPosition > last) {
// Slide it forward, current position is now first
first = currentPosition;
last = min((currentPosition + windowLength) - 1, reader.length - 1);
} else {
return const AsyncValue.data(true);
}
// Get another input batch futher back
final nextWindow = await cubit.loadElementsFromReader(
reader, last + 1, (last + 1) - first);
final asErr = nextWindow.asError;
if (asErr != null) {
return AsyncValue.error(asErr.error, asErr.stackTrace);
}
final asLoading = nextWindow.asLoading;
if (asLoading != null) {
return const AsyncValue.loading();
}
_currentWindow = InputWindow(
elements: nextWindow.asData!.value, first: first, last: last);
return const AsyncValue.data(true);
});
////////////////////////////////////////////////////////////////////////////
final DHTLogCubit<proto.Message> cubit;
late InputWindow _currentWindow;
} }

View File

@ -0,0 +1,74 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../proto/proto.dart' as proto;
class MessageIntegrity {
MessageIntegrity._({
required TypedKey author,
required VeilidCryptoSystem crypto,
}) : _author = author,
_crypto = crypto;
static Future<MessageIntegrity> create({required TypedKey author}) async {
final crypto = await Veilid.instance.getCryptoSystem(author.kind);
return MessageIntegrity._(author: author, crypto: crypto);
}
////////////////////////////////////////////////////////////////////////////
// Public interface
Future<Uint8List> generateMessageId(proto.Message? previous) async {
if (previous == null) {
// If there's no last sent message,
// we start at a hash of the identity public key
return _generateInitialId();
} else {
// If there is a last message, we generate the hash
// of the last message's signature and use it as our next id
return _hashSignature(previous.signature);
}
}
Future<void> signMessage(
proto.Message message,
SecretKey authorSecret,
) async {
// Ensure this message is not already signed
assert(!message.hasSignature(), 'should not sign message twice');
// Generate data to sign
final data = Uint8List.fromList(utf8.encode(message.writeToJson()));
// Sign with our identity
final signature = await _crypto.sign(_author.value, authorSecret, data);
// Add to the message
message.signature = signature.toProto();
}
Future<bool> verifyMessage(proto.Message message) async {
// Ensure the message is signed
assert(message.hasSignature(), 'should not verify unsigned message');
final signature = message.signature.toVeilid();
// Generate data to sign
final messageNoSig = message.deepCopy()..clearSignature();
final data = Uint8List.fromList(utf8.encode(messageNoSig.writeToJson()));
// Verify signature
return _crypto.verify(_author.value, data, signature);
}
////////////////////////////////////////////////////////////////////////////
// Private implementation
Future<Uint8List> _generateInitialId() async =>
(await _crypto.generateHash(_author.decode())).decode();
Future<Uint8List> _hashSignature(proto.Signature signature) async =>
(await _crypto.generateHash(signature.toVeilid().decode())).decode();
////////////////////////////////////////////////////////////////////////////
final TypedKey _author;
final VeilidCryptoSystem _crypto;
}

View File

@ -21,14 +21,14 @@ class MessageReconciliation {
void reconcileMessages( void reconcileMessages(
TypedKey author, TypedKey author,
DHTLogStateData<proto.Message> inputMessages, DHTLogStateData<proto.Message> inputMessagesCubitState,
DHTLogCubit<proto.Message> inputMessagesCubit) { DHTLogCubit<proto.Message> inputMessagesCubit) {
if (inputMessages.elements.isEmpty) { if (inputMessagesCubitState.elements.isEmpty) {
return; return;
} }
_inputSources[author] = _inputSources[author] = AuthorInputSource.fromCubit(
AuthorInputSource(messages: inputMessages, cubit: inputMessagesCubit); cubitState: inputMessagesCubitState, cubit: inputMessagesCubit);
singleFuture(this, onError: _onError, () async { singleFuture(this, onError: _onError, () async {
// Take entire list of input sources we have currently and process them // Take entire list of input sources we have currently and process them
@ -63,14 +63,15 @@ class MessageReconciliation {
{required TypedKey author, {required TypedKey author,
required AuthorInputSource inputSource}) async { required AuthorInputSource inputSource}) async {
// Get the position of our most recent reconciled message from this author // Get the position of our most recent reconciled message from this author
final lastReconciledMessage = final outputPosition = await _findLastOutputPosition(author: author);
await _findNewestReconciledMessage(author: author);
// Find oldest message we have not yet reconciled // Find oldest message we have not yet reconciled
final inputQueue = await _buildAuthorInputQueue( final inputQueue = await AuthorInputQueue.create(
author: author, author: author,
inputSource: inputSource, inputSource: inputSource,
lastOutputPosition: lastReconciledMessage); outputPosition: outputPosition,
onError: _onError,
);
return inputQueue; return inputQueue;
} }
@ -78,7 +79,7 @@ class MessageReconciliation {
// reconciled message from this author // reconciled message from this author
// XXX: For a group chat, this should find when the author // XXX: For a group chat, this should find when the author
// was added to the membership so we don't just go back in time forever // was added to the membership so we don't just go back in time forever
Future<OutputPosition?> _findNewestReconciledMessage( Future<OutputPosition?> _findLastOutputPosition(
{required TypedKey author}) async => {required TypedKey author}) async =>
_outputCubit.operate((arr) async { _outputCubit.operate((arr) async {
var pos = arr.length - 1; var pos = arr.length - 1;
@ -95,26 +96,6 @@ class MessageReconciliation {
return null; return null;
}); });
// Find oldest message we have not yet reconciled and build a queue forward
// from that position
Future<AuthorInputQueue?> _buildAuthorInputQueue(
{required TypedKey author,
required AuthorInputSource inputSource,
required OutputPosition? lastOutputPosition}) async {
// Make an author input queue
final authorInputQueue = AuthorInputQueue(
author: author,
inputSource: inputSource,
lastOutputPosition: lastOutputPosition,
onError: _onError);
if (!await authorInputQueue.prepareInputQueue()) {
return null;
}
return authorInputQueue;
}
// Process a list of author input queues and insert their messages // Process a list of author input queues and insert their messages
// into the output array, performing validation steps along the way // into the output array, performing validation steps along the way
Future<void> _reconcileInputQueues({ Future<void> _reconcileInputQueues({
@ -130,8 +111,8 @@ class MessageReconciliation {
// Sort queues from earliest to latest and then by author // Sort queues from earliest to latest and then by author
// to ensure a deterministic insert order // to ensure a deterministic insert order
inputQueues.sort((a, b) { inputQueues.sort((a, b) {
final acmp = a.lastOutputPosition?.pos ?? -1; final acmp = a.outputPosition?.pos ?? -1;
final bcmp = b.lastOutputPosition?.pos ?? -1; final bcmp = b.outputPosition?.pos ?? -1;
if (acmp == bcmp) { if (acmp == bcmp) {
return a.author.toString().compareTo(b.author.toString()); return a.author.toString().compareTo(b.author.toString());
} }
@ -139,7 +120,7 @@ class MessageReconciliation {
}); });
// Start at the earliest position we know about in all the queues // Start at the earliest position we know about in all the queues
final firstOutputPos = inputQueues.first.lastOutputPosition?.pos; final firstOutputPos = inputQueues.first.outputPosition?.pos;
// Get the timestamp for this output position // Get the timestamp for this output position
var currentOutputMessage = firstOutputPos == null var currentOutputMessage = firstOutputPos == null
? null ? null
@ -167,7 +148,7 @@ class MessageReconciliation {
added = true; added = true;
// Advance this queue // Advance this queue
if (!inputQueue.consume()) { if (!await inputQueue.consume()) {
// Queue is empty now, run a queue purge // Queue is empty now, run a queue purge
someQueueEmpty = true; someQueueEmpty = true;
} }

View File

@ -1 +1,2 @@
export 'message_integrity.dart';
export 'message_reconciliation.dart'; export 'message_reconciliation.dart';

View File

@ -1,6 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
@ -10,7 +8,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../models/models.dart'; import '../models/models.dart';
import 'message_reconciliation.dart'; import 'reconciliation/reconciliation.dart';
class RenderStateElement { class RenderStateElement {
RenderStateElement( RenderStateElement(
@ -102,12 +100,11 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Make crypto // Make crypto
Future<void> _initCrypto() async { Future<void> _initCrypto() async {
_messagesCrypto = await _activeAccountInfo _conversationCrypto = await _activeAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey); .makeConversationCrypto(_remoteIdentityPublicKey);
_localMessagesCryptoSystem = _senderMessageIntegrity = await MessageIntegrity.create(
await Veilid.instance.getCryptoSystem(_localMessagesRecordKey.kind); author: _activeAccountInfo.localAccount.identityMaster
_identityCryptoSystem = .identityPublicTypedKey());
await _activeAccountInfo.localAccount.identityMaster.identityCrypto;
} }
// Open local messages key // Open local messages key
@ -119,7 +116,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::' debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::'
'SentMessages', 'SentMessages',
parent: _localConversationRecordKey, parent: _localConversationRecordKey,
crypto: _messagesCrypto), crypto: _conversationCrypto),
decodeElement: proto.Message.fromBuffer); decodeElement: proto.Message.fromBuffer);
_sentSubscription = _sentSubscription =
_sentMessagesCubit!.stream.listen(_updateSentMessagesState); _sentMessagesCubit!.stream.listen(_updateSentMessagesState);
@ -133,7 +130,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::'
'RcvdMessages', 'RcvdMessages',
parent: _remoteConversationRecordKey, parent: _remoteConversationRecordKey,
crypto: _messagesCrypto), crypto: _conversationCrypto),
decodeElement: proto.Message.fromBuffer); decodeElement: proto.Message.fromBuffer);
_rcvdSubscription = _rcvdSubscription =
_rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState); _rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState);
@ -222,31 +219,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
_renderState(); _renderState();
} }
Future<Uint8List> _hashSignature(proto.Signature signature) async =>
(await _localMessagesCryptoSystem
.generateHash(signature.toVeilid().decode()))
.decode();
Future<void> _signMessage(proto.Message message) async {
// Generate data to sign
final data = Uint8List.fromList(utf8.encode(message.writeToJson()));
// Sign with our identity
final signature = await _identityCryptoSystem.sign(
_activeAccountInfo.localAccount.identityMaster.identityPublicKey,
_activeAccountInfo.userLogin.identitySecret.value,
data);
// Add to the message
message.signature = signature.toProto();
}
Future<Uint8List> _generateInitialId(
{required PublicKey identityPublicKey}) async =>
(await _localMessagesCryptoSystem
.generateHash(identityPublicKey.decode()))
.decode();
Future<void> _processMessageToSend( Future<void> _processMessageToSend(
proto.Message message, proto.Message? previousMessage) async { proto.Message message, proto.Message? previousMessage) async {
// Get the previous message if we don't have one // Get the previous message if we don't have one
@ -255,20 +227,12 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
? null ? null
: await r.getProtobuf(proto.Message.fromBuffer, r.length - 1)); : await r.getProtobuf(proto.Message.fromBuffer, r.length - 1));
if (previousMessage == null) { message.id =
// If there's no last sent message, await _senderMessageIntegrity.generateMessageId(previousMessage);
// we start at a hash of the identity public key
message.id = await _generateInitialId(
identityPublicKey:
_activeAccountInfo.localAccount.identityMaster.identityPublicKey);
} else {
// If there is a last message, we generate the hash
// of the last message's signature and use it as our next id
message.id = await _hashSignature(previousMessage.signature);
}
// Now sign it // Now sign it
await _signMessage(message); await _senderMessageIntegrity.signMessage(
message, _activeAccountInfo.userLogin.identitySecret.value);
} }
// Async process to send messages in the background // Async process to send messages in the background
@ -303,17 +267,17 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Generate state for each message // Generate state for each message
final sentMessagesMap = final sentMessagesMap =
IMap<String, DHTLogElementState<proto.Message>>.fromValues( IMap<String, OnlineElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.uniqueIdString, keyMapper: (x) => x.value.authorUniqueIdString,
values: sentMessages.elements, values: sentMessages.elements,
); );
final reconciledMessagesMap = final reconciledMessagesMap =
IMap<String, proto.ReconciledMessage>.fromValues( IMap<String, proto.ReconciledMessage>.fromValues(
keyMapper: (x) => x.content.uniqueIdString, keyMapper: (x) => x.content.authorUniqueIdString,
values: reconciledMessages.elements, values: reconciledMessages.elements,
); );
final sendingMessagesMap = IMap<String, proto.Message>.fromValues( final sendingMessagesMap = IMap<String, proto.Message>.fromValues(
keyMapper: (x) => x.uniqueIdString, keyMapper: (x) => x.authorUniqueIdString,
values: sendingMessages, values: sendingMessages,
); );
@ -405,9 +369,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
final TypedKey _remoteConversationRecordKey; final TypedKey _remoteConversationRecordKey;
final TypedKey _remoteMessagesRecordKey; final TypedKey _remoteMessagesRecordKey;
late final VeilidCrypto _messagesCrypto; late final VeilidCrypto _conversationCrypto;
late final VeilidCryptoSystem _localMessagesCryptoSystem; late final MessageIntegrity _senderMessageIntegrity;
late final VeilidCryptoSystem _identityCryptoSystem;
DHTLogCubit<proto.Message>? _sentMessagesCubit; DHTLogCubit<proto.Message>? _sentMessagesCubit;
DHTLogCubit<proto.Message>? _rcvdMessagesCubit; DHTLogCubit<proto.Message>? _rcvdMessagesCubit;

View File

@ -127,7 +127,7 @@ class ChatComponent extends StatelessWidget {
author: isLocal ? _localUser : _remoteUser, author: isLocal ? _localUser : _remoteUser,
createdAt: createdAt:
(message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(),
id: message.content.uniqueIdString, id: message.content.authorUniqueIdString,
text: contextText.text, text: contextText.text,
showStatus: status != null, showStatus: status != null,
status: status); status: status);

View File

@ -16,13 +16,15 @@ Map<String, dynamic> reconciledMessageToJson(proto.ReconciledMessage m) =>
m.writeToJsonMap(); m.writeToJsonMap();
extension MessageExt on proto.Message { extension MessageExt on proto.Message {
Uint8List get uniqueIdBytes { Uint8List get idBytes => Uint8List.fromList(id);
Uint8List get authorUniqueIdBytes {
final author = this.author.toVeilid().decode(); final author = this.author.toVeilid().decode();
final id = this.id; final id = this.id;
return Uint8List.fromList([...author, ...id]); return Uint8List.fromList([...author, ...id]);
} }
String get uniqueIdString => base64UrlNoPadEncode(uniqueIdBytes); String get authorUniqueIdString => base64UrlNoPadEncode(authorUniqueIdBytes);
static int compareTimestamp(proto.Message a, proto.Message b) => static int compareTimestamp(proto.Message a, proto.Message b) =>
a.timestamp.compareTo(b.timestamp); a.timestamp.compareTo(b.timestamp);

View File

@ -9,16 +9,6 @@ import 'package:meta/meta.dart';
import '../../../veilid_support.dart'; import '../../../veilid_support.dart';
@immutable
class DHTLogElementState<T> extends Equatable {
const DHTLogElementState({required this.value, required this.isOffline});
final T value;
final bool isOffline;
@override
List<Object?> get props => [value, isOffline];
}
@immutable @immutable
class DHTLogStateData<T> extends Equatable { class DHTLogStateData<T> extends Equatable {
const DHTLogStateData( const DHTLogStateData(
@ -28,7 +18,7 @@ class DHTLogStateData<T> extends Equatable {
required this.follow}); required this.follow});
// The view of the elements in the dhtlog // The view of the elements in the dhtlog
// Span is from [tail-length, tail) // Span is from [tail-length, tail)
final IList<DHTLogElementState<T>> elements; final IList<OnlineElementState<T>> elements;
// One past the end of the last element // One past the end of the last element
final int tail; final int tail;
// The total number of elements to try to keep in 'elements' // The total number of elements to try to keep in 'elements'
@ -92,7 +82,8 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
Future<void> _refreshInner(void Function(AsyncValue<DHTLogStateData<T>>) emit, Future<void> _refreshInner(void Function(AsyncValue<DHTLogStateData<T>>) emit,
{bool forceRefresh = false}) async { {bool forceRefresh = false}) async {
final avElements = await loadElements(_tail, _count); final avElements = await operate(
(reader) => loadElementsFromReader(reader, _tail, _count));
final err = avElements.asError; final err = avElements.asError;
if (err != null) { if (err != null) {
emit(AsyncValue.error(err.error, err.stackTrace)); emit(AsyncValue.error(err.error, err.stackTrace));
@ -109,26 +100,22 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
} }
// Tail is one past the last element to load // Tail is one past the last element to load
Future<AsyncValue<IList<DHTLogElementState<T>>>> loadElements( Future<AsyncValue<IList<OnlineElementState<T>>>> loadElementsFromReader(
int tail, int count, DHTLogReadOperations reader, int tail, int count,
{bool forceRefresh = false}) async { {bool forceRefresh = false}) async {
await _initWait();
try { try {
final allItems = await _log.operate((reader) async { final length = reader.length;
final length = reader.length; final end = ((tail - 1) % length) + 1;
final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0;
final start = (count < end) ? end - count : 0;
final offlinePositions = await reader.getOfflinePositions(); final offlinePositions = await reader.getOfflinePositions();
final allItems = (await reader.getRange(start, final allItems = (await reader.getRange(start,
length: end - start, forceRefresh: forceRefresh)) length: end - start, forceRefresh: forceRefresh))
?.indexed ?.indexed
.map((x) => DHTLogElementState( .map((x) => OnlineElementState(
value: _decodeElement(x.$2), value: _decodeElement(x.$2),
isOffline: offlinePositions.contains(x.$1))) isOffline: offlinePositions.contains(x.$1)))
.toIList(); .toIList();
return allItems;
});
if (allItems == null) { if (allItems == null) {
return const AsyncValue.loading(); return const AsyncValue.loading();
} }

View File

@ -0,0 +1,12 @@
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
@immutable
class OnlineElementState<T> extends Equatable {
const OnlineElementState({required this.value, required this.isOffline});
final T value;
final bool isOffline;
@override
List<Object?> get props => [value, isOffline];
}

View File

@ -10,6 +10,7 @@ export 'src/config.dart';
export 'src/identity.dart'; export 'src/identity.dart';
export 'src/json_tools.dart'; export 'src/json_tools.dart';
export 'src/memory_tools.dart'; export 'src/memory_tools.dart';
export 'src/online_element_state.dart';
export 'src/output.dart'; export 'src/output.dart';
export 'src/persistent_queue.dart'; export 'src/persistent_queue.dart';
export 'src/protobuf_tools.dart'; export 'src/protobuf_tools.dart';