mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-07-21 05:38:42 -04:00
simplify reconciliation
This commit is contained in:
parent
bf38c2c44d
commit
4797184a1a
12 changed files with 512 additions and 345 deletions
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue