lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit a4aeee78dffb0dc54b9abc45735f1900cbfa67bd
parent 361ca23987629201e255e5e801835c7ef660b9df
Author: triesap <tyson@radroots.org>
Date:   Fri, 10 Jul 2026 18:57:54 +0000

transport: unify runtime transport contract

- make RadrootsTransport the async-capable production status, delivery, and fetch trait
- register runtime transports through the core trait instead of a runtime adapter API
- implement the core trait for the Reticulum preview transport without success or retry behavior
- add source-boundary and behavior tests for runtime and Reticulum contract alignment

Diffstat:
MCargo.lock | 2++
Mcrates/runtime/src/lib.rs | 6++----
Mcrates/runtime/src/transport.rs | 275+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcrates/transport/Cargo.toml | 1+
Mcrates/transport/src/lib.rs | 1+
Mcrates/transport/src/transport.rs | 27++++++++++++++++++---------
Mcrates/transport/tests/source_boundary.rs | 39+++++++++++++++++++++++++++++++++++++++
Mcrates/transport/tests/transport.rs | 94++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mcrates/transport_reticulum/Cargo.toml | 1+
Mcrates/transport_reticulum/src/lib.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/transport_reticulum/tests/reticulum_preview.rs | 52++++++++++++++++++++++++++++++++++++++++++++++++----
11 files changed, 377 insertions(+), 186 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4848,6 +4848,7 @@ dependencies = [ name = "radroots_transport" version = "0.1.0-alpha.2" dependencies = [ + "futures", "serde", "serde_json", "sha2", @@ -4884,6 +4885,7 @@ dependencies = [ name = "radroots_transport_reticulum" version = "0.1.0-alpha.2" dependencies = [ + "futures", "radroots_transport", "serde", "serde_json", diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs @@ -42,13 +42,11 @@ pub use signals::shutdown_signal; pub use tracing::{ default_shared_runtime_logs_dir, default_shared_runtime_logs_dir_for, init, init_with_logs_dir, }; -#[cfg(feature = "transport-reticulum")] -pub use transport::RadrootsRuntimeReticulumPreviewTransport; #[cfg(feature = "transport")] pub use transport::{ RadrootsRuntimeBoundedQueue, RadrootsRuntimeQueueStatus, RadrootsRuntimeQueueTask, - RadrootsRuntimeTransportAdapter, RadrootsRuntimeTransportDispatchRequest, - RadrootsRuntimeTransportError, RadrootsRuntimeTransportFuture, RadrootsRuntimeTransportPayload, + RadrootsRuntimeTransportDispatchRequest, RadrootsRuntimeTransportError, + RadrootsRuntimeTransportFuture, RadrootsRuntimeTransportPayload, RadrootsRuntimeTransportRegistry, }; #[cfg(feature = "transport-workers")] diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs @@ -7,9 +7,9 @@ use radroots_events::draft::RadrootsSignedNostrEvent; #[cfg(feature = "transport-workers")] use radroots_transport::RadrootsTransportTargetReceipt; use radroots_transport::{ - RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, - RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportKind, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetSet, + RadrootsTransport, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, + RadrootsTransportError, RadrootsTransportKind, RadrootsTransportSatisfactionPolicy, + RadrootsTransportTarget, RadrootsTransportTargetSet, }; use thiserror::Error; @@ -18,11 +18,11 @@ pub type RadrootsRuntimeTransportFuture<'a, T> = #[derive(Debug, Error)] pub enum RadrootsRuntimeTransportError { - #[error("transport adapter for `{0}` is not registered")] - AdapterNotRegistered(String), + #[error("transport `{0}` is not registered")] + TransportNotRegistered(String), - #[error("transport adapter for `{0}` is already registered")] - AdapterAlreadyRegistered(String), + #[error("transport `{0}` is already registered")] + TransportAlreadyRegistered(String), #[error("transport dispatch received no targets")] EmptyDispatchTargets, @@ -44,8 +44,8 @@ pub enum RadrootsRuntimeTransportError { signed_event_id: String, }, - #[error("transport adapter `{kind}` failed: {message}")] - Adapter { kind: String, message: String }, + #[error("transport `{kind}` failed: {message}")] + Transport { kind: String, message: String }, } impl From<RadrootsTransportError> for RadrootsRuntimeTransportError { @@ -109,18 +109,9 @@ impl RadrootsRuntimeTransportDispatchRequest { } } -pub trait RadrootsRuntimeTransportAdapter: Send + Sync { - fn transport_kind(&self) -> RadrootsTransportKind; - - fn deliver<'a>( - &'a self, - request: RadrootsRuntimeTransportDispatchRequest, - ) -> RadrootsRuntimeTransportFuture<'a, RadrootsTransportDeliveryReceipt>; -} - #[derive(Clone, Default)] pub struct RadrootsRuntimeTransportRegistry { - adapters: BTreeMap<RadrootsTransportKind, Arc<dyn RadrootsRuntimeTransportAdapter>>, + transports: BTreeMap<RadrootsTransportKind, Arc<dyn RadrootsTransport>>, } impl RadrootsRuntimeTransportRegistry { @@ -128,72 +119,31 @@ impl RadrootsRuntimeTransportRegistry { Self::default() } - pub fn register<A>(&mut self, adapter: A) -> Result<(), RadrootsRuntimeTransportError> + pub fn register<T>(&mut self, transport: T) -> Result<(), RadrootsRuntimeTransportError> where - A: RadrootsRuntimeTransportAdapter + 'static, + T: RadrootsTransport + 'static, { - let kind = adapter.transport_kind(); - if self.adapters.contains_key(&kind) { - return Err(RadrootsRuntimeTransportError::AdapterAlreadyRegistered( + let kind = transport.transport_kind(); + if self.transports.contains_key(&kind) { + return Err(RadrootsRuntimeTransportError::TransportAlreadyRegistered( kind.canonical_label(), )); } - self.adapters.insert(kind, Arc::new(adapter)); + self.transports.insert(kind, Arc::new(transport)); Ok(()) } - pub fn adapter( + pub fn transport( &self, kind: &RadrootsTransportKind, - ) -> Result<Arc<dyn RadrootsRuntimeTransportAdapter>, RadrootsRuntimeTransportError> { - self.adapters.get(kind).cloned().ok_or_else(|| { - RadrootsRuntimeTransportError::AdapterNotRegistered(kind.canonical_label()) + ) -> Result<Arc<dyn RadrootsTransport>, RadrootsRuntimeTransportError> { + self.transports.get(kind).cloned().ok_or_else(|| { + RadrootsRuntimeTransportError::TransportNotRegistered(kind.canonical_label()) }) } pub fn registered_kinds(&self) -> Vec<RadrootsTransportKind> { - self.adapters.keys().cloned().collect() - } -} - -#[cfg(feature = "transport-reticulum")] -#[derive(Clone, Debug)] -pub struct RadrootsRuntimeReticulumPreviewTransport { - transport: radroots_transport_reticulum::RadrootsReticulumPreviewTransport, -} - -#[cfg(feature = "transport-reticulum")] -impl RadrootsRuntimeReticulumPreviewTransport { - pub fn new(transport: radroots_transport_reticulum::RadrootsReticulumPreviewTransport) -> Self { - Self { transport } - } -} - -#[cfg(feature = "transport-reticulum")] -impl Default for RadrootsRuntimeReticulumPreviewTransport { - fn default() -> Self { - Self::new(radroots_transport_reticulum::RadrootsReticulumPreviewTransport::default()) - } -} - -#[cfg(feature = "transport-reticulum")] -impl RadrootsRuntimeTransportAdapter for RadrootsRuntimeReticulumPreviewTransport { - fn transport_kind(&self) -> RadrootsTransportKind { - RadrootsTransportKind::Reticulum - } - - fn deliver<'a>( - &'a self, - request: RadrootsRuntimeTransportDispatchRequest, - ) -> RadrootsRuntimeTransportFuture<'a, RadrootsTransportDeliveryReceipt> { - Box::pin(async move { - self.transport - .deliver(request.transport_delivery_request()) - .map_err(|error| RadrootsRuntimeTransportError::Adapter { - kind: RadrootsTransportKind::Reticulum.canonical_label(), - message: error.to_string(), - }) - }) + self.transports.keys().cloned().collect() } } @@ -504,7 +454,7 @@ impl<'a> RadrootsRuntimeDeliveryWorker<'a> { } let mut plan_target_receipts = Vec::new(); for (kind, targets) in by_kind { - let adapter = self.registry.adapter(&kind)?; + let transport = self.registry.transport(&kind)?; let transport_targets = targets .iter() .map(|target| target.target.clone()) @@ -519,7 +469,13 @@ impl<'a> RadrootsRuntimeDeliveryWorker<'a> { satisfaction_policy.clone(), job.now_ms, )?; - let receipt = adapter.deliver(request).await?; + let receipt = transport + .deliver(request.transport_delivery_request()) + .await + .map_err(|error| RadrootsRuntimeTransportError::Transport { + kind: kind.canonical_label(), + message: error.to_string(), + })?; for target_receipt in receipt.target_receipts { target_states.insert( target_receipt.target.fingerprint.as_str().to_owned(), @@ -654,10 +610,9 @@ where #[cfg(test)] mod tests { use super::{ - RadrootsRuntimeBoundedQueue, RadrootsRuntimeTransportAdapter, - RadrootsRuntimeTransportDispatchRequest, RadrootsRuntimeTransportError, - RadrootsRuntimeTransportFuture, RadrootsRuntimeTransportPayload, - RadrootsRuntimeTransportRegistry, + RadrootsRuntimeBoundedQueue, RadrootsRuntimeTransportDispatchRequest, + RadrootsRuntimeTransportError, RadrootsRuntimeTransportFuture, + RadrootsRuntimeTransportPayload, RadrootsRuntimeTransportRegistry, }; #[cfg(feature = "transport-workers")] use super::{ @@ -670,26 +625,41 @@ mod tests { #[cfg(feature = "transport-workers")] use radroots_events::draft::{RadrootsSignedNostrEvent, RadrootsSignedNostrEventParts}; use radroots_transport::{ - RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryTargetStatus, - RadrootsTransportKind, RadrootsTransportOutcome, RadrootsTransportOutcomeKind, - RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, - RadrootsTransportTarget, RadrootsTransportTargetReceipt, + RadrootsTransport, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, + RadrootsTransportDeliveryTargetStatus, RadrootsTransportFetchReceipt, + RadrootsTransportFetchRequest, RadrootsTransportFuture, + RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportOutcome, + RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass, + RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget, + RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, }; - struct StaticAdapter { + struct StaticTransport { kind: RadrootsTransportKind, outcome_kind: RadrootsTransportOutcomeKind, } - impl RadrootsRuntimeTransportAdapter for StaticAdapter { + impl RadrootsTransport for StaticTransport { fn transport_kind(&self) -> RadrootsTransportKind { self.kind.clone() } + fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { + Box::pin(async move { + Ok(RadrootsTransportStatus::new( + self.kind.clone(), + true, + RadrootsTransportImplementationState::Real, + true, + "ready", + )) + }) + } + fn deliver<'a>( &'a self, - request: RadrootsRuntimeTransportDispatchRequest, - ) -> RadrootsRuntimeTransportFuture<'a, RadrootsTransportDeliveryReceipt> { + request: RadrootsTransportDeliveryRequest, + ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { Box::pin(async move { Ok(RadrootsTransportDeliveryReceipt { request_id: request.request_id, @@ -708,6 +678,30 @@ mod tests { }) }) } + + fn fetch<'a>( + &'a self, + request: RadrootsTransportFetchRequest, + ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { + Box::pin(async move { + Ok(RadrootsTransportFetchReceipt::new( + request.request_id, + request + .target_set + .targets() + .iter() + .cloned() + .map(|target| { + RadrootsTransportTargetReceipt::new( + target, + RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Seen), + ) + }) + .collect(), + 0, + )) + }) + } } fn target(kind: RadrootsTransportKind, uri: &str) -> RadrootsTransportTarget { @@ -751,10 +745,10 @@ mod tests { } #[tokio::test] - async fn registry_dispatches_nostr_adapter_by_transport_kind() { + async fn registry_dispatches_transport_by_transport_kind() { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry - .register(StaticAdapter { + .register(StaticTransport { kind: RadrootsTransportKind::Nostr, outcome_kind: RadrootsTransportOutcomeKind::Accepted, }) @@ -764,29 +758,42 @@ mod tests { vec![RadrootsTransportKind::Nostr] ); assert!(matches!( - registry.register(StaticAdapter { + registry.register(StaticTransport { kind: RadrootsTransportKind::Nostr, outcome_kind: RadrootsTransportOutcomeKind::Accepted, }), - Err(RadrootsRuntimeTransportError::AdapterAlreadyRegistered(_)) + Err(RadrootsRuntimeTransportError::TransportAlreadyRegistered(_)) )); - let adapter = registry - .adapter(&RadrootsTransportKind::Nostr) - .expect("nostr adapter"); - let receipt = adapter - .deliver( - RadrootsRuntimeTransportDispatchRequest::new( - "nostr-delivery", - RadrootsRuntimeTransportPayload::DigestOnly("sha256:event".to_owned()), - vec![target(RadrootsTransportKind::Nostr, "wss://relay.example")], - RadrootsTransportSatisfactionPolicy::any_accepted(), - 1_000, - ) - .expect("request"), - ) + let transport = registry + .transport(&RadrootsTransportKind::Nostr) + .expect("nostr transport"); + let request = RadrootsRuntimeTransportDispatchRequest::new( + "nostr-delivery", + RadrootsRuntimeTransportPayload::DigestOnly("sha256:event".to_owned()), + vec![target(RadrootsTransportKind::Nostr, "wss://relay.example")], + RadrootsTransportSatisfactionPolicy::any_accepted(), + 1_000, + ) + .expect("request"); + let receipt = transport + .deliver(request.transport_delivery_request()) .await .expect("receipt"); + let status = transport.status().await.expect("status"); + assert_eq!(status.kind, RadrootsTransportKind::Nostr); + let fetch = transport + .fetch(RadrootsTransportFetchRequest::new( + "nostr-fetch", + RadrootsTransportTargetSet::new(vec![target( + RadrootsTransportKind::Nostr, + "wss://relay.example", + )]) + .expect("target set"), + )) + .await + .expect("fetch"); + assert_eq!(fetch.fetched_count, 0); assert_eq!( receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), @@ -800,30 +807,46 @@ mod tests { #[cfg(feature = "transport-reticulum")] #[tokio::test] - async fn registry_dispatches_reticulum_preview_without_success() { + async fn registry_dispatches_reticulum_preview_transport_without_success() { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry - .register(super::RadrootsRuntimeReticulumPreviewTransport::default()) + .register(radroots_transport_reticulum::RadrootsReticulumPreviewTransport::default()) .expect("register"); - let adapter = registry - .adapter(&RadrootsTransportKind::Reticulum) - .expect("reticulum adapter"); - let receipt = adapter - .deliver( - RadrootsRuntimeTransportDispatchRequest::new( - "reticulum-delivery", - RadrootsRuntimeTransportPayload::DigestOnly("sha256:event".to_owned()), - vec![target( - RadrootsTransportKind::Reticulum, - "reticulum:preview-unavailable", - )], - RadrootsTransportSatisfactionPolicy::any_accepted(), - 1_000, - ) - .expect("request"), - ) + let transport = registry + .transport(&RadrootsTransportKind::Reticulum) + .expect("reticulum transport"); + let request = RadrootsRuntimeTransportDispatchRequest::new( + "reticulum-delivery", + RadrootsRuntimeTransportPayload::DigestOnly("sha256:event".to_owned()), + vec![target( + RadrootsTransportKind::Reticulum, + "reticulum:preview-unavailable", + )], + RadrootsTransportSatisfactionPolicy::any_accepted(), + 1_000, + ) + .expect("request"); + let receipt = transport + .deliver(request.transport_delivery_request()) .await .expect("receipt"); + let status = transport.status().await.expect("status"); + assert_eq!( + status.implementation, + RadrootsTransportImplementationState::PreviewUnavailable + ); + let fetch = transport + .fetch(RadrootsTransportFetchRequest::new( + "reticulum-fetch", + RadrootsTransportTargetSet::new(vec![target( + RadrootsTransportKind::Reticulum, + "reticulum:preview-unavailable", + )]) + .expect("target set"), + )) + .await + .expect("fetch"); + assert_eq!(fetch.fetched_count, 0); assert_eq!( receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), @@ -855,7 +878,7 @@ mod tests { async fn delivery_worker_dispatches_ready_targets_and_skips_deferred() { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry - .register(StaticAdapter { + .register(StaticTransport { kind: RadrootsTransportKind::Nostr, outcome_kind: RadrootsTransportOutcomeKind::Accepted, }) @@ -952,7 +975,7 @@ mod tests { async fn delivery_worker_required_targets_use_fingerprints() { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry - .register(StaticAdapter { + .register(StaticTransport { kind: RadrootsTransportKind::Nostr, outcome_kind: RadrootsTransportOutcomeKind::Accepted, }) @@ -1012,7 +1035,7 @@ mod tests { async fn delivery_worker_keeps_accepted_and_delivered_satisfaction_distinct() { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry - .register(StaticAdapter { + .register(StaticTransport { kind: RadrootsTransportKind::Nostr, outcome_kind: RadrootsTransportOutcomeKind::Accepted, }) @@ -1055,7 +1078,7 @@ mod tests { async fn delivery_worker_reports_quorum_satisfaction() { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry - .register(StaticAdapter { + .register(StaticTransport { kind: RadrootsTransportKind::Nostr, outcome_kind: RadrootsTransportOutcomeKind::Accepted, }) diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml @@ -20,6 +20,7 @@ serde = { workspace = true, optional = true } sha2 = { workspace = true, default-features = false } [dev-dependencies] +futures = { workspace = true } serde_json = { workspace = true, features = ["std"] } [lints.rust] diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs @@ -32,4 +32,5 @@ pub use target::{ }; pub use transport::{ RadrootsTransport, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest, + RadrootsTransportFuture, }; diff --git a/crates/transport/src/transport.rs b/crates/transport/src/transport.rs @@ -1,22 +1,31 @@ use crate::{ RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportError, - RadrootsTransportStatus, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, + RadrootsTransportKind, RadrootsTransportStatus, RadrootsTransportTargetReceipt, + RadrootsTransportTargetSet, }; +use alloc::boxed::Box; use alloc::string::String; use alloc::vec::Vec; +use core::future::Future; +use core::pin::Pin; -pub trait RadrootsTransport { - fn status(&self) -> Result<RadrootsTransportStatus, RadrootsTransportError>; +pub type RadrootsTransportFuture<'a, T> = + Pin<Box<dyn Future<Output = Result<T, RadrootsTransportError>> + Send + 'a>>; - fn deliver( - &self, +pub trait RadrootsTransport: Send + Sync { + fn transport_kind(&self) -> RadrootsTransportKind; + + fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus>; + + fn deliver<'a>( + &'a self, request: RadrootsTransportDeliveryRequest, - ) -> Result<RadrootsTransportDeliveryReceipt, RadrootsTransportError>; + ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt>; - fn fetch( - &self, + fn fetch<'a>( + &'a self, request: RadrootsTransportFetchRequest, - ) -> Result<RadrootsTransportFetchReceipt, RadrootsTransportError>; + ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt>; } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/transport/tests/source_boundary.rs b/crates/transport/tests/source_boundary.rs @@ -308,6 +308,45 @@ fn core_transport_sources_reject_relay_shaped_public_contracts() { } #[test] +fn runtime_transport_registry_uses_core_transport_contract() { + let crates_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("transport crate parent"); + let runtime_source_raw = read_source(crates_root.join("runtime/src/transport.rs").as_path()); + let runtime_source = production_source(runtime_source_raw.as_str()); + let reticulum_source_raw = + read_source(crates_root.join("transport_reticulum/src/lib.rs").as_path()); + let reticulum_source = production_source(reticulum_source_raw.as_str()); + + assert!( + runtime_source.contains("Arc<dyn RadrootsTransport>"), + "runtime registry must store the core RadrootsTransport trait object" + ); + assert!( + runtime_source.contains("T: RadrootsTransport + 'static"), + "runtime registry registration must accept the core RadrootsTransport trait" + ); + assert!( + runtime_source.contains("transport.transport_kind()"), + "runtime registry must key transports through the core trait transport_kind" + ); + for forbidden in [ + "pub trait RadrootsRuntimeTransportAdapter", + "dyn RadrootsRuntimeTransportAdapter", + "RadrootsRuntimeReticulumPreviewTransport", + ] { + assert!( + !runtime_source.contains(forbidden), + "runtime transport source must not retain split adapter contract `{forbidden}`" + ); + } + assert!( + reticulum_source.contains("impl RadrootsTransport for RadrootsReticulumPreviewTransport"), + "Reticulum preview transport must implement the core transport contract" + ); +} + +#[test] fn transport_publish_capabilities_keep_canonical_status_fields() { let source_raw = read_source( Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/crates/transport/tests/transport.rs b/crates/transport/tests/transport.rs @@ -2,12 +2,12 @@ use radroots_transport::{ RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, RadrootsTransport, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, - RadrootsTransportFetchRequest, RadrootsTransportImplementationState, RadrootsTransportKind, - RadrootsTransportMeshScopeId, RadrootsTransportOutcome, RadrootsTransportOutcomeKind, - RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, - RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, - RadrootsTransportTargetLabel, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, - RadrootsTransportTargetUri, + RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, + RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome, + RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass, + RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget, + RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, + RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, RadrootsTransportTargetUri, }; #[test] @@ -812,41 +812,51 @@ fn neutral_transport_trait_covers_status_delivery_and_fetch() { } impl RadrootsTransport for MemoryTransport { - fn status(&self) -> Result<RadrootsTransportStatus, RadrootsTransportError> { - Ok(RadrootsTransportStatus::new( - RadrootsTransportKind::Local, - true, - RadrootsTransportImplementationState::Real, - true, - "ready", - )) + fn transport_kind(&self) -> RadrootsTransportKind { + RadrootsTransportKind::Local + } + + fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { + Box::pin(async move { + Ok(RadrootsTransportStatus::new( + RadrootsTransportKind::Local, + true, + RadrootsTransportImplementationState::Real, + true, + "ready", + )) + }) } - fn deliver( - &self, + fn deliver<'a>( + &'a self, request: RadrootsTransportDeliveryRequest, - ) -> Result<RadrootsTransportDeliveryReceipt, RadrootsTransportError> { - Ok(RadrootsTransportDeliveryReceipt { - request_id: request.request_id, - target_receipts: vec![RadrootsTransportTargetReceipt::new( - self.target.clone(), - RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Delivered), - )], + ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { + Box::pin(async move { + Ok(RadrootsTransportDeliveryReceipt { + request_id: request.request_id, + target_receipts: vec![RadrootsTransportTargetReceipt::new( + self.target.clone(), + RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Delivered), + )], + }) }) } - fn fetch( - &self, + fn fetch<'a>( + &'a self, request: RadrootsTransportFetchRequest, - ) -> Result<RadrootsTransportFetchReceipt, RadrootsTransportError> { - Ok(RadrootsTransportFetchReceipt::new( - request.request_id, - vec![RadrootsTransportTargetReceipt::new( - self.target.clone(), - RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Seen), - )], - 1, - )) + ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { + Box::pin(async move { + Ok(RadrootsTransportFetchReceipt::new( + request.request_id, + vec![RadrootsTransportTargetReceipt::new( + self.target.clone(), + RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Seen), + )], + 1, + )) + }) } } @@ -854,23 +864,25 @@ fn neutral_transport_trait_covers_status_delivery_and_fetch() { .expect("local target"); let target_set = RadrootsTransportTargetSet::new(vec![target.clone()]).expect("target set"); let transport = MemoryTransport { target }; - let status = transport.status().expect("status"); + assert_eq!(transport.transport_kind(), RadrootsTransportKind::Local); + let status = futures::executor::block_on(transport.status()).expect("status"); assert_eq!(status.kind, RadrootsTransportKind::Local); - let delivery = transport - .deliver(RadrootsTransportDeliveryRequest::new( + let delivery = + futures::executor::block_on(transport.deliver(RadrootsTransportDeliveryRequest::new( "deliver-1", "sha256:payload", target_set.clone(), RadrootsTransportSatisfactionPolicy::all_delivered(), - )) + ))) .expect("deliver"); assert_eq!( delivery.target_receipts[0].outcome.kind, RadrootsTransportOutcomeKind::Delivered ); - let fetch = transport - .fetch(RadrootsTransportFetchRequest::new("fetch-1", target_set)) - .expect("fetch"); + let fetch = futures::executor::block_on( + transport.fetch(RadrootsTransportFetchRequest::new("fetch-1", target_set)), + ) + .expect("fetch"); assert_eq!(fetch.fetched_count, 1); assert_eq!( fetch.target_receipts[0].outcome.kind, diff --git a/crates/transport_reticulum/Cargo.toml b/crates/transport_reticulum/Cargo.toml @@ -20,6 +20,7 @@ radroots_transport = { workspace = true, default-features = false } serde = { workspace = true, optional = true, features = ["derive"] } [dev-dependencies] +futures = { workspace = true } serde_json = { workspace = true, features = ["std"] } [lints.rust] diff --git a/crates/transport_reticulum/src/lib.rs b/crates/transport_reticulum/src/lib.rs @@ -4,13 +4,15 @@ extern crate alloc; use alloc::borrow::ToOwned; +use alloc::boxed::Box; use alloc::string::String; use alloc::vec::Vec; use core::fmt; use radroots_transport::{ RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, - RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, - RadrootsTransportDeliveryTargetStatus, RadrootsTransportImplementationState, + RadrootsTransport, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, + RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, + RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetReceipt, @@ -282,6 +284,49 @@ impl Default for RadrootsReticulumPreviewTransport { } } +impl RadrootsTransport for RadrootsReticulumPreviewTransport { + fn transport_kind(&self) -> RadrootsTransportKind { + RadrootsTransportKind::Reticulum + } + + fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { + Box::pin(async move { Ok(self.profile.status().transport_status) }) + } + + fn deliver<'a>( + &'a self, + request: RadrootsTransportDeliveryRequest, + ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { + Box::pin(async move { + self.deliver(request) + .map_err(reticulum_preview_error_to_transport_error) + }) + } + + fn fetch<'a>( + &'a self, + request: RadrootsTransportFetchRequest, + ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { + Box::pin(async move { + ensure_reticulum_targets(request.target_set.targets()) + .map_err(reticulum_preview_error_to_transport_error)?; + let outcome = preview_outcome(self.profile.behavior); + let target_receipts = request + .target_set + .targets() + .iter() + .cloned() + .map(|target| RadrootsTransportTargetReceipt::new(target, outcome.clone())) + .collect::<Vec<_>>(); + Ok(RadrootsTransportFetchReceipt::new( + request.request_id, + target_receipts, + 0, + )) + }) + } +} + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsReticulumPreviewFetchRequest { @@ -339,6 +384,22 @@ impl fmt::Display for RadrootsReticulumPreviewError { } } +fn reticulum_preview_error_to_transport_error( + error: RadrootsReticulumPreviewError, +) -> RadrootsTransportError { + match error { + RadrootsReticulumPreviewError::InvalidEndpoint + | RadrootsReticulumPreviewError::NonReticulumTarget => { + RadrootsTransportError::InvalidTargetUri + } + RadrootsReticulumPreviewError::InvalidAgentEndpoint + | RadrootsReticulumPreviewError::InvalidProfileId + | RadrootsReticulumPreviewError::InvalidFetchLimit => { + RadrootsTransportError::InvalidTransportKind + } + } +} + fn ensure_reticulum_targets( targets: &[RadrootsTransportTarget], ) -> Result<(), RadrootsReticulumPreviewError> { diff --git a/crates/transport_reticulum/tests/reticulum_preview.rs b/crates/transport_reticulum/tests/reticulum_preview.rs @@ -1,9 +1,10 @@ use radroots_transport::{ RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, - RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportDeliveryRequest, - RadrootsTransportDeliveryTargetStatus, RadrootsTransportImplementationState, - RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportSatisfactionClass, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetSet, + RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransport, RadrootsTransportDeliveryRequest, + RadrootsTransportDeliveryTargetStatus, RadrootsTransportFetchRequest, + RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportMeshScopeId, + RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, + RadrootsTransportTarget, RadrootsTransportTargetSet, }; use radroots_transport_reticulum::{ RadrootsReticulumPreviewAgentEndpoint, RadrootsReticulumPreviewBehavior, @@ -273,6 +274,49 @@ fn direct_preview_delivery_accepts_any_typed_reticulum_scope_as_inert_metadata() } #[test] +fn core_transport_trait_reports_preview_status_delivery_and_fetch() { + let transport = RadrootsReticulumPreviewTransport::default(); + let target_set = RadrootsTransportTargetSet::new(vec![reticulum_target( + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, + )]) + .expect("target set"); + let status = futures::executor::block_on(RadrootsTransport::status(&transport)) + .expect("transport status"); + assert_eq!(status.kind, RadrootsTransportKind::Reticulum); + assert_eq!( + status.implementation, + RadrootsTransportImplementationState::PreviewUnavailable + ); + assert!(!status.usable_for_delivery); + + let delivery = futures::executor::block_on(RadrootsTransport::deliver( + &transport, + RadrootsTransportDeliveryRequest::new( + "core-delivery", + "sha256:preview-payload", + target_set.clone(), + RadrootsTransportSatisfactionPolicy::any_accepted(), + ), + )) + .expect("delivery receipt"); + assert_eq!( + delivery.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::PreviewUnavailable + ); + + let fetch = futures::executor::block_on(RadrootsTransport::fetch( + &transport, + RadrootsTransportFetchRequest::new("core-fetch", target_set), + )) + .expect("fetch receipt"); + assert_eq!(fetch.fetched_count, 0); + assert_eq!( + fetch.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::PreviewUnavailable + ); +} + +#[test] fn reject_delivery_attempts_returns_unavailable_without_success_or_nostr_routing() { let transport = RadrootsReticulumPreviewTransport::default(); let request = delivery_request(vec![reticulum_target(