checkpoint

This commit is contained in:
Christien Rioux 2024-02-15 11:05:59 -07:00
parent 031d7aea82
commit fcccacfafa
8 changed files with 152 additions and 87 deletions

View File

@ -1,12 +1,28 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:equatable/equatable.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
import '../../../veilid_support.dart'; import '../../../veilid_support.dart';
@immutable
class DHTRecordWatchChange extends Equatable {
const DHTRecordWatchChange(
{required this.local, required this.data, required this.subkeys});
final bool local;
final Uint8List data;
final List<ValueSubkeyRange> subkeys;
@override
List<Object?> get props => [local, data, subkeys];
}
/////////////////////////////////////////////////
class DHTRecord { class DHTRecord {
DHTRecord( DHTRecord(
{required VeilidRoutingContext routingContext, {required VeilidRoutingContext routingContext,
@ -34,7 +50,7 @@ class DHTRecord {
bool _open; bool _open;
bool _valid; bool _valid;
@internal @internal
StreamController<VeilidUpdateValueChange>? watchController; StreamController<DHTRecordWatchChange>? watchController;
@internal @internal
bool needsWatchStateUpdate; bool needsWatchStateUpdate;
@internal @internal
@ -160,76 +176,100 @@ class DHTRecord {
Future<Uint8List?> tryWriteBytes(Uint8List newValue, Future<Uint8List?> tryWriteBytes(Uint8List newValue,
{int subkey = -1}) async { {int subkey = -1}) async {
subkey = subkeyOrDefault(subkey); subkey = subkeyOrDefault(subkey);
newValue = await _crypto.encrypt(newValue, subkey); final lastSeq = _subkeySeqCache[subkey];
final encryptedNewValue = await _crypto.encrypt(newValue, subkey);
// Set the new data if possible // Set the new data if possible
var valueData = await _routingContext.setDHTValue( var newValueData = await _routingContext.setDHTValue(
_recordDescriptor.key, subkey, newValue); _recordDescriptor.key, subkey, encryptedNewValue);
if (valueData == null) { if (newValueData == null) {
// Get the data to check its sequence number // A newer value wasn't found on the set, but
valueData = await _routingContext.getDHTValue( // we may get a newer value when getting the value for the sequence number
newValueData = await _routingContext.getDHTValue(
_recordDescriptor.key, subkey, false); _recordDescriptor.key, subkey, false);
assert(valueData != null, "can't get value that was just set"); if (newValueData == null) {
_subkeySeqCache[subkey] = valueData!.seq; assert(newValueData != null, "can't get value that was just set");
return null; return null;
} }
_subkeySeqCache[subkey] = valueData.seq; }
return valueData.data;
// Record new sequence number
final isUpdated = newValueData.seq != lastSeq;
_subkeySeqCache[subkey] = newValueData.seq;
// See if the encrypted data returned is exactly the same
// if so, shortcut and don't bother decrypting it
if (newValueData.data == encryptedNewValue) {
if (isUpdated) {
addLocalValueChange(newValue, subkey);
}
return null;
}
// Decrypt value to return it
final decryptedNewValue = await _crypto.decrypt(newValueData.data, subkey);
if (isUpdated) {
addLocalValueChange(decryptedNewValue, subkey);
}
return decryptedNewValue;
} }
Future<void> eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async { Future<void> eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async {
subkey = subkeyOrDefault(subkey); subkey = subkeyOrDefault(subkey);
newValue = await _crypto.encrypt(newValue, subkey); final lastSeq = _subkeySeqCache[subkey];
final encryptedNewValue = await _crypto.encrypt(newValue, subkey);
ValueData? valueData; ValueData? newValueData;
do {
do { do {
// Set the new data // Set the new data
valueData = await _routingContext.setDHTValue( newValueData = await _routingContext.setDHTValue(
_recordDescriptor.key, subkey, newValue); _recordDescriptor.key, subkey, encryptedNewValue);
// Repeat if newer data on the network was found // Repeat if newer data on the network was found
} while (valueData != null); } while (newValueData != null);
// Get the data to check its sequence number // Get the data to check its sequence number
valueData = newValueData = await _routingContext.getDHTValue(
await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false); _recordDescriptor.key, subkey, false);
assert(valueData != null, "can't get value that was just set"); if (newValueData == null) {
_subkeySeqCache[subkey] = valueData!.seq; assert(newValueData != null, "can't get value that was just set");
return;
}
// Record new sequence number
_subkeySeqCache[subkey] = newValueData.seq;
// The encrypted data returned should be exactly the same
// as what we are trying to set,
// otherwise we still need to keep trying to set the value
} while (newValueData.data != encryptedNewValue);
final isUpdated = newValueData.seq != lastSeq;
if (isUpdated) {
addLocalValueChange(newValue, subkey);
}
} }
Future<void> eventualUpdateBytes( Future<void> eventualUpdateBytes(
Future<Uint8List> Function(Uint8List oldValue) update, Future<Uint8List> Function(Uint8List? oldValue) update,
{int subkey = -1}) async { {int subkey = -1}) async {
subkey = subkeyOrDefault(subkey); subkey = subkeyOrDefault(subkey);
// Get existing identity key, do not allow force refresh here
// Get the existing data, do not allow force refresh here
// because if we need a refresh the setDHTValue will fail anyway // because if we need a refresh the setDHTValue will fail anyway
var valueData = var oldValue =
await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false); await get(subkey: subkey, forceRefresh: false, onlyUpdates: false);
// Ensure it exists already
if (valueData == null) {
throw const FormatException('value does not exist');
}
do { do {
// Update cache
_subkeySeqCache[subkey] = valueData!.seq;
// Update the data // Update the data
final oldData = await _crypto.decrypt(valueData.data, subkey); final updatedValue = await update(oldValue);
final updatedData = await update(oldData);
final newData = await _crypto.encrypt(updatedData, subkey);
// Set it back // Try to write it back to the network
valueData = await _routingContext.setDHTValue( oldValue = await tryWriteBytes(updatedValue, subkey: subkey);
_recordDescriptor.key, subkey, newData);
// Repeat if newer data on the network was found // Repeat update if newer data on the network was found
} while (valueData != null); } while (oldValue != null);
// Get the data to check its sequence number
valueData =
await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false);
assert(valueData != null, "can't get value that was just set");
_subkeySeqCache[subkey] = valueData!.seq;
} }
Future<T?> tryWriteJson<T>(T Function(dynamic) fromJson, T newValue, Future<T?> tryWriteJson<T>(T Function(dynamic) fromJson, T newValue,
@ -259,12 +299,12 @@ class DHTRecord {
eventualWriteBytes(newValue.writeToBuffer(), subkey: subkey); eventualWriteBytes(newValue.writeToBuffer(), subkey: subkey);
Future<void> eventualUpdateJson<T>( Future<void> eventualUpdateJson<T>(
T Function(dynamic) fromJson, Future<T> Function(T) update, T Function(dynamic) fromJson, Future<T> Function(T?) update,
{int subkey = -1}) => {int subkey = -1}) =>
eventualUpdateBytes(jsonUpdate(fromJson, update), subkey: subkey); eventualUpdateBytes(jsonUpdate(fromJson, update), subkey: subkey);
Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>( Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, Future<T> Function(T) update, T Function(List<int>) fromBuffer, Future<T> Function(T?) update,
{int subkey = -1}) => {int subkey = -1}) =>
eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey); eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey);
@ -281,25 +321,34 @@ class DHTRecord {
} }
} }
Future<StreamSubscription<VeilidUpdateValueChange>> listen( Future<StreamSubscription<DHTRecordWatchChange>> listen(
Future<void> Function( Future<void> Function(
DHTRecord record, Uint8List data, List<ValueSubkeyRange> subkeys) DHTRecord record, Uint8List data, List<ValueSubkeyRange> subkeys)
onUpdate, onUpdate,
) async { {bool localChanges = true}) async {
// Set up watch requirements // Set up watch requirements
watchController ??= watchController ??=
StreamController<VeilidUpdateValueChange>.broadcast(onCancel: () { StreamController<DHTRecordWatchChange>.broadcast(onCancel: () {
// If there are no more listeners then we can get rid of the controller // If there are no more listeners then we can get rid of the controller
watchController = null; watchController = null;
}); });
return watchController!.stream.listen( return watchController!.stream.listen(
(update) { (change) {
if (change.local && !localChanges) {
return;
}
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
final out = await _crypto.decrypt( final Uint8List data;
update.valueData.data, update.subkeys.first.low); if (change.local) {
// local changes are not encrypted
await onUpdate(this, out, update.subkeys); data = change.data;
} else {
// incoming/remote changes are encrypted
data =
await _crypto.decrypt(change.data, change.subkeys.first.low);
}
await onUpdate(this, data, change.subkeys);
}); });
}, },
cancelOnError: true, cancelOnError: true,
@ -316,4 +365,14 @@ class DHTRecord {
needsWatchStateUpdate = true; needsWatchStateUpdate = true;
} }
} }
void addLocalValueChange(Uint8List data, int subkey) {
watchController?.add(DHTRecordWatchChange(
local: true, data: data, subkeys: [ValueSubkeyRange.single(subkey)]));
}
void addRemoteValueChange(VeilidUpdateValueChange update) {
watchController?.add(DHTRecordWatchChange(
local: false, data: update.valueData.data, subkeys: update.subkeys));
}
} }

