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:
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(