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:collection';
import 'dart:math';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../proto/proto.dart' as proto;
import 'author_input_source.dart';
import 'message_integrity.dart';
import 'output_position.dart';
class AuthorInputQueue {
AuthorInputQueue({
required this.author,
required this.inputSource,
required this.lastOutputPosition,
required this.onError,
}):
assert(inputSource.messages.count>0, 'no input source window length'),
assert(inputSource.messages.elements.isNotEmpty, 'no input source elements'),
assert(inputSource.messages.tail >= inputSource.messages.elements.length, 'tail is before initial messages end'),
assert(inputSource.messages.tail > 0, 'tail is not greater than zero'),
currentPosition = inputSource.messages.tail,
currentWindow = inputSource.messages.elements,
windowLength = inputSource.messages.count,
windowFirst = inputSource.messages.tail - inputSource.messages.elements.length,
windowLast = inputSource.messages.tail - 1;
AuthorInputQueue._({
required TypedKey author,
required AuthorInputSource inputSource,
required OutputPosition? outputPosition,
required void Function(Object, StackTrace?) onError,
required MessageIntegrity messageIntegrity,
}) : _author = author,
_onError = onError,
_inputSource = inputSource,
_outputPosition = outputPosition,
_lastMessage = outputPosition?.message,
_messageIntegrity = messageIntegrity,
_currentPosition = inputSource.currentWindow.last;
////////////////////////////////////////////////////////////////////////////
bool get isEmpty => toReconcile.isEmpty;
proto.Message? get current => toReconcile.firstOrNull;
bool consume() {
toReconcile.removeFirst();
return toReconcile.isNotEmpty;
static Future<AuthorInputQueue?> create({
required TypedKey author,
required AuthorInputSource inputSource,
required OutputPosition? outputPosition,
required void Function(Object, StackTrace?) onError,
}) async {
final queue = AuthorInputQueue._(
author: author,
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
// the current cubit state which is at the tail of the log
// Find the last reconciled message for this author
////////////////////////////////////////////////////////////////////////////
// Public interface
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) {
for (var rn = currentWindow.length;
rn >= 0 && currentPosition >= 0;
rn--, currentPosition--) {
final elem = currentWindow[rn];
_lastMessage = _currentMessage;
// If we've found an input element that is older than our last
// reconciled message for this author, then we stop
if (lastOutputPosition != null) {
if (elem.value.timestamp < lastOutputPosition!.message.timestamp) {
break outer;
}
}
_currentPosition++;
// Drop the 'offline' elements because we don't reconcile
// anything until it has been confirmed to be committed to the DHT
if (elem.isOffline) {
// Get more window if we need to
if (!await _updateWindow()) {
// 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;
}
// 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;
}
////////////////////////////////////////////////////////////////////////////
// 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
Future<bool> updateWindow() async {
// Check if we are still in the window
if (currentPosition>=windowFirst && currentPosition <= windowLast) {
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;
Future<bool> _updateWindow() async {
// Check if we are still in the window
if (_currentPosition >= _inputSource.currentWindow.first &&
_currentPosition <= _inputSource.currentWindow.last) {
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 ListQueue<proto.Message> toReconcile = ListQueue<proto.Message>();
final AuthorInputSource inputSource;
final OutputPosition? lastOutputPosition;
final void Function(Object, StackTrace?) onError;
final TypedKey _author;
final AuthorInputSource _inputSource;
final OutputPosition? _outputPosition;
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
int currentPosition;
// The current input window elements
IList<DHTLogElementState<proto.Message>> currentWindow;
// The first position of the sliding input window
int windowFirst;
// The last position of the sliding input window
int windowLast;
int _currentPosition;
// The current message we're looking at
proto.Message? _currentMessage;
// Desired maximum window length
int windowLength;
static const int _maxQueueChunk = 256;
static const int _maxWindowLength = 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 '../../../proto/proto.dart' as proto;
class AuthorInputSource {
AuthorInputSource({required this.messages, required this.cubit});
final DHTLogStateData<proto.Message> messages;
final DHTLogCubit<proto.Message> cubit;
@immutable
class InputWindow {
const InputWindow(
{required this.elements, required this.first, required this.last});
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(
TypedKey author,
DHTLogStateData<proto.Message> inputMessages,
DHTLogStateData<proto.Message> inputMessagesCubitState,
DHTLogCubit<proto.Message> inputMessagesCubit) {
if (inputMessages.elements.isEmpty) {
if (inputMessagesCubitState.elements.isEmpty) {
return;
}
_inputSources[author] =
AuthorInputSource(messages: inputMessages, cubit: inputMessagesCubit);
_inputSources[author] = AuthorInputSource.fromCubit(
cubitState: inputMessagesCubitState, cubit: inputMessagesCubit);
singleFuture(this, onError: _onError, () async {
// Take entire list of input sources we have currently and process them
@ -63,14 +63,15 @@ class MessageReconciliation {
{required TypedKey author,
required AuthorInputSource inputSource}) async {
// Get the position of our most recent reconciled message from this author
final lastReconciledMessage =
await _findNewestReconciledMessage(author: author);
final outputPosition = await _findLastOutputPosition(author: author);
// Find oldest message we have not yet reconciled
final inputQueue = await _buildAuthorInputQueue(
author: author,
inputSource: inputSource,
lastOutputPosition: lastReconciledMessage);
final inputQueue = await AuthorInputQueue.create(
author: author,
inputSource: inputSource,
outputPosition: outputPosition,
onError: _onError,
);
return inputQueue;
}
@ -78,7 +79,7 @@ class MessageReconciliation {
// reconciled message from this 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
Future<OutputPosition?> _findNewestReconciledMessage(
Future<OutputPosition?> _findLastOutputPosition(
{required TypedKey author}) async =>
_outputCubit.operate((arr) async {
var pos = arr.length - 1;
@ -95,26 +96,6 @@ class MessageReconciliation {
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
// into the output array, performing validation steps along the way
Future<void> _reconcileInputQueues({
@ -130,8 +111,8 @@ class MessageReconciliation {
// Sort queues from earliest to latest and then by author
// to ensure a deterministic insert order
inputQueues.sort((a, b) {
final acmp = a.lastOutputPosition?.pos ?? -1;
final bcmp = b.lastOutputPosition?.pos ?? -1;
final acmp = a.outputPosition?.pos ?? -1;
final bcmp = b.outputPosition?.pos ?? -1;
if (acmp == bcmp) {
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
final firstOutputPos = inputQueues.first.lastOutputPosition?.pos;
final firstOutputPos = inputQueues.first.outputPosition?.pos;
// Get the timestamp for this output position
var currentOutputMessage = firstOutputPos == null
? null
@ -167,7 +148,7 @@ class MessageReconciliation {
added = true;
// Advance this queue
if (!inputQueue.consume()) {
if (!await inputQueue.consume()) {
// Queue is empty now, run a queue purge
someQueueEmpty = true;
}

View File

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

View File

@ -1,6 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:async_tools/async_tools.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 '../../proto/proto.dart' as proto;
import '../models/models.dart';
import 'message_reconciliation.dart';
import 'reconciliation/reconciliation.dart';
class RenderStateElement {
RenderStateElement(
@ -102,12 +100,11 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Make crypto
Future<void> _initCrypto() async {
_messagesCrypto = await _activeAccountInfo
_conversationCrypto = await _activeAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey);
_localMessagesCryptoSystem =
await Veilid.instance.getCryptoSystem(_localMessagesRecordKey.kind);
_identityCryptoSystem =
await _activeAccountInfo.localAccount.identityMaster.identityCrypto;
_senderMessageIntegrity = await MessageIntegrity.create(
author: _activeAccountInfo.localAccount.identityMaster
.identityPublicTypedKey());
}
// Open local messages key
@ -119,7 +116,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::'
'SentMessages',
parent: _localConversationRecordKey,
crypto: _messagesCrypto),
crypto: _conversationCrypto),
decodeElement: proto.Message.fromBuffer);
_sentSubscription =
_sentMessagesCubit!.stream.listen(_updateSentMessagesState);
@ -133,7 +130,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::'
'RcvdMessages',
parent: _remoteConversationRecordKey,
crypto: _messagesCrypto),
crypto: _conversationCrypto),
decodeElement: proto.Message.fromBuffer);
_rcvdSubscription =
_rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState);
@ -222,31 +219,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
_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(
proto.Message message, proto.Message? previousMessage) async {
// Get the previous message if we don't have one
@ -255,20 +227,12 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
? null
: await r.getProtobuf(proto.Message.fromBuffer, r.length - 1));
if (previousMessage == null) {
// If there's no last sent message,
// 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);
}
message.id =
await _senderMessageIntegrity.generateMessageId(previousMessage);
// Now sign it
await _signMessage(message);
await _senderMessageIntegrity.signMessage(
message, _activeAccountInfo.userLogin.identitySecret.value);
}
// Async process to send messages in the background
@ -303,17 +267,17 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Generate state for each message
final sentMessagesMap =
IMap<String, DHTLogElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.uniqueIdString,
IMap<String, OnlineElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.authorUniqueIdString,
values: sentMessages.elements,
);
final reconciledMessagesMap =
IMap<String, proto.ReconciledMessage>.fromValues(
keyMapper: (x) => x.content.uniqueIdString,
keyMapper: (x) => x.content.authorUniqueIdString,
values: reconciledMessages.elements,
);
final sendingMessagesMap = IMap<String, proto.Message>.fromValues(
keyMapper: (x) => x.uniqueIdString,
keyMapper: (x) => x.authorUniqueIdString,
values: sendingMessages,
);
@ -405,9 +369,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
final TypedKey _remoteConversationRecordKey;
final TypedKey _remoteMessagesRecordKey;
late final VeilidCrypto _messagesCrypto;
late final VeilidCryptoSystem _localMessagesCryptoSystem;
late final VeilidCryptoSystem _identityCryptoSystem;
late final VeilidCrypto _conversationCrypto;
late final MessageIntegrity _senderMessageIntegrity;
DHTLogCubit<proto.Message>? _sentMessagesCubit;
DHTLogCubit<proto.Message>? _rcvdMessagesCubit;

View File

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

View File

@ -16,13 +16,15 @@ Map<String, dynamic> reconciledMessageToJson(proto.ReconciledMessage m) =>
m.writeToJsonMap();
extension MessageExt on proto.Message {
Uint8List get uniqueIdBytes {
Uint8List get idBytes => Uint8List.fromList(id);
Uint8List get authorUniqueIdBytes {
final author = this.author.toVeilid().decode();
final id = this.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) =>
a.timestamp.compareTo(b.timestamp);

View File

@ -9,16 +9,6 @@ import 'package:meta/meta.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
class DHTLogStateData<T> extends Equatable {
const DHTLogStateData(
@ -28,7 +18,7 @@ class DHTLogStateData<T> extends Equatable {
required this.follow});
// The view of the elements in the dhtlog
// Span is from [tail-length, tail)
final IList<DHTLogElementState<T>> elements;
final IList<OnlineElementState<T>> elements;
// One past the end of the last element
final int tail;
// 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,
{bool forceRefresh = false}) async {
final avElements = await loadElements(_tail, _count);
final avElements = await operate(
(reader) => loadElementsFromReader(reader, _tail, _count));
final err = avElements.asError;
if (err != null) {
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
Future<AsyncValue<IList<DHTLogElementState<T>>>> loadElements(
int tail, int count,
Future<AsyncValue<IList<OnlineElementState<T>>>> loadElementsFromReader(
DHTLogReadOperations reader, int tail, int count,
{bool forceRefresh = false}) async {
await _initWait();
try {
final allItems = await _log.operate((reader) async {
final length = reader.length;
final end = ((tail - 1) % length) + 1;
final start = (count < end) ? end - count : 0;
final length = reader.length;
final end = ((tail - 1) % length) + 1;
final start = (count < end) ? end - count : 0;
final offlinePositions = await reader.getOfflinePositions();
final allItems = (await reader.getRange(start,
length: end - start, forceRefresh: forceRefresh))
?.indexed
.map((x) => DHTLogElementState(
value: _decodeElement(x.$2),
isOffline: offlinePositions.contains(x.$1)))
.toIList();
return allItems;
});
final offlinePositions = await reader.getOfflinePositions();
final allItems = (await reader.getRange(start,
length: end - start, forceRefresh: forceRefresh))
?.indexed
.map((x) => OnlineElementState(
value: _decodeElement(x.$2),
isOffline: offlinePositions.contains(x.$1)))
.toIList();
if (allItems == null) {
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/json_tools.dart';
export 'src/memory_tools.dart';
export 'src/online_element_state.dart';
export 'src/output.dart';
export 'src/persistent_queue.dart';
export 'src/protobuf_tools.dart';