diff --git a/CHANGELOG.md b/CHANGELOG.md index 83530aae..82425b97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ **UNRELEASED** +- _BREAKING API CHANGES_: + - set_dht_value now accepts a new flag called `allow_offline`, which defaults to `true`. + - The previous `writer: Option` argument position is now `options: Option` + - This will only be a breaking change for anyone utilizing the previous `writer` argument. + - `writer` is now a member of `SetDHTValueOptions`, alongside the new `allow_offline` property. + - veilid-core: - Add private route example - Add `require_inbound_relay` option in VeilidConfig. Default is false, but if enabled, forces OutboundOnly/InboundRelay mode. Can be used as an extra layer of IP address obscurity for some threat models. (@neequ57) diff --git a/veilid-core/src/storage_manager/get_value.rs b/veilid-core/src/storage_manager/get_value.rs index 4053e08c..6e0362c6 100644 --- a/veilid-core/src/storage_manager/get_value.rs +++ b/veilid-core/src/storage_manager/get_value.rs @@ -145,11 +145,11 @@ impl StorageManager { }; // Validate with schema - if !schema.check_subkey_value_data( + if schema.check_subkey_value_data( descriptor.owner(), subkey, value.value_data(), - ) { + ).is_err() { // Validation failed, ignore this value // Move to the next node return Ok(FanoutCallOutput{peer_info_list: vec![], disposition: FanoutCallDisposition::Invalid}); diff --git a/veilid-core/src/storage_manager/mod.rs b/veilid-core/src/storage_manager/mod.rs index de415372..2604504b 100644 --- a/veilid-core/src/storage_manager/mod.rs +++ b/veilid-core/src/storage_manager/mod.rs @@ -727,7 +727,7 @@ impl StorageManager { record_key: TypedRecordKey, subkey: ValueSubkey, data: Vec, - writer: Option, + options: Option, ) -> VeilidAPIResult> { let mut inner = self.inner.lock().await; @@ -748,7 +748,11 @@ impl StorageManager { }; // Use the specified writer, or if not specified, the default writer when the record was opened - let opt_writer = writer.or(opt_writer); + let opt_writer = options.as_ref().and_then(|o| o.writer).or(opt_writer); + let allow_offline = options + .unwrap_or_default() + .allow_offline + .unwrap_or_default(); // If we don't have a writer then we can't write let Some(writer) = opt_writer else { @@ -781,9 +785,13 @@ impl StorageManager { }; // Validate with schema - if !schema.check_subkey_value_data(descriptor.owner(), subkey, &value_data) { + if let Err(e) = schema.check_subkey_value_data(descriptor.owner(), subkey, &value_data) { + veilid_log!(self debug "schema validation error: {}", e); // Validation failed, ignore this value - apibail_generic!("failed schema validation"); + apibail_generic!(format!( + "failed schema validation: {}:{}", + record_key, subkey + )); } // Sign the new value data with the writer @@ -821,10 +829,19 @@ impl StorageManager { }; if already_writing || !self.dht_is_online() { - veilid_log!(self debug "Writing subkey offline: {}:{} len={}", record_key, subkey, signed_value_data.value_data().data().len() ); - // Add to offline writes to flush - Self::add_offline_subkey_write_inner(&mut inner, record_key, subkey, safety_selection); - return Ok(None); + if allow_offline == AllowOffline(true) { + veilid_log!(self debug "Writing subkey offline: {}:{} len={}", record_key, subkey, signed_value_data.value_data().data().len() ); + // Add to offline writes to flush + Self::add_offline_subkey_write_inner( + &mut inner, + record_key, + subkey, + safety_selection, + ); + return Ok(None); + } else { + apibail_try_again!("offline, try again later"); + } }; // Drop the lock for network access @@ -847,12 +864,17 @@ impl StorageManager { Err(e) => { // Failed to write, try again later let mut inner = self.inner.lock().await; - Self::add_offline_subkey_write_inner( - &mut inner, - record_key, - subkey, - safety_selection, - ); + + if allow_offline == AllowOffline(true) { + Self::add_offline_subkey_write_inner( + &mut inner, + record_key, + subkey, + safety_selection, + ); + } else { + apibail_try_again!("offline, try again later"); + } // Remove from active subkey writes let asw = inner.active_subkey_writes.get_mut(&record_key).unwrap(); diff --git a/veilid-core/src/storage_manager/set_value.rs b/veilid-core/src/storage_manager/set_value.rs index 50ae4490..c8b43368 100644 --- a/veilid-core/src/storage_manager/set_value.rs +++ b/veilid-core/src/storage_manager/set_value.rs @@ -142,11 +142,11 @@ impl StorageManager { veilid_log!(registry debug "SetValue got value back: len={} seq={}", value.value_data().data().len(), value.value_data().seq()); // Validate with schema - if !ctx.schema.check_subkey_value_data( + if ctx.schema.check_subkey_value_data( descriptor.owner(), subkey, value.value_data(), - ) { + ).is_err() { // Validation failed, ignore this value and pretend we never saw this node return Ok(FanoutCallOutput{peer_info_list: vec![], disposition: FanoutCallDisposition::Invalid}); } @@ -474,7 +474,10 @@ impl StorageManager { }; // Validate new value with schema - if !schema.check_subkey_value_data(actual_descriptor.owner(), subkey, value.value_data()) { + if schema + .check_subkey_value_data(actual_descriptor.owner(), subkey, value.value_data()) + .is_err() + { // Validation failed, ignore this value return Ok(NetworkResult::invalid_message("failed schema validation")); } diff --git a/veilid-core/src/storage_manager/watch_value.rs b/veilid-core/src/storage_manager/watch_value.rs index 47d62a99..3ca9c6cc 100644 --- a/veilid-core/src/storage_manager/watch_value.rs +++ b/veilid-core/src/storage_manager/watch_value.rs @@ -1179,11 +1179,10 @@ impl StorageManager { let schema = descriptor.schema()?; // Validate with schema - if !schema.check_subkey_value_data( - descriptor.owner(), - first_subkey, - value.value_data(), - ) { + if schema + .check_subkey_value_data(descriptor.owner(), first_subkey, value.value_data()) + .is_err() + { // Validation failed, ignore this value // Move to the next node return Ok(NetworkResult::invalid_message(format!( diff --git a/veilid-core/src/tests/common/test_dht.rs b/veilid-core/src/tests/common/test_dht.rs index a3614bba..a76e40d3 100644 --- a/veilid-core/src/tests/common/test_dht.rs +++ b/veilid-core/src/tests/common/test_dht.rs @@ -319,7 +319,15 @@ pub async fn test_open_writer_dht_value(api: VeilidAPI) { // Verify subkey 0 can be set because we have overridden with the correct writer let set_dht_test_value_0_result = rc - .set_dht_value(key, 0, test_value_1.clone(), Some(keypair)) + .set_dht_value( + key, + 0, + test_value_1.clone(), + Some(SetDHTValueOptions { + writer: Some(keypair), + allow_offline: None, + }), + ) .await; assert!(set_dht_test_value_0_result.is_ok()); @@ -327,6 +335,69 @@ pub async fn test_open_writer_dht_value(api: VeilidAPI) { rc.delete_dht_record(key).await.unwrap(); } +pub async fn test_set_dht_value_allow_offline(api: VeilidAPI) { + let rc = api.routing_context().unwrap(); + + // Create a DHT record + let rec = rc + .create_dht_record(DHTSchema::dflt(1).unwrap(), None, Some(CRYPTO_KIND_VLD0)) + .await + .unwrap(); + let dht_key = *rec.key(); + + let test_value = String::from("Test offline value").as_bytes().to_vec(); + + // Test 1: Default behavior (options = None) should allow offline writes + let set_result = rc.set_dht_value(dht_key, 0, test_value.clone(), None).await; + assert!(set_result.is_ok()); + + // Test 2: Default behavior (allow_offline = None) should allow offline writes + let set_result = rc + .set_dht_value( + dht_key, + 0, + test_value.clone(), + Some(SetDHTValueOptions { + writer: None, + allow_offline: None, + }), + ) + .await; + assert!(set_result.is_ok()); + + // Test 3: Explicitly allow offline writes + let set_result = rc + .set_dht_value( + dht_key, + 1, + test_value.clone(), + Some(SetDHTValueOptions { + writer: None, + allow_offline: Some(AllowOffline(true)), + }), + ) + .await; + assert!(set_result.is_ok()); + + // Test 4: Disallow offline writes + let set_result = rc + .set_dht_value( + dht_key, + 2, + test_value.clone(), + Some(SetDHTValueOptions { + writer: None, + allow_offline: Some(AllowOffline(false)), + }), + ) + .await; + assert!(set_result.is_err()); + assert!(set_result.unwrap_err().to_string().contains("offline")); + + rc.close_dht_record(dht_key).await.unwrap(); + rc.delete_dht_record(dht_key).await.unwrap(); +} + // Network-related code to make sure veilid node is connetected to other peers async fn wait_for_public_internet_ready(api: &VeilidAPI) { @@ -364,6 +435,7 @@ pub async fn test_all() { test_create_dht_record_with_owner(api.clone()).await; test_set_get_dht_value(api.clone()).await; test_open_writer_dht_value(api.clone()).await; + test_set_dht_value_allow_offline(api.clone()).await; api.shutdown().await; } diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index bb488f31..fe015171 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -1669,7 +1669,15 @@ impl VeilidAPI { // Do a record set let value = match rc - .set_dht_value(key, subkey as ValueSubkey, data, writer) + .set_dht_value( + key, + subkey as ValueSubkey, + data, + Some(SetDHTValueOptions { + writer, + allow_offline: None, + }), + ) .await { Err(e) => { diff --git a/veilid-core/src/veilid_api/routing_context.rs b/veilid-core/src/veilid_api/routing_context.rs index f91a4caa..9d49b4bd 100644 --- a/veilid-core/src/veilid_api/routing_context.rs +++ b/veilid-core/src/veilid_api/routing_context.rs @@ -435,15 +435,15 @@ impl RoutingContext { key: TypedRecordKey, subkey: ValueSubkey, data: Vec, - writer: Option, + options: Option, ) -> VeilidAPIResult> { veilid_log!(self debug - "RoutingContext::set_dht_value(self: {:?}, key: {:?}, subkey: {:?}, data: len={}, writer: {:?})", self, key, subkey, data.len(), writer); + "RoutingContext::set_dht_value(self: {:?}, key: {:?}, subkey: {:?}, data: len={}, options: {:?})", self, key, subkey, data.len(), options); Crypto::validate_crypto_kind(key.kind)?; let storage_manager = self.api.core_context()?.storage_manager(); - Box::pin(storage_manager.set_value(key, subkey, data, writer)).await + Box::pin(storage_manager.set_value(key, subkey, data, options)).await } /// Add or update a watch to a DHT value that informs the user via an VeilidUpdate::ValueChange callback when the record has subkeys change. diff --git a/veilid-core/src/veilid_api/types/dht/mod.rs b/veilid-core/src/veilid_api/types/dht/mod.rs index b006a78c..a827e6d1 100644 --- a/veilid-core/src/veilid_api/types/dht/mod.rs +++ b/veilid-core/src/veilid_api/types/dht/mod.rs @@ -1,6 +1,7 @@ mod dht_record_descriptor; mod dht_record_report; mod schema; +mod set_dht_value_options; mod value_data; mod value_subkey_range_set; @@ -9,6 +10,7 @@ use super::*; pub use dht_record_descriptor::*; pub use dht_record_report::*; pub use schema::*; +pub use set_dht_value_options::*; pub use value_data::*; pub use value_subkey_range_set::*; diff --git a/veilid-core/src/veilid_api/types/dht/schema/dflt.rs b/veilid-core/src/veilid_api/types/dht/schema/dflt.rs index c3034ea7..7b3099cb 100644 --- a/veilid-core/src/veilid_api/types/dht/schema/dflt.rs +++ b/veilid-core/src/veilid_api/types/dht/schema/dflt.rs @@ -62,13 +62,12 @@ impl DHTSchemaDFLT { } /// Check a subkey value data against the schema - #[must_use] pub fn check_subkey_value_data( &self, owner: &PublicKey, subkey: ValueSubkey, value_data: &ValueData, - ) -> bool { + ) -> VeilidAPIResult<()> { let subkey = subkey as usize; // Check if subkey is in owner range @@ -80,19 +79,27 @@ impl DHTSchemaDFLT { // Ensure value size is within additional limit if value_data.data_size() <= max_value_len { - return true; + return Ok(()); } // Value too big - return false; + apibail_invalid_argument!( + "value too big", + "data", + format!("{:?}", value_data.data()) + ); } // Wrong writer - return false; + apibail_invalid_argument!( + "wrong writer", + "writer", + format!("{:?}", value_data.writer()) + ); } // Subkey out of range - false + apibail_invalid_argument!("subkey out of range", "subkey", subkey); } /// Check if a key is a schema member diff --git a/veilid-core/src/veilid_api/types/dht/schema/mod.rs b/veilid-core/src/veilid_api/types/dht/schema/mod.rs index 19bc613d..455eda40 100644 --- a/veilid-core/src/veilid_api/types/dht/schema/mod.rs +++ b/veilid-core/src/veilid_api/types/dht/schema/mod.rs @@ -64,13 +64,12 @@ impl DHTSchema { } /// Check a subkey value data against the schema - #[must_use] pub fn check_subkey_value_data( &self, owner: &PublicKey, subkey: ValueSubkey, value_data: &ValueData, - ) -> bool { + ) -> VeilidAPIResult<()> { match self { DHTSchema::DFLT(d) => d.check_subkey_value_data(owner, subkey, value_data), DHTSchema::SMPL(s) => s.check_subkey_value_data(owner, subkey, value_data), diff --git a/veilid-core/src/veilid_api/types/dht/schema/smpl.rs b/veilid-core/src/veilid_api/types/dht/schema/smpl.rs index 3a0c6b17..705c8179 100644 --- a/veilid-core/src/veilid_api/types/dht/schema/smpl.rs +++ b/veilid-core/src/veilid_api/types/dht/schema/smpl.rs @@ -107,13 +107,12 @@ impl DHTSchemaSMPL { } /// Check a subkey value data against the schema - #[must_use] pub fn check_subkey_value_data( &self, owner: &PublicKey, subkey: ValueSubkey, value_data: &ValueData, - ) -> bool { + ) -> VeilidAPIResult<()> { let mut cur_subkey = subkey as usize; let max_value_len = usize::min( @@ -127,14 +126,22 @@ impl DHTSchemaSMPL { if value_data.writer() == owner { // Ensure value size is within additional limit if value_data.data_size() <= max_value_len { - return true; + return Ok(()); } // Value too big - return false; + apibail_invalid_argument!( + "value too big", + "data", + format!("{:?}", value_data.data()) + ); } // Wrong writer - return false; + apibail_invalid_argument!( + "wrong writer", + "writer", + format!("{:?}", value_data.writer()) + ); } cur_subkey -= self.o_cnt as usize; @@ -146,20 +153,28 @@ impl DHTSchemaSMPL { if value_data.writer() == &m.m_key { // Ensure value size is in allowed range if value_data.data_size() <= max_value_len { - return true; + return Ok(()); } // Value too big - return false; + apibail_invalid_argument!( + "value too big", + "data", + format!("{:?}", value_data.data()) + ); } // Wrong writer - return false; + apibail_invalid_argument!( + "wrong writer", + "writer", + format!("{:?}", value_data.writer()) + ); } cur_subkey -= m.m_cnt as usize; } // Subkey out of range - false + apibail_invalid_argument!("subkey out of range", "subkey", subkey); } /// Check if a key is a schema member diff --git a/veilid-core/src/veilid_api/types/dht/set_dht_value_options.rs b/veilid-core/src/veilid_api/types/dht/set_dht_value_options.rs new file mode 100644 index 00000000..35794d53 --- /dev/null +++ b/veilid-core/src/veilid_api/types/dht/set_dht_value_options.rs @@ -0,0 +1,40 @@ +use crate::{Deserialize, JsonSchema, KeyPair, Serialize}; + +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +use crate::Tsify; + +#[derive(Debug, JsonSchema, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[cfg_attr( + all(target_arch = "wasm32", target_os = "unknown"), + derive(Tsify), + tsify(from_wasm_abi, into_wasm_abi) +)] +pub struct AllowOffline(pub bool); +impl Default for AllowOffline { + fn default() -> Self { + Self(true) + } +} + +#[derive(Debug, JsonSchema, Serialize, Deserialize, Clone)] +#[cfg_attr( + all(target_arch = "wasm32", target_os = "unknown"), + derive(Tsify), + tsify(from_wasm_abi, into_wasm_abi) +)] +pub struct SetDHTValueOptions { + #[schemars(with = "Option")] + pub writer: Option, + /// Defaults to true. If false, the value will not be written if the node is offline, + /// and a TryAgain error will be returned. + pub allow_offline: Option, +} + +impl Default for SetDHTValueOptions { + fn default() -> Self { + Self { + writer: None, + allow_offline: Some(AllowOffline(true)), + } + } +} diff --git a/veilid-flutter/example/integration_test/test_dht.dart b/veilid-flutter/example/integration_test/test_dht.dart index b613f209..180eafb8 100644 --- a/veilid-flutter/example/integration_test/test_dht.dart +++ b/veilid-flutter/example/integration_test/test_dht.dart @@ -244,7 +244,8 @@ Future testOpenWriterDHTValue() async { // Should have prior sequence number as its returned value because it // exists online at seq 0 vdtemp = await rc.setDHTValue(key, 0, va, - writer: KeyPair(key: owner, secret: secret)); + options: + SetDHTValueOptions(writer: KeyPair(key: owner, secret: secret))); expect(vdtemp, isNotNull); expect(vdtemp!.data, equals(vb)); expect(vdtemp.seq, equals(0)); @@ -252,7 +253,8 @@ Future testOpenWriterDHTValue() async { // Should update the second time to seq 1 vdtemp = await rc.setDHTValue(key, 0, va, - writer: KeyPair(key: owner, secret: secret)); + options: + SetDHTValueOptions(writer: KeyPair(key: owner, secret: secret))); expect(vdtemp, isNull); // Clean up diff --git a/veilid-flutter/example/pubspec.lock b/veilid-flutter/example/pubspec.lock index a24600d5..621ca6d0 100644 --- a/veilid-flutter/example/pubspec.lock +++ b/veilid-flutter/example/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" async_tools: dependency: transitive description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -195,10 +195,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -450,7 +450,7 @@ packages: path: ".." relative: true source: path - version: "0.4.4" + version: "0.4.7" veilid_test: dependency: "direct dev" description: @@ -462,18 +462,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" xdg_directories: dependency: transitive description: diff --git a/veilid-flutter/lib/routing_context.dart b/veilid-flutter/lib/routing_context.dart index c9eeed93..5fc124ed 100644 --- a/veilid-flutter/lib/routing_context.dart +++ b/veilid-flutter/lib/routing_context.dart @@ -273,6 +273,25 @@ enum DHTReportScope { String toJson() => name.toPascalCase(); } +/// SetDHTValueOptions + +@freezed +sealed class SetDHTValueOptions with _$SetDHTValueOptions { + const factory SetDHTValueOptions({ + KeyPair? writer, + bool? allowOffline, + }) = _SetDHTValueOptions; + + factory SetDHTValueOptions.fromJson(dynamic json) => + _$SetDHTValueOptionsFromJson(json as Map); + + @override + Map toJson() => { + 'writer': writer, + 'allow_offline': allowOffline, + }; +} + ////////////////////////////////////// /// VeilidRoutingContext @@ -300,7 +319,7 @@ abstract class VeilidRoutingContext { Future getDHTValue(TypedKey key, int subkey, {bool forceRefresh = false}); Future setDHTValue(TypedKey key, int subkey, Uint8List data, - {KeyPair? writer}); + {SetDHTValueOptions? options}); Future watchDHTValues(TypedKey key, {List? subkeys, Timestamp? expiration, int? count}); Future cancelDHTWatch(TypedKey key, {List? subkeys}); diff --git a/veilid-flutter/lib/routing_context.freezed.dart b/veilid-flutter/lib/routing_context.freezed.dart index 210f4f07..d3564bc5 100644 --- a/veilid-flutter/lib/routing_context.freezed.dart +++ b/veilid-flutter/lib/routing_context.freezed.dart @@ -1453,4 +1453,165 @@ class __$DHTRecordReportCopyWithImpl<$Res> } } +/// @nodoc +mixin _$SetDHTValueOptions { + KeyPair? get writer; + bool? get allowOffline; + + /// Create a copy of SetDHTValueOptions + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $SetDHTValueOptionsCopyWith get copyWith => + _$SetDHTValueOptionsCopyWithImpl( + this as SetDHTValueOptions, _$identity); + + /// Serializes this SetDHTValueOptions to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is SetDHTValueOptions && + (identical(other.writer, writer) || other.writer == writer) && + (identical(other.allowOffline, allowOffline) || + other.allowOffline == allowOffline)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, writer, allowOffline); + + @override + String toString() { + return 'SetDHTValueOptions(writer: $writer, allowOffline: $allowOffline)'; + } +} + +/// @nodoc +abstract mixin class $SetDHTValueOptionsCopyWith<$Res> { + factory $SetDHTValueOptionsCopyWith( + SetDHTValueOptions value, $Res Function(SetDHTValueOptions) _then) = + _$SetDHTValueOptionsCopyWithImpl; + @useResult + $Res call({KeyPair? writer, bool? allowOffline}); +} + +/// @nodoc +class _$SetDHTValueOptionsCopyWithImpl<$Res> + implements $SetDHTValueOptionsCopyWith<$Res> { + _$SetDHTValueOptionsCopyWithImpl(this._self, this._then); + + final SetDHTValueOptions _self; + final $Res Function(SetDHTValueOptions) _then; + + /// Create a copy of SetDHTValueOptions + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? writer = freezed, + Object? allowOffline = freezed, + }) { + return _then(_self.copyWith( + writer: freezed == writer + ? _self.writer + : writer // ignore: cast_nullable_to_non_nullable + as KeyPair?, + allowOffline: freezed == allowOffline + ? _self.allowOffline + : allowOffline // ignore: cast_nullable_to_non_nullable + as bool?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _SetDHTValueOptions implements SetDHTValueOptions { + const _SetDHTValueOptions({this.writer, this.allowOffline}); + factory _SetDHTValueOptions.fromJson(Map json) => + _$SetDHTValueOptionsFromJson(json); + + @override + final KeyPair? writer; + @override + final bool? allowOffline; + + /// Create a copy of SetDHTValueOptions + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$SetDHTValueOptionsCopyWith<_SetDHTValueOptions> get copyWith => + __$SetDHTValueOptionsCopyWithImpl<_SetDHTValueOptions>(this, _$identity); + + @override + Map toJson() { + return _$SetDHTValueOptionsToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _SetDHTValueOptions && + (identical(other.writer, writer) || other.writer == writer) && + (identical(other.allowOffline, allowOffline) || + other.allowOffline == allowOffline)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, writer, allowOffline); + + @override + String toString() { + return 'SetDHTValueOptions(writer: $writer, allowOffline: $allowOffline)'; + } +} + +/// @nodoc +abstract mixin class _$SetDHTValueOptionsCopyWith<$Res> + implements $SetDHTValueOptionsCopyWith<$Res> { + factory _$SetDHTValueOptionsCopyWith( + _SetDHTValueOptions value, $Res Function(_SetDHTValueOptions) _then) = + __$SetDHTValueOptionsCopyWithImpl; + @override + @useResult + $Res call({KeyPair? writer, bool? allowOffline}); +} + +/// @nodoc +class __$SetDHTValueOptionsCopyWithImpl<$Res> + implements _$SetDHTValueOptionsCopyWith<$Res> { + __$SetDHTValueOptionsCopyWithImpl(this._self, this._then); + + final _SetDHTValueOptions _self; + final $Res Function(_SetDHTValueOptions) _then; + + /// Create a copy of SetDHTValueOptions + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? writer = freezed, + Object? allowOffline = freezed, + }) { + return _then(_SetDHTValueOptions( + writer: freezed == writer + ? _self.writer + : writer // ignore: cast_nullable_to_non_nullable + as KeyPair?, + allowOffline: freezed == allowOffline + ? _self.allowOffline + : allowOffline // ignore: cast_nullable_to_non_nullable + as bool?, + )); + } +} + // dart format on diff --git a/veilid-flutter/lib/routing_context.g.dart b/veilid-flutter/lib/routing_context.g.dart index e9d081e8..5544c656 100644 --- a/veilid-flutter/lib/routing_context.g.dart +++ b/veilid-flutter/lib/routing_context.g.dart @@ -128,3 +128,15 @@ Map _$DHTRecordReportToJson(_DHTRecordReport instance) => 'local_seqs': instance.localSeqs, 'network_seqs': instance.networkSeqs, }; + +_SetDHTValueOptions _$SetDHTValueOptionsFromJson(Map json) => + _SetDHTValueOptions( + writer: json['writer'] == null ? null : KeyPair.fromJson(json['writer']), + allowOffline: json['allow_offline'] as bool?, + ); + +Map _$SetDHTValueOptionsToJson(_SetDHTValueOptions instance) => + { + 'writer': instance.writer?.toJson(), + 'allow_offline': instance.allowOffline, + }; diff --git a/veilid-flutter/lib/veilid_config.dart b/veilid-flutter/lib/veilid_config.dart index b91502d4..92df714c 100644 --- a/veilid-flutter/lib/veilid_config.dart +++ b/veilid-flutter/lib/veilid_config.dart @@ -266,6 +266,18 @@ sealed class VeilidConfigProtocol with _$VeilidConfigProtocol { //////////// +@freezed +sealed class VeilidConfigPrivacy with _$VeilidConfigPrivacy { + const factory VeilidConfigPrivacy({ + required bool requireInboundRelay, + }) = _VeilidConfigPrivacy; + + factory VeilidConfigPrivacy.fromJson(dynamic json) => + _$VeilidConfigPrivacyFromJson(json as Map); +} + +//////////// + @freezed sealed class VeilidConfigTLS with _$VeilidConfigTLS { const factory VeilidConfigTLS({ @@ -370,6 +382,7 @@ sealed class VeilidConfigNetwork with _$VeilidConfigNetwork { required VeilidConfigTLS tls, required VeilidConfigApplication application, required VeilidConfigProtocol protocol, + required VeilidConfigPrivacy privacy, String? networkKeyPassword, }) = _VeilidConfigNetwork; diff --git a/veilid-flutter/lib/veilid_config.freezed.dart b/veilid-flutter/lib/veilid_config.freezed.dart index 5cce7091..25382348 100644 --- a/veilid-flutter/lib/veilid_config.freezed.dart +++ b/veilid-flutter/lib/veilid_config.freezed.dart @@ -4296,6 +4296,169 @@ class __$VeilidConfigProtocolCopyWithImpl<$Res> } } +/// @nodoc +mixin _$VeilidConfigPrivacy implements DiagnosticableTreeMixin { + bool get requireInboundRelay; + + /// Create a copy of VeilidConfigPrivacy + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VeilidConfigPrivacyCopyWith get copyWith => + _$VeilidConfigPrivacyCopyWithImpl( + this as VeilidConfigPrivacy, _$identity); + + /// Serializes this VeilidConfigPrivacy to a JSON map. + Map toJson(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'VeilidConfigPrivacy')) + ..add(DiagnosticsProperty('requireInboundRelay', requireInboundRelay)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VeilidConfigPrivacy && + (identical(other.requireInboundRelay, requireInboundRelay) || + other.requireInboundRelay == requireInboundRelay)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, requireInboundRelay); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'VeilidConfigPrivacy(requireInboundRelay: $requireInboundRelay)'; + } +} + +/// @nodoc +abstract mixin class $VeilidConfigPrivacyCopyWith<$Res> { + factory $VeilidConfigPrivacyCopyWith( + VeilidConfigPrivacy value, $Res Function(VeilidConfigPrivacy) _then) = + _$VeilidConfigPrivacyCopyWithImpl; + @useResult + $Res call({bool requireInboundRelay}); +} + +/// @nodoc +class _$VeilidConfigPrivacyCopyWithImpl<$Res> + implements $VeilidConfigPrivacyCopyWith<$Res> { + _$VeilidConfigPrivacyCopyWithImpl(this._self, this._then); + + final VeilidConfigPrivacy _self; + final $Res Function(VeilidConfigPrivacy) _then; + + /// Create a copy of VeilidConfigPrivacy + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? requireInboundRelay = null, + }) { + return _then(_self.copyWith( + requireInboundRelay: null == requireInboundRelay + ? _self.requireInboundRelay + : requireInboundRelay // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _VeilidConfigPrivacy + with DiagnosticableTreeMixin + implements VeilidConfigPrivacy { + const _VeilidConfigPrivacy({required this.requireInboundRelay}); + factory _VeilidConfigPrivacy.fromJson(Map json) => + _$VeilidConfigPrivacyFromJson(json); + + @override + final bool requireInboundRelay; + + /// Create a copy of VeilidConfigPrivacy + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$VeilidConfigPrivacyCopyWith<_VeilidConfigPrivacy> get copyWith => + __$VeilidConfigPrivacyCopyWithImpl<_VeilidConfigPrivacy>( + this, _$identity); + + @override + Map toJson() { + return _$VeilidConfigPrivacyToJson( + this, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'VeilidConfigPrivacy')) + ..add(DiagnosticsProperty('requireInboundRelay', requireInboundRelay)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _VeilidConfigPrivacy && + (identical(other.requireInboundRelay, requireInboundRelay) || + other.requireInboundRelay == requireInboundRelay)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, requireInboundRelay); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'VeilidConfigPrivacy(requireInboundRelay: $requireInboundRelay)'; + } +} + +/// @nodoc +abstract mixin class _$VeilidConfigPrivacyCopyWith<$Res> + implements $VeilidConfigPrivacyCopyWith<$Res> { + factory _$VeilidConfigPrivacyCopyWith(_VeilidConfigPrivacy value, + $Res Function(_VeilidConfigPrivacy) _then) = + __$VeilidConfigPrivacyCopyWithImpl; + @override + @useResult + $Res call({bool requireInboundRelay}); +} + +/// @nodoc +class __$VeilidConfigPrivacyCopyWithImpl<$Res> + implements _$VeilidConfigPrivacyCopyWith<$Res> { + __$VeilidConfigPrivacyCopyWithImpl(this._self, this._then); + + final _VeilidConfigPrivacy _self; + final $Res Function(_VeilidConfigPrivacy) _then; + + /// Create a copy of VeilidConfigPrivacy + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? requireInboundRelay = null, + }) { + return _then(_VeilidConfigPrivacy( + requireInboundRelay: null == requireInboundRelay + ? _self.requireInboundRelay + : requireInboundRelay // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + /// @nodoc mixin _$VeilidConfigTLS implements DiagnosticableTreeMixin { String get certificatePath; @@ -5922,6 +6085,7 @@ mixin _$VeilidConfigNetwork implements DiagnosticableTreeMixin { VeilidConfigTLS get tls; VeilidConfigApplication get application; VeilidConfigProtocol get protocol; + VeilidConfigPrivacy get privacy; String? get networkKeyPassword; /// Create a copy of VeilidConfigNetwork @@ -5965,6 +6129,7 @@ mixin _$VeilidConfigNetwork implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('tls', tls)) ..add(DiagnosticsProperty('application', application)) ..add(DiagnosticsProperty('protocol', protocol)) + ..add(DiagnosticsProperty('privacy', privacy)) ..add(DiagnosticsProperty('networkKeyPassword', networkKeyPassword)); } @@ -6012,6 +6177,7 @@ mixin _$VeilidConfigNetwork implements DiagnosticableTreeMixin { other.application == application) && (identical(other.protocol, protocol) || other.protocol == protocol) && + (identical(other.privacy, privacy) || other.privacy == privacy) && (identical(other.networkKeyPassword, networkKeyPassword) || other.networkKeyPassword == networkKeyPassword)); } @@ -6038,12 +6204,13 @@ mixin _$VeilidConfigNetwork implements DiagnosticableTreeMixin { tls, application, protocol, + privacy, networkKeyPassword ]); @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VeilidConfigNetwork(connectionInitialTimeoutMs: $connectionInitialTimeoutMs, connectionInactivityTimeoutMs: $connectionInactivityTimeoutMs, maxConnectionsPerIp4: $maxConnectionsPerIp4, maxConnectionsPerIp6Prefix: $maxConnectionsPerIp6Prefix, maxConnectionsPerIp6PrefixSize: $maxConnectionsPerIp6PrefixSize, maxConnectionFrequencyPerMin: $maxConnectionFrequencyPerMin, clientAllowlistTimeoutMs: $clientAllowlistTimeoutMs, reverseConnectionReceiptTimeMs: $reverseConnectionReceiptTimeMs, holePunchReceiptTimeMs: $holePunchReceiptTimeMs, routingTable: $routingTable, rpc: $rpc, dht: $dht, upnp: $upnp, detectAddressChanges: $detectAddressChanges, restrictedNatRetries: $restrictedNatRetries, tls: $tls, application: $application, protocol: $protocol, networkKeyPassword: $networkKeyPassword)'; + return 'VeilidConfigNetwork(connectionInitialTimeoutMs: $connectionInitialTimeoutMs, connectionInactivityTimeoutMs: $connectionInactivityTimeoutMs, maxConnectionsPerIp4: $maxConnectionsPerIp4, maxConnectionsPerIp6Prefix: $maxConnectionsPerIp6Prefix, maxConnectionsPerIp6PrefixSize: $maxConnectionsPerIp6PrefixSize, maxConnectionFrequencyPerMin: $maxConnectionFrequencyPerMin, clientAllowlistTimeoutMs: $clientAllowlistTimeoutMs, reverseConnectionReceiptTimeMs: $reverseConnectionReceiptTimeMs, holePunchReceiptTimeMs: $holePunchReceiptTimeMs, routingTable: $routingTable, rpc: $rpc, dht: $dht, upnp: $upnp, detectAddressChanges: $detectAddressChanges, restrictedNatRetries: $restrictedNatRetries, tls: $tls, application: $application, protocol: $protocol, privacy: $privacy, networkKeyPassword: $networkKeyPassword)'; } } @@ -6072,6 +6239,7 @@ abstract mixin class $VeilidConfigNetworkCopyWith<$Res> { VeilidConfigTLS tls, VeilidConfigApplication application, VeilidConfigProtocol protocol, + VeilidConfigPrivacy privacy, String? networkKeyPassword}); $VeilidConfigRoutingTableCopyWith<$Res> get routingTable; @@ -6080,6 +6248,7 @@ abstract mixin class $VeilidConfigNetworkCopyWith<$Res> { $VeilidConfigTLSCopyWith<$Res> get tls; $VeilidConfigApplicationCopyWith<$Res> get application; $VeilidConfigProtocolCopyWith<$Res> get protocol; + $VeilidConfigPrivacyCopyWith<$Res> get privacy; } /// @nodoc @@ -6113,6 +6282,7 @@ class _$VeilidConfigNetworkCopyWithImpl<$Res> Object? tls = null, Object? application = null, Object? protocol = null, + Object? privacy = null, Object? networkKeyPassword = freezed, }) { return _then(_self.copyWith( @@ -6188,6 +6358,10 @@ class _$VeilidConfigNetworkCopyWithImpl<$Res> ? _self.protocol : protocol // ignore: cast_nullable_to_non_nullable as VeilidConfigProtocol, + privacy: null == privacy + ? _self.privacy + : privacy // ignore: cast_nullable_to_non_nullable + as VeilidConfigPrivacy, networkKeyPassword: freezed == networkKeyPassword ? _self.networkKeyPassword : networkKeyPassword // ignore: cast_nullable_to_non_nullable @@ -6254,6 +6428,16 @@ class _$VeilidConfigNetworkCopyWithImpl<$Res> return _then(_self.copyWith(protocol: value)); }); } + + /// Create a copy of VeilidConfigNetwork + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $VeilidConfigPrivacyCopyWith<$Res> get privacy { + return $VeilidConfigPrivacyCopyWith<$Res>(_self.privacy, (value) { + return _then(_self.copyWith(privacy: value)); + }); + } } /// @nodoc @@ -6280,6 +6464,7 @@ class _VeilidConfigNetwork required this.tls, required this.application, required this.protocol, + required this.privacy, this.networkKeyPassword}); factory _VeilidConfigNetwork.fromJson(Map json) => _$VeilidConfigNetworkFromJson(json); @@ -6321,6 +6506,8 @@ class _VeilidConfigNetwork @override final VeilidConfigProtocol protocol; @override + final VeilidConfigPrivacy privacy; + @override final String? networkKeyPassword; /// Create a copy of VeilidConfigNetwork @@ -6369,6 +6556,7 @@ class _VeilidConfigNetwork ..add(DiagnosticsProperty('tls', tls)) ..add(DiagnosticsProperty('application', application)) ..add(DiagnosticsProperty('protocol', protocol)) + ..add(DiagnosticsProperty('privacy', privacy)) ..add(DiagnosticsProperty('networkKeyPassword', networkKeyPassword)); } @@ -6416,6 +6604,7 @@ class _VeilidConfigNetwork other.application == application) && (identical(other.protocol, protocol) || other.protocol == protocol) && + (identical(other.privacy, privacy) || other.privacy == privacy) && (identical(other.networkKeyPassword, networkKeyPassword) || other.networkKeyPassword == networkKeyPassword)); } @@ -6442,12 +6631,13 @@ class _VeilidConfigNetwork tls, application, protocol, + privacy, networkKeyPassword ]); @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VeilidConfigNetwork(connectionInitialTimeoutMs: $connectionInitialTimeoutMs, connectionInactivityTimeoutMs: $connectionInactivityTimeoutMs, maxConnectionsPerIp4: $maxConnectionsPerIp4, maxConnectionsPerIp6Prefix: $maxConnectionsPerIp6Prefix, maxConnectionsPerIp6PrefixSize: $maxConnectionsPerIp6PrefixSize, maxConnectionFrequencyPerMin: $maxConnectionFrequencyPerMin, clientAllowlistTimeoutMs: $clientAllowlistTimeoutMs, reverseConnectionReceiptTimeMs: $reverseConnectionReceiptTimeMs, holePunchReceiptTimeMs: $holePunchReceiptTimeMs, routingTable: $routingTable, rpc: $rpc, dht: $dht, upnp: $upnp, detectAddressChanges: $detectAddressChanges, restrictedNatRetries: $restrictedNatRetries, tls: $tls, application: $application, protocol: $protocol, networkKeyPassword: $networkKeyPassword)'; + return 'VeilidConfigNetwork(connectionInitialTimeoutMs: $connectionInitialTimeoutMs, connectionInactivityTimeoutMs: $connectionInactivityTimeoutMs, maxConnectionsPerIp4: $maxConnectionsPerIp4, maxConnectionsPerIp6Prefix: $maxConnectionsPerIp6Prefix, maxConnectionsPerIp6PrefixSize: $maxConnectionsPerIp6PrefixSize, maxConnectionFrequencyPerMin: $maxConnectionFrequencyPerMin, clientAllowlistTimeoutMs: $clientAllowlistTimeoutMs, reverseConnectionReceiptTimeMs: $reverseConnectionReceiptTimeMs, holePunchReceiptTimeMs: $holePunchReceiptTimeMs, routingTable: $routingTable, rpc: $rpc, dht: $dht, upnp: $upnp, detectAddressChanges: $detectAddressChanges, restrictedNatRetries: $restrictedNatRetries, tls: $tls, application: $application, protocol: $protocol, privacy: $privacy, networkKeyPassword: $networkKeyPassword)'; } } @@ -6478,6 +6668,7 @@ abstract mixin class _$VeilidConfigNetworkCopyWith<$Res> VeilidConfigTLS tls, VeilidConfigApplication application, VeilidConfigProtocol protocol, + VeilidConfigPrivacy privacy, String? networkKeyPassword}); @override @@ -6492,6 +6683,8 @@ abstract mixin class _$VeilidConfigNetworkCopyWith<$Res> $VeilidConfigApplicationCopyWith<$Res> get application; @override $VeilidConfigProtocolCopyWith<$Res> get protocol; + @override + $VeilidConfigPrivacyCopyWith<$Res> get privacy; } /// @nodoc @@ -6525,6 +6718,7 @@ class __$VeilidConfigNetworkCopyWithImpl<$Res> Object? tls = null, Object? application = null, Object? protocol = null, + Object? privacy = null, Object? networkKeyPassword = freezed, }) { return _then(_VeilidConfigNetwork( @@ -6600,6 +6794,10 @@ class __$VeilidConfigNetworkCopyWithImpl<$Res> ? _self.protocol : protocol // ignore: cast_nullable_to_non_nullable as VeilidConfigProtocol, + privacy: null == privacy + ? _self.privacy + : privacy // ignore: cast_nullable_to_non_nullable + as VeilidConfigPrivacy, networkKeyPassword: freezed == networkKeyPassword ? _self.networkKeyPassword : networkKeyPassword // ignore: cast_nullable_to_non_nullable @@ -6666,6 +6864,16 @@ class __$VeilidConfigNetworkCopyWithImpl<$Res> return _then(_self.copyWith(protocol: value)); }); } + + /// Create a copy of VeilidConfigNetwork + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $VeilidConfigPrivacyCopyWith<$Res> get privacy { + return $VeilidConfigPrivacyCopyWith<$Res>(_self.privacy, (value) { + return _then(_self.copyWith(privacy: value)); + }); + } } /// @nodoc diff --git a/veilid-flutter/lib/veilid_config.g.dart b/veilid-flutter/lib/veilid_config.g.dart index 2dc59a97..a4ab5b31 100644 --- a/veilid-flutter/lib/veilid_config.g.dart +++ b/veilid-flutter/lib/veilid_config.g.dart @@ -314,6 +314,17 @@ Map _$VeilidConfigProtocolToJson( 'wss': instance.wss.toJson(), }; +_VeilidConfigPrivacy _$VeilidConfigPrivacyFromJson(Map json) => + _VeilidConfigPrivacy( + requireInboundRelay: json['require_inbound_relay'] as bool, + ); + +Map _$VeilidConfigPrivacyToJson( + _VeilidConfigPrivacy instance) => + { + 'require_inbound_relay': instance.requireInboundRelay, + }; + _VeilidConfigTLS _$VeilidConfigTLSFromJson(Map json) => _VeilidConfigTLS( certificatePath: json['certificate_path'] as String, @@ -472,6 +483,7 @@ _VeilidConfigNetwork _$VeilidConfigNetworkFromJson(Map json) => tls: VeilidConfigTLS.fromJson(json['tls']), application: VeilidConfigApplication.fromJson(json['application']), protocol: VeilidConfigProtocol.fromJson(json['protocol']), + privacy: VeilidConfigPrivacy.fromJson(json['privacy']), networkKeyPassword: json['network_key_password'] as String?, ); @@ -499,6 +511,7 @@ Map _$VeilidConfigNetworkToJson( 'tls': instance.tls.toJson(), 'application': instance.application.toJson(), 'protocol': instance.protocol.toJson(), + 'privacy': instance.privacy.toJson(), 'network_key_password': instance.networkKeyPassword, }; diff --git a/veilid-flutter/lib/veilid_ffi.dart b/veilid-flutter/lib/veilid_ffi.dart index 6bff4610..463ca290 100644 --- a/veilid-flutter/lib/veilid_ffi.dart +++ b/veilid-flutter/lib/veilid_ffi.dart @@ -696,17 +696,17 @@ class VeilidRoutingContextFFI extends VeilidRoutingContext { @override Future setDHTValue(TypedKey key, int subkey, Uint8List data, - {KeyPair? writer}) async { + {SetDHTValueOptions? options}) async { _ctx.ensureValid(); final nativeKey = jsonEncode(key).toNativeUtf8(); final nativeData = base64UrlNoPadEncode(data).toNativeUtf8(); - final nativeWriter = - writer != null ? jsonEncode(writer).toNativeUtf8() : nullptr; + final nativeOptions = + options != null ? jsonEncode(options).toNativeUtf8() : nullptr; final recvPort = ReceivePort('routing_context_set_dht_value'); final sendPort = recvPort.sendPort; _ctx.ffi._routingContextSetDHTValue(sendPort.nativePort, _ctx.id!, - nativeKey, subkey, nativeData, nativeWriter); + nativeKey, subkey, nativeData, nativeOptions); final valueData = await processFutureOptJson(ValueData.fromJson, recvPort.first); return valueData; diff --git a/veilid-flutter/lib/veilid_js.dart b/veilid-flutter/lib/veilid_js.dart index a76af65f..98047ca9 100644 --- a/veilid-flutter/lib/veilid_js.dart +++ b/veilid-flutter/lib/veilid_js.dart @@ -193,7 +193,7 @@ class VeilidRoutingContextJS extends VeilidRoutingContext { @override Future setDHTValue(TypedKey key, int subkey, Uint8List data, - {KeyPair? writer}) async { + {SetDHTValueOptions? options}) async { final id = _ctx.requireId(); final opt = await _wrapApiPromise( js_util.callMethod(wasm, 'routing_context_set_dht_value', [ @@ -201,7 +201,7 @@ class VeilidRoutingContextJS extends VeilidRoutingContext { jsonEncode(key), subkey, base64UrlNoPadEncode(data), - if (writer != null) jsonEncode(writer) else null + if (options != null) jsonEncode(options) else null ])); if (opt == null) { return null; diff --git a/veilid-flutter/rust/src/dart_ffi.rs b/veilid-flutter/rust/src/dart_ffi.rs index 28519d6c..08c67b30 100644 --- a/veilid-flutter/rust/src/dart_ffi.rs +++ b/veilid-flutter/rust/src/dart_ffi.rs @@ -793,14 +793,14 @@ pub extern "C" fn routing_context_set_dht_value( key: FfiStr, subkey: u32, data: FfiStr, - writer: FfiStr, + options: FfiStr, ) { let key: veilid_core::TypedRecordKey = veilid_core::deserialize_opt_json(key.into_opt_string()).unwrap(); let data: Vec = data_encoding::BASE64URL_NOPAD .decode(data.into_opt_string().unwrap().as_bytes()) .unwrap(); - let writer: Option = writer + let options: Option = options .into_opt_string() .map(|s| veilid_core::deserialize_json(&s).unwrap()); @@ -809,7 +809,7 @@ pub extern "C" fn routing_context_set_dht_value( let routing_context = get_routing_context(id, "routing_context_set_dht_value")?; let res = routing_context - .set_dht_value(key, subkey, data, writer) + .set_dht_value(key, subkey, data, options) .await?; APIResult::Ok(res) } diff --git a/veilid-python/tests/test_dht.py b/veilid-python/tests/test_dht.py index 817a466b..735a7c90 100644 --- a/veilid-python/tests/test_dht.py +++ b/veilid-python/tests/test_dht.py @@ -238,14 +238,14 @@ async def test_open_writer_dht_value(api_connection: veilid.VeilidAPI): # Verify subkey 0 can be set because override with the right writer # Should have prior sequence number as its returned value because it exists online at seq 0 - vdtemp = await rc.set_dht_value(key, ValueSubkey(0), va, veilid.KeyPair.from_parts(owner, secret)) + vdtemp = await rc.set_dht_value(key, ValueSubkey(0), va, veilid.SetDHTValueOptions(veilid.KeyPair.from_parts(owner, secret))) assert vdtemp is not None assert vdtemp.data == vb assert vdtemp.seq == 0 assert vdtemp.writer == owner # Should update the second time to seq 1 - vdtemp = await rc.set_dht_value(key, ValueSubkey(0), va, veilid.KeyPair.from_parts(owner, secret)) + vdtemp = await rc.set_dht_value(key, ValueSubkey(0), va, veilid.SetDHTValueOptions(veilid.KeyPair.from_parts(owner, secret))) assert vdtemp is None # Clean up diff --git a/veilid-python/veilid/api.py b/veilid-python/veilid/api.py index ec71258e..523a4296 100644 --- a/veilid-python/veilid/api.py +++ b/veilid-python/veilid/api.py @@ -84,7 +84,7 @@ class RoutingContext(ABC): @abstractmethod async def set_dht_value( - self, key: types.TypedKey, subkey: types.ValueSubkey, data: bytes, writer: Optional[types.KeyPair] = None + self, key: types.TypedKey, subkey: types.ValueSubkey, data: bytes, options: Optional[types.SetDHTValueOptions] = None ) -> Optional[types.ValueData]: pass diff --git a/veilid-python/veilid/json_api.py b/veilid-python/veilid/json_api.py index 221f2ba1..3037777a 100644 --- a/veilid-python/veilid/json_api.py +++ b/veilid-python/veilid/json_api.py @@ -36,6 +36,7 @@ from .types import ( SafetySelection, SecretKey, Sequencing, + SetDHTValueOptions, SharedSecret, Signature, Stability, @@ -721,12 +722,12 @@ class _JsonRoutingContext(RoutingContext): return None if ret is None else ValueData.from_json(ret) async def set_dht_value( - self, key: TypedKey, subkey: ValueSubkey, data: bytes, writer: Optional[KeyPair] = None + self, key: TypedKey, subkey: ValueSubkey, data: bytes, options: Optional[SetDHTValueOptions] = None ) -> Optional[ValueData]: assert isinstance(key, TypedKey) assert isinstance(subkey, ValueSubkey) assert isinstance(data, bytes) - assert writer is None or isinstance(writer, KeyPair) + assert options is None or isinstance(options, SetDHTValueOptions) ret = raise_api_result( await self.api.send_ndjson_request( @@ -737,7 +738,7 @@ class _JsonRoutingContext(RoutingContext): key=key, subkey=subkey, data=data, - writer=writer, + options=options, ) ) return None if ret is None else ValueData.from_json(ret) diff --git a/veilid-python/veilid/schema/RecvMessage.json b/veilid-python/veilid/schema/RecvMessage.json index b51fd5c6..e46987ca 100644 --- a/veilid-python/veilid/schema/RecvMessage.json +++ b/veilid-python/veilid/schema/RecvMessage.json @@ -3944,6 +3944,7 @@ ] }, "VeilidCapability": { + "description": "A four-character code", "type": "array", "items": { "type": "integer", diff --git a/veilid-python/veilid/schema/Request.json b/veilid-python/veilid/schema/Request.json index 87655182..a8308ffd 100644 --- a/veilid-python/veilid/schema/Request.json +++ b/veilid-python/veilid/schema/Request.json @@ -461,6 +461,16 @@ "key": { "type": "string" }, + "options": { + "anyOf": [ + { + "$ref": "#/definitions/SetDHTValueOptions" + }, + { + "type": "null" + } + ] + }, "rc_op": { "type": "string", "enum": [ @@ -471,12 +481,6 @@ "type": "integer", "format": "uint32", "minimum": 0.0 - }, - "writer": { - "type": [ - "string", - "null" - ] } } }, @@ -1656,6 +1660,9 @@ } }, "definitions": { + "AllowOffline": { + "type": "boolean" + }, "DHTReportScope": { "description": "DHT Record Report Scope", "oneOf": [ @@ -1852,6 +1859,28 @@ "EnsureOrdered" ] }, + "SetDHTValueOptions": { + "type": "object", + "properties": { + "allow_offline": { + "description": "Defaults to true. If false, the value will not be written if the node is offline, and a TryAgain error will be returned.", + "anyOf": [ + { + "$ref": "#/definitions/AllowOffline" + }, + { + "type": "null" + } + ] + }, + "writer": { + "type": [ + "string", + "null" + ] + } + } + }, "Stability": { "type": "string", "enum": [ diff --git a/veilid-python/veilid/types.py b/veilid-python/veilid/types.py index f91f0036..343bcd09 100644 --- a/veilid-python/veilid/types.py +++ b/veilid-python/veilid/types.py @@ -431,6 +431,27 @@ class DHTRecordReport: return self.__dict__ +class SetDHTValueOptions: + writer: Optional[KeyPair] + allow_offline: Optional[bool] + + def __init__(self, writer: Optional[KeyPair], allow_offline: Optional[bool] = None): + self.writer = writer + self.allow_offline = allow_offline + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(writer={self.writer!r}, allow_offline={self.allow_offline!r})>" + + @classmethod + def from_json(cls, j: dict) -> Self: + return cls( + KeyPair(j["writer"]) if "writer" in j else None, + j["allow_offline"] if "allow_offline" in j else None, + ) + + def to_json(self) -> dict: + return self.__dict__ + @total_ordering class ValueData: seq: ValueSeqNum diff --git a/veilid-remote-api/src/process.rs b/veilid-remote-api/src/process.rs index 3249b4d5..4a1e591a 100644 --- a/veilid-remote-api/src/process.rs +++ b/veilid-remote-api/src/process.rs @@ -324,11 +324,11 @@ impl JsonRequestProcessor { key, subkey, data, - writer, + options, } => RoutingContextResponseOp::SetDhtValue { result: to_json_api_result( routing_context - .set_dht_value(key, subkey, data, writer) + .set_dht_value(key, subkey, data, options) .await, ), }, diff --git a/veilid-remote-api/src/routing_context.rs b/veilid-remote-api/src/routing_context.rs index 297be42c..1c302bf6 100644 --- a/veilid-remote-api/src/routing_context.rs +++ b/veilid-remote-api/src/routing_context.rs @@ -72,8 +72,7 @@ pub enum RoutingContextRequestOp { #[serde(with = "as_human_base64")] #[schemars(with = "String")] data: Vec, - #[schemars(with = "Option")] - writer: Option, + options: Option, }, WatchDhtValues { #[schemars(with = "String")] diff --git a/veilid-wasm/src/lib.rs b/veilid-wasm/src/lib.rs index 838cd9be..dc8edab1 100644 --- a/veilid-wasm/src/lib.rs +++ b/veilid-wasm/src/lib.rs @@ -593,7 +593,7 @@ pub fn routing_context_set_dht_value( key: String, subkey: u32, data: String, - writer: Option, + options: Option, ) -> Promise { wrap_api_future_json(async move { let key: veilid_core::TypedRecordKey = @@ -601,15 +601,16 @@ pub fn routing_context_set_dht_value( let data: Vec = data_encoding::BASE64URL_NOPAD .decode(data.as_bytes()) .map_err(VeilidAPIError::generic)?; - let writer: Option = match writer { - Some(s) => veilid_core::deserialize_json(&s).map_err(VeilidAPIError::generic)?, + + let options: Option = match options { + Some(s) => Some(veilid_core::deserialize_json(&s).map_err(VeilidAPIError::generic)?), None => None, }; let routing_context = get_routing_context(id, "routing_context_set_dht_value")?; let res = routing_context - .set_dht_value(key, subkey, data, writer) + .set_dht_value(key, subkey, data, options) .await?; APIResult::Ok(res) }) diff --git a/veilid-wasm/src/veilid_routing_context_js.rs b/veilid-wasm/src/veilid_routing_context_js.rs index 3b613b5d..7c4eca77 100644 --- a/veilid-wasm/src/veilid_routing_context_js.rs +++ b/veilid-wasm/src/veilid_routing_context_js.rs @@ -322,17 +322,14 @@ impl VeilidRoutingContext { key: String, subkey: u32, data: Box<[u8]>, - writer: Option, + options: Option, ) -> APIResult> { let key = TypedRecordKey::from_str(&key)?; let data = data.into_vec(); - let writer = writer - .map(|writer| KeyPair::from_str(&writer)) - .map_or(APIResult::Ok(None), |r| r.map(Some))?; let routing_context = self.getRoutingContext()?; let res = routing_context - .set_dht_value(key, subkey, data, writer) + .set_dht_value(key, subkey, data, options) .await?; APIResult::Ok(res) } diff --git a/veilid-wasm/tests/package-lock.json b/veilid-wasm/tests/package-lock.json index 2eb3f9b1..8863f9b3 100644 --- a/veilid-wasm/tests/package-lock.json +++ b/veilid-wasm/tests/package-lock.json @@ -21,7 +21,7 @@ }, "../pkg": { "name": "veilid-wasm", - "version": "0.4.6", + "version": "0.4.7", "dev": true, "license": "MPL-2.0" }, diff --git a/veilid-wasm/tests/src/VeilidRoutingContext.test.ts b/veilid-wasm/tests/src/VeilidRoutingContext.test.ts index dd625226..40c7cc6c 100644 --- a/veilid-wasm/tests/src/VeilidRoutingContext.test.ts +++ b/veilid-wasm/tests/src/VeilidRoutingContext.test.ts @@ -225,7 +225,10 @@ describe('VeilidRoutingContext', () => { dhtRecord.key, 0, textEncoder.encode(`${data}👋`), - `${dhtRecord.owner}:${dhtRecord.owner_secret}` + { + writer: `${dhtRecord.owner}:${dhtRecord.owner_secret}`, + allow_offline: undefined + } ); expect(setValueRes).toBeUndefined(); });