simplify reconciliation

This commit is contained in:
Christien Rioux 2025-04-19 22:21:40 -04:00
parent bf38c2c44d
commit 4797184a1a
12 changed files with 512 additions and 345 deletions

View file

@ -15,6 +15,7 @@ import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart'; import '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../models/chat_component_state.dart'; import '../models/chat_component_state.dart';
import '../models/message_state.dart'; import '../models/message_state.dart';
import '../models/window_state.dart'; import '../models/window_state.dart';
@ -383,13 +384,13 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
if (chatMessage == null) { if (chatMessage == null) {
continue; continue;
} }
chatMessages.insert(0, chatMessage);
if (!tsSet.add(chatMessage.id)) { if (!tsSet.add(chatMessage.id)) {
// ignore: avoid_print log.error('duplicate id found: ${chatMessage.id}'
print('duplicate id found: ${chatMessage.id}:\n' // '\nMessages:\n${messagesState.window}'
'Messages:\n${messagesState.window}\n' // '\nChatMessages:\n$chatMessages'
'ChatMessages:\n$chatMessages'); );
assert(false, 'should not have duplicate id'); } else {
chatMessages.insert(0, chatMessage);
} }
} }
return currentState.copyWith( return currentState.copyWith(

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
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;
@ -6,106 +7,127 @@ import '../../../proto/proto.dart' as proto;
import '../../../tools/tools.dart'; import '../../../tools/tools.dart';
import 'author_input_source.dart'; import 'author_input_source.dart';
import 'message_integrity.dart'; import 'message_integrity.dart';
import 'output_position.dart';
class AuthorInputQueue { class AuthorInputQueue {
AuthorInputQueue._({ AuthorInputQueue._({
required TypedKey author, required TypedKey author,
required AuthorInputSource inputSource, required AuthorInputSource inputSource,
required OutputPosition? outputPosition, required int inputPosition,
required proto.Message? previousMessage,
required void Function(Object, StackTrace?) onError, required void Function(Object, StackTrace?) onError,
required MessageIntegrity messageIntegrity, required MessageIntegrity messageIntegrity,
}) : _author = author, }) : _author = author,
_onError = onError, _onError = onError,
_inputSource = inputSource, _inputSource = inputSource,
_outputPosition = outputPosition, _previousMessage = previousMessage,
_lastMessage = outputPosition?.message.content,
_messageIntegrity = messageIntegrity, _messageIntegrity = messageIntegrity,
_currentPosition = inputSource.currentWindow.last; _inputPosition = inputPosition;
static Future<AuthorInputQueue?> create({ static Future<AuthorInputQueue?> create({
required TypedKey author, required TypedKey author,
required AuthorInputSource inputSource, required AuthorInputSource inputSource,
required OutputPosition? outputPosition, required proto.Message? previousMessage,
required void Function(Object, StackTrace?) onError, required void Function(Object, StackTrace?) onError,
}) async { }) async {
// Get ending input position
final inputPosition = await inputSource.getTailPosition() - 1;
// Create an input queue for the input source
final queue = AuthorInputQueue._( final queue = AuthorInputQueue._(
author: author, author: author,
inputSource: inputSource, inputSource: inputSource,
outputPosition: outputPosition, inputPosition: inputPosition,
previousMessage: previousMessage,
onError: onError, onError: onError,
messageIntegrity: await MessageIntegrity.create(author: author)); messageIntegrity: await MessageIntegrity.create(author: author));
if (!await queue._findStartOfWork()) {
// Rewind the queue's 'inputPosition' to the first unreconciled message
if (!await queue._rewindInputToAfterLastMessage()) {
return null; return null;
} }
return queue; return queue;
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Public interface // Public interface
// Check if there are no messages left in this queue to reconcile /// Get the input source for this queue
bool get isDone => _isDone; AuthorInputSource get inputSource => _inputSource;
// Get the current message that needs reconciliation /// Get the author of this queue
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; TypedKey get author => _author;
// Remove a reconciled message and move to the next message /// Get the current message that needs reconciliation
// Returns true if there is more work to do Future<proto.Message?> getCurrentMessage() async {
Future<bool> consume() async { try {
if (_isDone) { // if we have a current message already, return it
if (_currentMessage != null) {
return _currentMessage;
}
// Get the window
final currentWindow = await _updateWindow(clampInputPosition: false);
if (currentWindow == null) {
return null;
}
final currentElement =
currentWindow.elements[_inputPosition - currentWindow.firstPosition];
return _currentMessage = currentElement.value;
// Catch everything so we can avoid ParallelWaitError
// ignore: avoid_catches_without_on_clauses
} catch (e, st) {
log.error('Exception getting current message: $e:\n$st\n');
_currentMessage = null;
return null;
}
}
/// Remove a reconciled message and move to the next message
/// Returns true if there is more work to do
Future<bool> advance() async {
final currentMessage = await getCurrentMessage();
if (currentMessage == null) {
return false; return false;
} }
while (true) {
_lastMessage = _currentMessage;
_currentPosition++; // Move current message to previous
_previousMessage = _currentMessage;
_currentMessage = null;
while (true) {
// Advance to next position
_inputPosition++;
// Get more window if we need to // Get more window if we need to
if (!await _updateWindow()) { final currentMessage = await getCurrentMessage();
// Window is not available so this queue can't work right now if (currentMessage == null) {
_isDone = true;
return false; return false;
} }
final nextMessage = _inputSource.currentWindow
.elements[_currentPosition - _inputSource.currentWindow.first];
// Drop the 'offline' elements because we don't reconcile if (_previousMessage != null) {
// 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 // Ensure the timestamp is not moving backward
if (nextMessage.value.timestamp < _lastMessage!.timestamp) { if (currentMessage.timestamp < _previousMessage!.timestamp) {
log.warning('timestamp backward: ${nextMessage.value.timestamp}' log.warning('timestamp backward: ${currentMessage.timestamp}'
' < ${_lastMessage!.timestamp}'); ' < ${_previousMessage!.timestamp}');
continue; continue;
} }
} }
// Verify the id chain for the message // Verify the id chain for the message
final matchId = await _messageIntegrity.generateMessageId(_lastMessage); final matchId =
if (matchId.compare(nextMessage.value.idBytes) != 0) { await _messageIntegrity.generateMessageId(_previousMessage);
log.warning( if (matchId.compare(currentMessage.idBytes) != 0) {
'id chain invalid: $matchId != ${nextMessage.value.idBytes}'); log.warning('id chain invalid: $matchId != ${currentMessage.idBytes}');
continue; continue;
} }
// Verify the signature for the message // Verify the signature for the message
if (!await _messageIntegrity.verifyMessage(nextMessage.value)) { if (!await _messageIntegrity.verifyMessage(currentMessage)) {
log.warning('invalid message signature: ${nextMessage.value}'); log.warning('invalid message signature: $currentMessage');
continue; continue;
} }
_currentMessage = nextMessage.value;
break; break;
} }
return true; return true;
@ -114,106 +136,166 @@ class AuthorInputQueue {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Internal implementation // Internal implementation
// Walk backward from the tail of the input queue to find the first /// Walk backward from the tail of the input queue to find the first
// message newer than our last reconciled message from this author /// message newer than our last reconciled message from this author
// Returns false if no work is needed /// Returns false if no work is needed
Future<bool> _findStartOfWork() async { Future<bool> _rewindInputToAfterLastMessage() async {
// Iterate windows over the inputSource // Iterate windows over the inputSource
InputWindow? currentWindow;
outer: outer:
while (true) { while (true) {
// Get more window if we need to
currentWindow = await _updateWindow(clampInputPosition: true);
if (currentWindow == null) {
// Window is not available or things are empty so this
// queue can't work right now
return false;
}
// Iterate through current window backward // Iterate through current window backward
for (var i = _inputSource.currentWindow.elements.length - 1; for (var i = currentWindow.elements.length - 1;
i >= 0 && _currentPosition >= 0; i >= 0 && _inputPosition >= 0;
i--, _currentPosition--) { i--, _inputPosition--) {
final elem = _inputSource.currentWindow.elements[i]; final elem = currentWindow.elements[i];
// If we've found an input element that is older or same time as our // If we've found an input element that is older or same time as our
// last reconciled message for this author, or we find the message // last reconciled message for this author, or we find the message
// itself then we stop // itself then we stop
if (_lastMessage != null) { if (_previousMessage != null) {
if (elem.value.authorUniqueIdBytes if (elem.value.authorUniqueIdBytes
.compare(_lastMessage!.authorUniqueIdBytes) == .compare(_previousMessage!.authorUniqueIdBytes) ==
0 || 0 ||
elem.value.timestamp <= _lastMessage!.timestamp) { elem.value.timestamp <= _previousMessage!.timestamp) {
break outer; break outer;
} }
} }
} }
// If we're at the beginning of the inputSource then we stop // If we're at the beginning of the inputSource then we stop
if (_currentPosition < 0) { if (_inputPosition < 0) {
break; 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
_isDone = true;
return false;
}
} }
// _currentPosition points to either before the input source starts // _inputPosition points to either before the input source starts
// or the position of the previous element. We still need to set the // or the position of the previous element. We still need to set the
// _currentMessage to the previous element so consume() can compare // _currentMessage to the previous element so consume() can compare
// against it if we can. // against it if we can.
if (_currentPosition >= 0) { if (_inputPosition >= 0) {
_currentMessage = _inputSource.currentWindow _currentMessage = currentWindow
.elements[_currentPosition - _inputSource.currentWindow.first].value; .elements[_inputPosition - currentWindow.firstPosition].value;
} }
// After this consume(), the currentPosition and _currentMessage should // After this advance(), the _inputPosition and _currentMessage should
// be equal to the first message to process and the current window to // be equal to the first message to process and the current window to
// process should not be empty // process should not be empty if there is work to do
return consume(); return advance();
}
/// Slide the window toward the current position and load the batch around it
Future<InputWindow?> _updateWindow({required bool clampInputPosition}) async {
final inputTailPosition = await _inputSource.getTailPosition();
if (inputTailPosition == 0) {
return null;
}
// Handle out-of-range input position
if (clampInputPosition) {
_inputPosition = min(max(_inputPosition, 0), inputTailPosition - 1);
} else if (_inputPosition < 0 || _inputPosition >= inputTailPosition) {
return null;
} }
// 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 // Check if we are still in the window
if (_currentPosition >= _inputSource.currentWindow.first && final currentWindow = _currentWindow;
_currentPosition <= _inputSource.currentWindow.last) {
return true; int firstPosition;
int lastPosition;
if (currentWindow != null) {
firstPosition = currentWindow.firstPosition;
lastPosition = currentWindow.lastPosition;
// Slide the window if we need to
if (_inputPosition >= firstPosition && _inputPosition <= lastPosition) {
return currentWindow;
} else if (_inputPosition < firstPosition) {
// Slide it backward, current position is now last
firstPosition = max((_inputPosition - _maxWindowLength) + 1, 0);
lastPosition = _inputPosition;
} else if (_inputPosition > lastPosition) {
// Slide it forward, current position is now first
firstPosition = _inputPosition;
lastPosition =
min((_inputPosition + _maxWindowLength) - 1, inputTailPosition - 1);
}
} else {
// need a new window, start with the input position at the end
lastPosition = _inputPosition;
firstPosition = max((_inputPosition - _maxWindowLength) + 1, 0);
} }
// Get another input batch futher back // Get another input batch futher back
final avOk = final avCurrentWindow = await _inputSource.getWindow(
await _inputSource.updateWindow(_currentPosition, _maxWindowLength); firstPosition, lastPosition - firstPosition + 1);
final asErr = avOk.asError; final asErr = avCurrentWindow.asError;
if (asErr != null) { if (asErr != null) {
_onError(asErr.error, asErr.stackTrace); _onError(asErr.error, asErr.stackTrace);
return false; _currentWindow = null;
return null;
} }
final asLoading = avOk.asLoading; final asLoading = avCurrentWindow.asLoading;
if (asLoading != null) { if (asLoading != null) {
// xxx: no need to block the cubit here for this _currentWindow = null;
// xxx: might want to switch to a 'busy' state though return null;
// 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 nextWindow = avCurrentWindow.asData!.value;
if (nextWindow == null || nextWindow.length == 0) {
_currentWindow = null;
return null;
}
// Handle out-of-range input position
// Doing this again because getWindow is allowed to return a smaller
// window than the one requested, possibly due to DHT consistency
// fluctuations and race conditions
if (clampInputPosition) {
_inputPosition = min(max(_inputPosition, nextWindow.firstPosition),
nextWindow.lastPosition);
} else if (_inputPosition < nextWindow.firstPosition ||
_inputPosition > nextWindow.lastPosition) {
return null;
}
return _currentWindow = nextWindow;
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
/// The author of this messages in the input source
final TypedKey _author; final TypedKey _author;
/// The input source we're pulling messages from
final AuthorInputSource _inputSource; final AuthorInputSource _inputSource;
final OutputPosition? _outputPosition;
/// What to call if an error happens
final void Function(Object, StackTrace?) _onError; final void Function(Object, StackTrace?) _onError;
/// The message integrity validator
final MessageIntegrity _messageIntegrity; final MessageIntegrity _messageIntegrity;
// The last message we've consumed /// The last message we reconciled/output
proto.Message? _lastMessage; proto.Message? _previousMessage;
// The current position in the input log that we are looking at
int _currentPosition;
// The current message we're looking at
proto.Message? _currentMessage;
// If we have reached the end
bool _isDone = false;
// Desired maximum window length /// The current message we're looking at
proto.Message? _currentMessage;
/// The current position in the input source that we are looking at
int _inputPosition;
/// The current input window from the InputSource;
InputWindow? _currentWindow;
/// Desired maximum window length
static const int _maxWindowLength = 256; static const int _maxWindowLength = 256;
} }

View file

@ -9,64 +9,68 @@ import '../../../proto/proto.dart' as proto;
@immutable @immutable
class InputWindow { class InputWindow {
const InputWindow( const InputWindow({required this.elements, required this.firstPosition})
{required this.elements, required this.first, required this.last}); : lastPosition = firstPosition + elements.length - 1,
isEmpty = elements.length == 0,
length = elements.length;
final IList<OnlineElementState<proto.Message>> elements; final IList<OnlineElementState<proto.Message>> elements;
final int first; final int firstPosition;
final int last; final int lastPosition;
final bool isEmpty;
final int length;
} }
class AuthorInputSource { class AuthorInputSource {
AuthorInputSource.fromCubit( AuthorInputSource.fromDHTLog(DHTLog dhtLog) : _dhtLog = dhtLog;
{required DHTLogStateData<proto.Message> cubitState,
required this.cubit}) {
_currentWindow = InputWindow(
elements: cubitState.window,
first: (cubitState.windowTail - cubitState.window.length) %
cubitState.length,
last: (cubitState.windowTail - 1) % cubitState.length);
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
InputWindow get currentWindow => _currentWindow; Future<int> getTailPosition() async =>
_dhtLog.operate((reader) async => reader.length);
Future<AsyncValue<bool>> updateWindow( Future<AsyncValue<InputWindow?>> getWindow(
int currentPosition, int windowLength) async => int startPosition, int windowLength) async =>
cubit.operate((reader) async { _dhtLog.operate((reader) async {
// See if we're beyond the input source // Don't allow negative length
if (currentPosition < 0 || currentPosition >= reader.length) { if (windowLength <= 0) {
return const AsyncValue.data(false); return const AsyncValue.data(null);
}
// 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);
} }
// Trim if we're beyond input source
var endPosition = startPosition + windowLength - 1;
startPosition = max(startPosition, 0);
endPosition = max(endPosition, 0);
// Get another input batch futher back // Get another input batch futher back
final nextWindow = await cubit.loadElementsFromReader( try {
reader, last + 1, (last + 1) - first); Set<int>? offlinePositions;
if (nextWindow == null) { if (_dhtLog.writer != null) {
offlinePositions = await reader.getOfflinePositions();
}
final messages = await reader.getRangeProtobuf(
proto.Message.fromBuffer, startPosition,
length: endPosition - startPosition + 1);
if (messages == null) {
return const AsyncValue.loading(); return const AsyncValue.loading();
} }
_currentWindow =
InputWindow(elements: nextWindow, first: first, last: last); final elements = messages.indexed
return const AsyncValue.data(true); .map((x) => OnlineElementState(
value: x.$2,
isOffline: offlinePositions?.contains(x.$1 + startPosition) ??
false))
.toIList();
final window =
InputWindow(elements: elements, firstPosition: startPosition);
return AsyncValue.data(window);
} on Exception catch (e, st) {
return AsyncValue.error(e, st);
}
}); });
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
final DHTLogCubit<proto.Message> cubit; final DHTLog _dhtLog;
late InputWindow _currentWindow;
} }

View file

@ -20,66 +20,113 @@ class MessageReconciliation {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
void reconcileMessages( void addInputSourceFromDHTLog(TypedKey author, DHTLog inputMessagesDHTLog) {
TypedKey author, _inputSources[author] = AuthorInputSource.fromDHTLog(inputMessagesDHTLog);
DHTLogStateData<proto.Message> inputMessagesCubitState,
DHTLogCubit<proto.Message> inputMessagesCubit) {
if (inputMessagesCubitState.window.isEmpty) {
return;
} }
_inputSources[author] = AuthorInputSource.fromCubit( void reconcileMessages(TypedKey? author) {
cubitState: inputMessagesCubitState, cubit: inputMessagesCubit); // xxx: can we use 'author' here to optimize _updateAuthorInputQueues?
singleFuture(this, onError: _onError, () async { singleFuture(this, onError: _onError, () async {
// Take entire list of input sources we have currently and process them // Update queues
final inputSources = _inputSources; final activeInputQueues = await _updateAuthorInputQueues();
_inputSources = {};
final inputFuts = <Future<AuthorInputQueue?>>[];
for (final kv in inputSources.entries) {
final author = kv.key;
final inputSource = kv.value;
inputFuts
.add(_enqueueAuthorInput(author: author, inputSource: inputSource));
}
final inputQueues = await inputFuts.wait;
// Make this safe to cast by removing inputs that were rejected or empty
inputQueues.removeNulls();
// Process all input queues together // Process all input queues together
await _outputCubit await _outputCubit
.operate((reconciledArray) async => _reconcileInputQueues( .operate((reconciledArray) async => _reconcileInputQueues(
reconciledArray: reconciledArray, reconciledArray: reconciledArray,
inputQueues: inputQueues.cast<AuthorInputQueue>(), activeInputQueues: activeInputQueues,
)); ));
}); });
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Set up a single author's message reconciliation // Prepare author input queues by removing dead ones and adding new ones
Future<AuthorInputQueue?> _enqueueAuthorInput( // Queues that are empty are not added until they have something in them
{required TypedKey author, // Active input queues with a current message are returned in a list
required AuthorInputSource inputSource}) async { Future<List<AuthorInputQueue>> _updateAuthorInputQueues() async {
// Remove any dead input queues
final deadQueues = <TypedKey>[];
for (final author in _inputQueues.keys) {
if (!_inputSources.containsKey(author)) {
deadQueues.add(author);
}
}
for (final author in deadQueues) {
_inputQueues.remove(author);
_outputPositions.remove(author);
}
await _outputCubit.operate((outputArray) async {
final dws = DelayedWaitSet<void, void>();
for (final kv in _inputSources.entries) {
final author = kv.key;
final inputSource = kv.value;
final iqExisting = _inputQueues[author];
if (iqExisting == null || iqExisting.inputSource != inputSource) {
dws.add((_) async {
try { try {
await _enqueueAuthorInput(
author: author,
inputSource: inputSource,
outputArray: outputArray);
// Catch everything so we can avoid ParallelWaitError
// ignore: avoid_catches_without_on_clauses
} catch (e, st) {
log.error('Exception updating author input queue: $e:\n$st\n');
_inputQueues.remove(author);
_outputPositions.remove(author);
}
});
}
}
await dws();
});
// Get the active input queues
final activeInputQueues = await _inputQueues.entries
.map((entry) async {
if (await entry.value.getCurrentMessage() != null) {
return entry.value;
} else {
return null;
}
})
.toList()
.wait
..removeNulls();
return activeInputQueues.cast<AuthorInputQueue>();
}
// Set up a single author's message reconciliation
Future<void> _enqueueAuthorInput(
{required TypedKey author,
required AuthorInputSource inputSource,
required TableDBArrayProtobuf<proto.ReconciledMessage>
outputArray}) 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 outputPosition = await _findLastOutputPosition(author: author); final outputPosition =
await _findLastOutputPosition(author: author, outputArray: outputArray);
// Find oldest message we have not yet reconciled // Find oldest message we have not yet reconciled
final inputQueue = await AuthorInputQueue.create( final inputQueue = await AuthorInputQueue.create(
author: author, author: author,
inputSource: inputSource, inputSource: inputSource,
outputPosition: outputPosition, previousMessage: outputPosition?.message.content,
onError: _onError, onError: _onError,
); );
return inputQueue;
// Catch everything so we can avoid ParallelWaitError if (inputQueue != null) {
// ignore: avoid_catches_without_on_clauses _inputQueues[author] = inputQueue;
} catch (e, st) { _outputPositions[author] = outputPosition;
log.error('Exception enqueing author input: $e:\n$st\n'); } else {
return null; _inputQueues.remove(author);
_outputPositions.remove(author);
} }
} }
@ -87,36 +134,38 @@ class MessageReconciliation {
// 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?> _findLastOutputPosition( Future<OutputPosition?> _findLastOutputPosition(
{required TypedKey author}) async => {required TypedKey author,
_outputCubit.operate((arr) async { required TableDBArrayProtobuf<proto.ReconciledMessage>
var pos = arr.length - 1; outputArray}) async {
var pos = outputArray.length - 1;
while (pos >= 0) { while (pos >= 0) {
final message = await arr.get(pos); final message = await outputArray.get(pos);
if (message.content.author.toVeilid() == author) { if (message.content.author.toVeilid() == author) {
return OutputPosition(message, pos); return OutputPosition(message, pos);
} }
pos--; pos--;
} }
return null; return null;
}); }
// 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({
required TableDBArrayProtobuf<proto.ReconciledMessage> reconciledArray, required TableDBArrayProtobuf<proto.ReconciledMessage> reconciledArray,
required List<AuthorInputQueue> inputQueues, required List<AuthorInputQueue> activeInputQueues,
}) async { }) async {
// Ensure queues all have something to do // Ensure we have active queues to process
inputQueues.removeWhere((q) => q.isDone); if (activeInputQueues.isEmpty) {
if (inputQueues.isEmpty) {
return; return;
} }
// 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) { activeInputQueues.sort((a, b) {
final acmp = a.outputPosition?.pos ?? -1; final aout = _outputPositions[a.author];
final bcmp = b.outputPosition?.pos ?? -1; final bout = _outputPositions[b.author];
final acmp = aout?.pos ?? -1;
final bcmp = bout?.pos ?? -1;
if (acmp == bcmp) { if (acmp == bcmp) {
return a.author.toString().compareTo(b.author.toString()); return a.author.toString().compareTo(b.author.toString());
} }
@ -124,21 +173,28 @@ 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
var currentOutputPosition = inputQueues.first.outputPosition; var currentOutputPosition =
_outputPositions[activeInputQueues.first.author];
final toInsert = final toInsert =
SortedList<proto.Message>(proto.MessageExt.compareTimestamp); SortedList<proto.Message>(proto.MessageExt.compareTimestamp);
while (inputQueues.isNotEmpty) { while (activeInputQueues.isNotEmpty) {
// Get up to '_maxReconcileChunk' of the items from the queues // Get up to '_maxReconcileChunk' of the items from the queues
// that we can insert at this location // that we can insert at this location
bool added; bool added;
do { do {
added = false; added = false;
var someQueueEmpty = false;
for (final inputQueue in inputQueues) { final emptyQueues = <AuthorInputQueue>{};
final inputCurrent = inputQueue.current!; for (final inputQueue in activeInputQueues) {
final inputCurrent = await inputQueue.getCurrentMessage();
if (inputCurrent == null) {
log.error('Active input queue did not have a current message: '
'${inputQueue.author}');
continue;
}
if (currentOutputPosition == null || if (currentOutputPosition == null ||
inputCurrent.timestamp < inputCurrent.timestamp <
currentOutputPosition.message.content.timestamp) { currentOutputPosition.message.content.timestamp) {
@ -146,16 +202,14 @@ class MessageReconciliation {
added = true; added = true;
// Advance this queue // Advance this queue
if (!await inputQueue.consume()) { if (!await inputQueue.advance()) {
// Queue is empty now, run a queue purge // Mark queue as empty for removal
someQueueEmpty = true; emptyQueues.add(inputQueue);
} }
} }
} }
// Remove empty queues now that we're done iterating // Remove finished queues now that we're done iterating
if (someQueueEmpty) { activeInputQueues.removeWhere(emptyQueues.contains);
inputQueues.removeWhere((q) => q.isDone);
}
if (toInsert.length >= _maxReconcileChunk) { if (toInsert.length >= _maxReconcileChunk) {
break; break;
@ -173,9 +227,27 @@ class MessageReconciliation {
..content = message) ..content = message)
.toList(); .toList();
await reconciledArray.insertAll( // Figure out where to insert the reconciled messages
currentOutputPosition?.pos ?? reconciledArray.length, final insertPos = currentOutputPosition?.pos ?? reconciledArray.length;
reconciledInserts);
// Insert them all at once
await reconciledArray.insertAll(insertPos, reconciledInserts);
// Update output positions for input queues
final updatePositions = _outputPositions.keys.toSet();
var outputPos = insertPos + reconciledInserts.length;
for (final inserted in reconciledInserts.reversed) {
if (updatePositions.isEmpty) {
// Last seen positions already recorded for each active author
break;
}
outputPos--;
final author = inserted.content.author.toVeilid();
if (updatePositions.contains(author)) {
_outputPositions[author] = OutputPosition(inserted, outputPos);
updatePositions.remove(author);
}
}
toInsert.clear(); toInsert.clear();
} else { } else {
@ -195,7 +267,9 @@ class MessageReconciliation {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
Map<TypedKey, AuthorInputSource> _inputSources = {}; final Map<TypedKey, AuthorInputSource> _inputSources = {};
final Map<TypedKey, AuthorInputQueue> _inputQueues = {};
final Map<TypedKey, OutputPosition?> _outputPositions = {};
final TableDBArrayProtobufCubit<proto.ReconciledMessage> _outputCubit; final TableDBArrayProtobufCubit<proto.ReconciledMessage> _outputCubit;
final void Function(Object, StackTrace?) _onError; final void Function(Object, StackTrace?) _onError;

View file

@ -77,8 +77,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
await _sentSubscription?.cancel(); await _sentSubscription?.cancel();
await _rcvdSubscription?.cancel(); await _rcvdSubscription?.cancel();
await _reconciledSubscription?.cancel(); await _reconciledSubscription?.cancel();
await _sentMessagesCubit?.close(); await _sentMessagesDHTLog?.close();
await _rcvdMessagesCubit?.close(); await _rcvdMessagesDHTLog?.close();
await _reconciledMessagesCubit?.close(); await _reconciledMessagesCubit?.close();
// If the local conversation record is gone, then delete the reconciled // If the local conversation record is gone, then delete the reconciled
@ -111,10 +111,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
await _initReconciledMessagesCubit(); await _initReconciledMessagesCubit();
// Local messages key // Local messages key
await _initSentMessagesCubit(); await _initSentMessagesDHTLog();
// Remote messages key // Remote messages key
await _initRcvdMessagesCubit(); await _initRcvdMessagesDHTLog();
// Command execution background process // Command execution background process
_commandRunnerFut = Future.delayed(Duration.zero, _commandRunner); _commandRunnerFut = Future.delayed(Duration.zero, _commandRunner);
@ -129,39 +129,40 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
} }
// Open local messages key // Open local messages key
Future<void> _initSentMessagesCubit() async { Future<void> _initSentMessagesDHTLog() async {
final writer = _accountInfo.identityWriter; final writer = _accountInfo.identityWriter;
_sentMessagesCubit = DHTLogCubit( final sentMessagesDHTLog =
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, await DHTLog.openWrite(_localMessagesRecordKey, writer,
debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::' debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::'
'SentMessages', 'SentMessages',
parent: _localConversationRecordKey, parent: _localConversationRecordKey,
crypto: _conversationCrypto), crypto: _conversationCrypto);
decodeElement: proto.Message.fromBuffer); _sentSubscription = await sentMessagesDHTLog.listen(_updateSentMessages);
_sentSubscription =
_sentMessagesCubit!.stream.listen(_updateSentMessagesState); _sentMessagesDHTLog = sentMessagesDHTLog;
_updateSentMessagesState(_sentMessagesCubit!.state); _reconciliation.addInputSourceFromDHTLog(
_accountInfo.identityTypedPublicKey, sentMessagesDHTLog);
} }
// Open remote messages key // Open remote messages key
Future<void> _initRcvdMessagesCubit() async { Future<void> _initRcvdMessagesDHTLog() async {
// Don't bother if we don't have a remote messages record key yet // Don't bother if we don't have a remote messages record key yet
if (_remoteMessagesRecordKey == null) { if (_remoteMessagesRecordKey == null) {
return; return;
} }
// Open new cubit if one is desired // Open new cubit if one is desired
_rcvdMessagesCubit = DHTLogCubit( final rcvdMessagesDHTLog = await DHTLog.openRead(_remoteMessagesRecordKey!,
open: () async => DHTLog.openRead(_remoteMessagesRecordKey!,
debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::'
'RcvdMessages', 'RcvdMessages',
parent: _remoteConversationRecordKey, parent: _remoteConversationRecordKey,
crypto: _conversationCrypto), crypto: _conversationCrypto);
decodeElement: proto.Message.fromBuffer); _rcvdSubscription = await rcvdMessagesDHTLog.listen(_updateRcvdMessages);
_rcvdSubscription =
_rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState); _rcvdMessagesDHTLog = rcvdMessagesDHTLog;
_updateRcvdMessagesState(_rcvdMessagesCubit!.state); _reconciliation.addInputSourceFromDHTLog(
_remoteIdentityPublicKey, rcvdMessagesDHTLog);
} }
Future<void> updateRemoteMessagesRecordKey( Future<void> updateRemoteMessagesRecordKey(
@ -175,17 +176,17 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
return; return;
} }
// Close existing cubit if we have one // Close existing DHTLog if we have one
final rcvdMessagesCubit = _rcvdMessagesCubit; final rcvdMessagesDHTLog = _rcvdMessagesDHTLog;
_rcvdMessagesCubit = null; _rcvdMessagesDHTLog = null;
_remoteMessagesRecordKey = null; _remoteMessagesRecordKey = null;
await _rcvdSubscription?.cancel(); await _rcvdSubscription?.cancel();
_rcvdSubscription = null; _rcvdSubscription = null;
await rcvdMessagesCubit?.close(); await rcvdMessagesDHTLog?.close();
// Init the new cubit if we should // Init the new DHTLog if we should
_remoteMessagesRecordKey = remoteMessagesRecordKey; _remoteMessagesRecordKey = remoteMessagesRecordKey;
await _initRcvdMessagesCubit(); await _initRcvdMessagesDHTLog();
}); });
} }
@ -275,30 +276,15 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Internal implementation // Internal implementation
// Called when the sent messages cubit gets a change // Called when the sent messages DHTLog gets a change
// This will re-render when messages are sent from another machine // This will re-render when messages are sent from another machine
void _updateSentMessagesState(DHTLogBusyState<proto.Message> avmessages) { void _updateSentMessages(DHTLogUpdate upd) {
final sentMessages = avmessages.state.asData?.value; _reconciliation.reconcileMessages(_accountInfo.identityTypedPublicKey);
if (sentMessages == null) {
return;
} }
_reconciliation.reconcileMessages( // Called when the received messages DHTLog gets a change
_accountInfo.identityTypedPublicKey, sentMessages, _sentMessagesCubit!); void _updateRcvdMessages(DHTLogUpdate upd) {
_reconciliation.reconcileMessages(_remoteIdentityPublicKey);
// Update the view
_renderState();
}
// Called when the received messages cubit gets a change
void _updateRcvdMessagesState(DHTLogBusyState<proto.Message> avmessages) {
final rcvdMessages = avmessages.state.asData?.value;
if (rcvdMessages == null) {
return;
}
_reconciliation.reconcileMessages(
_remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!);
} }
// Called when the reconciled messages window gets a change // Called when the reconciled messages window gets a change
@ -327,7 +313,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// _renderState(); // _renderState();
try { try {
await _sentMessagesCubit!.operateAppendEventual((writer) async { await _sentMessagesDHTLog!.operateAppendEventual((writer) async {
// Get the previous message if we have one // Get the previous message if we have one
var previousMessage = writer.length == 0 var previousMessage = writer.length == 0
? null ? null
@ -357,16 +343,17 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Produce a state for this cubit from the input cubits and queues // Produce a state for this cubit from the input cubits and queues
void _renderState() { void _renderState() {
// Get all reconciled messages // Get all reconciled messages in the cubit window
final reconciledMessages = final reconciledMessages =
_reconciledMessagesCubit?.state.state.asData?.value; _reconciledMessagesCubit?.state.state.asData?.value;
// Get all sent messages
final sentMessages = _sentMessagesCubit?.state.state.asData?.value; // Get all sent messages that are still offline
//final sentMessages = _sentMessagesDHTLog.
//Get all items in the unsent queue //Get all items in the unsent queue
//final unsentMessages = _unsentMessagesQueue.queue; //final unsentMessages = _unsentMessagesQueue.queue;
// If we aren't ready to render a state, say we're loading // If we aren't ready to render a state, say we're loading
if (reconciledMessages == null || sentMessages == null) { if (reconciledMessages == null) {
emit(const AsyncLoading()); emit(const AsyncLoading());
return; return;
} }
@ -377,11 +364,11 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// keyMapper: (x) => x.content.authorUniqueIdString, // keyMapper: (x) => x.content.authorUniqueIdString,
// values: reconciledMessages.windowElements, // values: reconciledMessages.windowElements,
// ); // );
final sentMessagesMap = // final sentMessagesMap =
IMap<String, OnlineElementState<proto.Message>>.fromValues( // IMap<String, OnlineElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.authorUniqueIdString, // keyMapper: (x) => x.value.authorUniqueIdString,
values: sentMessages.window, // values: sentMessages.window,
); // );
// final unsentMessagesMap = IMap<String, proto.Message>.fromValues( // final unsentMessagesMap = IMap<String, proto.Message>.fromValues(
// keyMapper: (x) => x.authorUniqueIdString, // keyMapper: (x) => x.authorUniqueIdString,
// values: unsentMessages, // values: unsentMessages,
@ -393,10 +380,12 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
final isLocal = final isLocal =
m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey; m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey;
final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime); final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime);
final sm = //final sm =
isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null; //isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null;
final sent = isLocal && sm != null; //final sent = isLocal && sm != null;
final sentOffline = isLocal && sm != null && sm.isOffline; //final sentOffline = isLocal && sm != null && sm.isOffline;
final sent = isLocal;
final sentOffline = false; //
renderedElements.add(RenderStateElement( renderedElements.add(RenderStateElement(
message: m.content, message: m.content,
@ -491,16 +480,16 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
late final VeilidCrypto _conversationCrypto; late final VeilidCrypto _conversationCrypto;
late final MessageIntegrity _senderMessageIntegrity; late final MessageIntegrity _senderMessageIntegrity;
DHTLogCubit<proto.Message>? _sentMessagesCubit; DHTLog? _sentMessagesDHTLog;
DHTLogCubit<proto.Message>? _rcvdMessagesCubit; DHTLog? _rcvdMessagesDHTLog;
TableDBArrayProtobufCubit<proto.ReconciledMessage>? _reconciledMessagesCubit; TableDBArrayProtobufCubit<proto.ReconciledMessage>? _reconciledMessagesCubit;
late final MessageReconciliation _reconciliation; late final MessageReconciliation _reconciliation;
late final PersistentQueue<proto.Message> _unsentMessagesQueue; late final PersistentQueue<proto.Message> _unsentMessagesQueue;
// IList<proto.Message> _sendingMessages = const IList.empty(); // IList<proto.Message> _sendingMessages = const IList.empty();
StreamSubscription<DHTLogBusyState<proto.Message>>? _sentSubscription; StreamSubscription<void>? _sentSubscription;
StreamSubscription<DHTLogBusyState<proto.Message>>? _rcvdSubscription; StreamSubscription<void>? _rcvdSubscription;
StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>? StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>?
_reconciledSubscription; _reconciledSubscription;
final StreamController<Future<void> Function()> _commandController; final StreamController<Future<void> Function()> _commandController;

View file

@ -213,7 +213,7 @@ class DHTLog implements DHTDeleteable<DHTLog> {
/// Runs a closure allowing read-only access to the log /// Runs a closure allowing read-only access to the log
Future<T> operate<T>(Future<T> Function(DHTLogReadOperations) closure) async { Future<T> operate<T>(Future<T> Function(DHTLogReadOperations) closure) async {
if (!isOpen) { if (!isOpen) {
throw StateError('log is not open"'); throw StateError('log is not open');
} }
return _spine.operate((spine) async { return _spine.operate((spine) async {
@ -230,7 +230,7 @@ class DHTLog implements DHTDeleteable<DHTLog> {
Future<T> operateAppend<T>( Future<T> operateAppend<T>(
Future<T> Function(DHTLogWriteOperations) closure) async { Future<T> Function(DHTLogWriteOperations) closure) async {
if (!isOpen) { if (!isOpen) {
throw StateError('log is not open"'); throw StateError('log is not open');
} }
return _spine.operateAppend((spine) async { return _spine.operateAppend((spine) async {
@ -249,7 +249,7 @@ class DHTLog implements DHTDeleteable<DHTLog> {
Future<T> Function(DHTLogWriteOperations) closure, Future<T> Function(DHTLogWriteOperations) closure,
{Duration? timeout}) async { {Duration? timeout}) async {
if (!isOpen) { if (!isOpen) {
throw StateError('log is not open"'); throw StateError('log is not open');
} }
return _spine.operateAppendEventual((spine) async { return _spine.operateAppendEventual((spine) async {
@ -264,7 +264,7 @@ class DHTLog implements DHTDeleteable<DHTLog> {
void Function(DHTLogUpdate) onChanged, void Function(DHTLogUpdate) onChanged,
) { ) {
if (!isOpen) { if (!isOpen) {
throw StateError('log is not open"'); throw StateError('log is not open');
} }
return _listenMutex.protect(() async { return _listenMutex.protect(() async {

View file

@ -112,33 +112,34 @@ 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 {
late final int length; late final int length;
final window = await _log.operate((reader) async { final windowElements = await _log.operate((reader) async {
length = reader.length; length = reader.length;
return loadElementsFromReader(reader, _windowTail, _windowSize); return _loadElementsFromReader(reader, _windowTail, _windowSize);
}); });
if (window == null) { if (windowElements == null) {
setWantsRefresh(); setWantsRefresh();
return; return;
} }
emit(AsyncValue.data(DHTLogStateData( emit(AsyncValue.data(DHTLogStateData(
length: length, length: length,
window: window, window: windowElements.$2,
windowTail: _windowTail, windowTail: windowElements.$1 + windowElements.$2.length,
windowSize: _windowSize, windowSize: windowElements.$2.length,
follow: _follow))); follow: _follow)));
setRefreshed(); setRefreshed();
} }
// Tail is one past the last element to load // Tail is one past the last element to load
Future<IList<OnlineElementState<T>>?> loadElementsFromReader( Future<(int, IList<OnlineElementState<T>>)?> _loadElementsFromReader(
DHTLogReadOperations reader, int tail, int count, DHTLogReadOperations reader, int tail, int count,
{bool forceRefresh = false}) async { {bool forceRefresh = false}) async {
final length = reader.length; final length = reader.length;
if (length == 0) {
return const IList.empty();
}
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;
if (length == 0) {
return (start, IList<OnlineElementState<T>>.empty());
}
// If this is writeable get the offline positions // If this is writeable get the offline positions
Set<int>? offlinePositions; Set<int>? offlinePositions;
@ -154,8 +155,11 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
value: _decodeElement(x.$2), value: _decodeElement(x.$2),
isOffline: offlinePositions?.contains(x.$1) ?? false)) isOffline: offlinePositions?.contains(x.$1) ?? false))
.toIList(); .toIList();
if (allItems == null) {
return null;
}
return allItems; return (start, allItems);
} }
void _update(DHTLogUpdate upd) { void _update(DHTLogUpdate upd) {

View file

@ -21,8 +21,14 @@ class _DHTLogRead implements DHTLogReadOperations {
return null; return null;
} }
return lookup.scope((sa) => return lookup.scope((sa) => sa.operate((read) async {
sa.operate((read) => read.get(lookup.pos, forceRefresh: forceRefresh))); if (lookup.pos >= read.length) {
veilidLoggy.error('DHTLog shortarray read @ ${lookup.pos}'
' >= length ${read.length}');
return null;
}
return read.get(lookup.pos, forceRefresh: forceRefresh);
}));
} }
(int, int) _clampStartLen(int start, int? len) { (int, int) _clampStartLen(int start, int? len) {
@ -49,7 +55,7 @@ class _DHTLogRead implements DHTLogReadOperations {
.slices(kMaxDHTConcurrency) .slices(kMaxDHTConcurrency)
.map((chunk) => chunk.map((pos) async { .map((chunk) => chunk.map((pos) async {
try { try {
return get(pos + start, forceRefresh: forceRefresh); return await get(pos + start, forceRefresh: forceRefresh);
// Need some way to debug ParallelWaitError // Need some way to debug ParallelWaitError
// ignore: avoid_catches_without_on_clauses // ignore: avoid_catches_without_on_clauses
} catch (e, st) { } catch (e, st) {
@ -59,36 +65,42 @@ class _DHTLogRead implements DHTLogReadOperations {
})); }));
for (final chunk in chunks) { for (final chunk in chunks) {
final elems = await chunk.wait; var elems = await chunk.wait;
// If any element was unavailable, return null // Return only the first contiguous range, anything else is garbage
if (elems.contains(null)) { // due to a representational error in the head or shortarray legnth
return null; final nullPos = elems.indexOf(null);
if (nullPos != -1) {
elems = elems.sublist(0, nullPos);
} }
out.addAll(elems.cast<Uint8List>()); out.addAll(elems.cast<Uint8List>());
if (nullPos != -1) {
break;
}
} }
return out; return out;
} }
@override @override
Future<Set<int>?> getOfflinePositions() async { Future<Set<int>> getOfflinePositions() async {
final positionOffline = <int>{}; final positionOffline = <int>{};
// Iterate positions backward from most recent // Iterate positions backward from most recent
for (var pos = _spine.length - 1; pos >= 0; pos--) { for (var pos = _spine.length - 1; pos >= 0; pos--) {
// Get position
final lookup = await _spine.lookupPosition(pos); final lookup = await _spine.lookupPosition(pos);
// If position doesn't exist then it definitely wasn't written to offline
if (lookup == null) { if (lookup == null) {
return null; continue;
} }
// Check each segment for offline positions // Check each segment for offline positions
var foundOffline = false; var foundOffline = false;
final success = await lookup.scope((sa) => sa.operate((read) async { await lookup.scope((sa) => sa.operate((read) async {
final segmentOffline = await read.getOfflinePositions(); final segmentOffline = await read.getOfflinePositions();
if (segmentOffline == null) {
return false;
}
// For each shortarray segment go through their segment positions // For each shortarray segment go through their segment positions
// in reverse order and see if they are offline // in reverse order and see if they are offline
@ -102,11 +114,7 @@ class _DHTLogRead implements DHTLogReadOperations {
foundOffline = true; foundOffline = true;
} }
} }
return true;
})); }));
if (!success) {
return null;
}
// If we found nothing offline in this segment then we can stop // If we found nothing offline in this segment then we can stop
if (!foundOffline) { if (!foundOffline) {
break; break;

View file

@ -107,9 +107,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
await write.clear(); await write.clear();
} else if (lookup.pos != write.length) { } else if (lookup.pos != write.length) {
// We should always be appending at the length // We should always be appending at the length
throw DHTExceptionInvalidData( await write.truncate(lookup.pos);
'_DHTLogWrite::add lookup.pos=${lookup.pos} '
'write.length=${write.length}');
} }
return write.add(value); return write.add(value);
})); }));

View file

@ -62,9 +62,6 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayCubitState<T>>
Set<int>? offlinePositions; Set<int>? offlinePositions;
if (_shortArray.writer != null) { if (_shortArray.writer != null) {
offlinePositions = await reader.getOfflinePositions(); offlinePositions = await reader.getOfflinePositions();
if (offlinePositions == null) {
return null;
}
} }
// Get the items // Get the items

View file

@ -62,7 +62,7 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations {
.slices(kMaxDHTConcurrency) .slices(kMaxDHTConcurrency)
.map((chunk) => chunk.map((pos) async { .map((chunk) => chunk.map((pos) async {
try { try {
return get(pos + start, forceRefresh: forceRefresh); return await get(pos + start, forceRefresh: forceRefresh);
// Need some way to debug ParallelWaitError // Need some way to debug ParallelWaitError
// ignore: avoid_catches_without_on_clauses // ignore: avoid_catches_without_on_clauses
} catch (e, st) { } catch (e, st) {
@ -72,13 +72,20 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations {
})); }));
for (final chunk in chunks) { for (final chunk in chunks) {
final elems = await chunk.wait; var elems = await chunk.wait;
// If any element was unavailable, return null // Return only the first contiguous range, anything else is garbage
if (elems.contains(null)) { // due to a representational error in the head or shortarray legnth
return null; final nullPos = elems.indexOf(null);
if (nullPos != -1) {
elems = elems.sublist(0, nullPos);
} }
out.addAll(elems.cast<Uint8List>()); out.addAll(elems.cast<Uint8List>());
if (nullPos != -1) {
break;
}
} }
return out; return out;

View file

@ -14,19 +14,22 @@ abstract class DHTRandomRead {
/// is specified, the network will always be checked for newer values /// is specified, the network will always be checked for newer values
/// rather than returning the existing locally stored copy of the elements. /// rather than returning the existing locally stored copy of the elements.
/// Throws an IndexError if the 'pos' is not within the length /// Throws an IndexError if the 'pos' is not within the length
/// of the container. /// of the container. May return null if the item is not available at this
/// time.
Future<Uint8List?> get(int pos, {bool forceRefresh = false}); Future<Uint8List?> get(int pos, {bool forceRefresh = false});
/// Return a list of a range of items in the DHTArray. If 'forceRefresh' /// Return a list of a range of items in the DHTArray. If 'forceRefresh'
/// is specified, the network will always be checked for newer values /// is specified, the network will always be checked for newer values
/// rather than returning the existing locally stored copy of the elements. /// rather than returning the existing locally stored copy of the elements.
/// Throws an IndexError if either 'start' or '(start+length)' is not within /// Throws an IndexError if either 'start' or '(start+length)' is not within
/// the length of the container. /// the length of the container. May return fewer items than the length
/// expected if the requested items are not available, but will always
/// return a contiguous range starting at 'start'.
Future<List<Uint8List>?> getRange(int start, Future<List<Uint8List>?> getRange(int start,
{int? length, bool forceRefresh = false}); {int? length, bool forceRefresh = false});
/// Get a list of the positions that were written offline and not flushed yet /// Get a list of the positions that were written offline and not flushed yet
Future<Set<int>?> getOfflinePositions(); Future<Set<int>> getOfflinePositions();
} }
extension DHTRandomReadExt on DHTRandomRead { extension DHTRandomReadExt on DHTRandomRead {