View File

@ -102,7 +102,7 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
DHTRecord get record => _record; DHTRecord get record => _record;
StreamSubscription<VeilidUpdateValueChange>? _subscription; StreamSubscription<DHTRecordWatchChange>? _subscription;
late DHTRecord _record; late DHTRecord _record;
bool _wantsCloseRecord; bool _wantsCloseRecord;
final StateFunction<T> _stateFunction; final StateFunction<T> _stateFunction;

View File

@ -351,7 +351,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
// Change // Change
for (final kv in _opened.entries) { for (final kv in _opened.entries) {
if (kv.key == updateValueChange.key) { if (kv.key == updateValueChange.key) {
kv.value.watchController?.add(updateValueChange); kv.value.addRemoteValueChange(updateValueChange);
break; break;
} }
} }

View File

@ -63,8 +63,7 @@ class DHTShortArray {
_DHTShortArrayCache _head; _DHTShortArrayCache _head;
// Subscription to head and linked record internal changes // Subscription to head and linked record internal changes
final Map<TypedKey, StreamSubscription<VeilidUpdateValueChange>> final Map<TypedKey, StreamSubscription<DHTRecordWatchChange>> _subscriptions;
_subscriptions;
// Stream of external changes // Stream of external changes
StreamController<void>? _watchController; StreamController<void>? _watchController;
// Watch mutex to ensure we keep the representation valid // Watch mutex to ensure we keep the representation valid
@ -545,10 +544,10 @@ class DHTShortArray {
} }
final result = await record!.get(subkey: recordSubkey); final result = await record!.get(subkey: recordSubkey);
if (result != null) {
// A change happened, notify any listeners // A change happened, notify any listeners
_watchController?.sink.add(null); _watchController?.sink.add(null);
}
return result; return result;
} on Exception catch (_) { } on Exception catch (_) {
// Exception on write means state needs to be reverted // Exception on write means state needs to be reverted
@ -607,8 +606,8 @@ class DHTShortArray {
final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0);
final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); final result = await record.tryWriteBytes(newValue, subkey: recordSubkey);
if (result != null) { if (result == null) {
// A change happened, notify any listeners // A newer value was not found, so the change took
_watchController?.sink.add(null); _watchController?.sink.add(null);
} }
return result; return result;
@ -625,7 +624,7 @@ class DHTShortArray {
} }
Future<void> eventualUpdateItem( Future<void> eventualUpdateItem(
int pos, Future<Uint8List> Function(Uint8List oldValue) update) async { int pos, Future<Uint8List> Function(Uint8List? oldValue) update) async {
var oldData = await getItem(pos); var oldData = await getItem(pos);
// Ensure it exists already // Ensure it exists already
if (oldData == null) { if (oldData == null) {
@ -633,7 +632,7 @@ class DHTShortArray {
} }
do { do {
// Update the data // Update the data
final updatedData = await update(oldData!); final updatedData = await update(oldData);
// Set it back // Set it back
oldData = await tryWriteItem(pos, updatedData); oldData = await tryWriteItem(pos, updatedData);
@ -673,14 +672,14 @@ class DHTShortArray {
Future<void> eventualUpdateItemJson<T>( Future<void> eventualUpdateItemJson<T>(
T Function(dynamic) fromJson, T Function(dynamic) fromJson,
int pos, int pos,
Future<T> Function(T) update, Future<T> Function(T?) update,
) => ) =>
eventualUpdateItem(pos, jsonUpdate(fromJson, update)); eventualUpdateItem(pos, jsonUpdate(fromJson, update));
Future<void> eventualUpdateItemProtobuf<T extends GeneratedMessage>( Future<void> eventualUpdateItemProtobuf<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, T Function(List<int>) fromBuffer,
int pos, int pos,
Future<T> Function(T) update, Future<T> Function(T?) update,
) => ) =>
eventualUpdateItem(pos, protobufUpdate(fromBuffer, update)); eventualUpdateItem(pos, protobufUpdate(fromBuffer, update));
@ -692,14 +691,17 @@ class DHTShortArray {
.wait; .wait;
// Update changes to the head record // Update changes to the head record
// Don't watch for local changes because this class already handles
// notifying listeners and knows when it makes local changes
if (!_subscriptions.containsKey(_headRecord.key)) { if (!_subscriptions.containsKey(_headRecord.key)) {
_subscriptions[_headRecord.key] = _subscriptions[_headRecord.key] =
await _headRecord.listen(_onUpdateRecord); await _headRecord.listen(localChanges: false, _onUpdateRecord);
} }
// Update changes to any linked records // Update changes to any linked records
for (final lr in _head.linkedRecords) { for (final lr in _head.linkedRecords) {
if (!_subscriptions.containsKey(lr.key)) { if (!_subscriptions.containsKey(lr.key)) {
_subscriptions[lr.key] = await lr.listen(_onUpdateRecord); _subscriptions[lr.key] =
await lr.listen(localChanges: false, _onUpdateRecord);
} }
} }
} on Exception { } on Exception {

View File

@ -13,14 +13,15 @@ Uint8List jsonEncodeBytes(Object? object,
utf8.encode(jsonEncode(object, toEncodable: toEncodable))); utf8.encode(jsonEncode(object, toEncodable: toEncodable)));
Future<Uint8List> jsonUpdateBytes<T>(T Function(dynamic) fromJson, Future<Uint8List> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
Uint8List oldBytes, Future<T> Function(T) update) async { Uint8List? oldBytes, Future<T> Function(T?) update) async {
final oldObj = fromJson(jsonDecode(utf8.decode(oldBytes))); final oldObj =
oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes)));
final newObj = await update(oldObj); final newObj = await update(oldObj);
return jsonEncodeBytes(newObj); return jsonEncodeBytes(newObj);
} }
Future<Uint8List> Function(Uint8List) jsonUpdate<T>( Future<Uint8List> Function(Uint8List?) jsonUpdate<T>(
T Function(dynamic) fromJson, Future<T> Function(T) update) => T Function(dynamic) fromJson, Future<T> Function(T?) update) =>
(oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update); (oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update);
T Function(Object?) genericFromJson<T>( T Function(Object?) genericFromJson<T>(

View File

@ -4,14 +4,14 @@ import 'package:protobuf/protobuf.dart';
Future<Uint8List> protobufUpdateBytes<T extends GeneratedMessage>( Future<Uint8List> protobufUpdateBytes<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, T Function(List<int>) fromBuffer,
Uint8List oldBytes, Uint8List? oldBytes,
Future<T> Function(T) update) async { Future<T> Function(T?) update) async {
final oldObj = fromBuffer(oldBytes); final oldObj = oldBytes == null ? null : fromBuffer(oldBytes);
final newObj = await update(oldObj); final newObj = await update(oldObj);
return Uint8List.fromList(newObj.writeToBuffer()); return Uint8List.fromList(newObj.writeToBuffer());
} }
Future<Uint8List> Function(Uint8List) Future<Uint8List> Function(Uint8List?)
protobufUpdate<T extends GeneratedMessage>( protobufUpdate<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, Future<T> Function(T) update) => T Function(List<int>) fromBuffer, Future<T> Function(T?) update) =>
(oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update); (oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update);

View File

@ -411,7 +411,7 @@ packages:
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
mutex: mutex:
dependency: transitive dependency: "direct main"
description: description:
path: "../mutex" path: "../mutex"
relative: true relative: true

View File

@ -16,6 +16,9 @@ dependencies:
json_annotation: ^4.8.1 json_annotation: ^4.8.1
loggy: ^2.0.3 loggy: ^2.0.3
meta: ^1.10.0 meta: ^1.10.0
mutex:
path: ../mutex
protobuf: ^3.0.0 protobuf: ^3.0.0
veilid: veilid:
# veilid: ^0.0.1 # veilid: ^0.0.1