lib

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

commit 273a24aafa6211b0fa60bc88647d4a57e0edc2b8
parent 3166eee6d9f4c71916ee6db9bc49cbaac6139538
Author: triesap <tyson@radroots.org>
Date:   Wed,  8 Jul 2026 07:10:23 +0000

transport: expose neutral sync status

Diffstat:
Mcrates/sdk/src/lib.rs | 3++-
Mcrates/sdk/src/sync_runtime.rs | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mcrates/sdk/src/transport.rs | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/sdk/tests/sync_runtime.rs | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
4 files changed, 303 insertions(+), 25 deletions(-)

diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -221,7 +221,8 @@ pub use crate::sync_runtime::{ PushOutboxTargetReceipt, SYNC_PROJECTION_REFRESH_DEFAULT_LIMIT, SYNC_PROJECTION_REFRESH_MAX_LIMIT, SdkRelayAuthPolicy, SyncEventStoreStatus, SyncOutboxStatus, SyncProjectionRefreshReceipt, SyncProjectionRefreshRequest, SyncStatusReceipt, - SyncStatusRequest, SyncStatusSource, SyncTransportProfileSummary, + SyncStatusRequest, SyncStatusSource, SyncTransportProfileSummary, SyncTransportStatusSummary, + SyncTransportTargetSummary, }; #[cfg(feature = "runtime")] pub use crate::trade_storage::{ diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -28,10 +28,13 @@ use radroots_trade::projection::{ RadrootsProjectionRefreshReceipt, RadrootsProjectionRefreshRequest, refresh_product_projections, }; -#[cfg(feature = "runtime")] -use radroots_transport::RadrootsTransportKind; #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] use radroots_transport::RadrootsTransportSatisfactionPolicy; +#[cfg(feature = "runtime")] +use radroots_transport::{ + RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportReadinessState, + RadrootsTransportStatus, RadrootsTransportTarget, +}; #[cfg(all(feature = "runtime", feature = "relay-runtime"))] use radroots_transport_nostr::RadrootsNostrClientPublishAdapter; #[cfg(feature = "runtime")] @@ -154,8 +157,101 @@ impl From<RadrootsOutboxStatusSummary> for SyncOutboxStatus { #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub struct SyncTransportProfileSummary { pub transport_profile_id: String, - pub configured_nostr_relay_count: usize, - pub configured_nostr_relays: Vec<String>, + pub configured_transport_target_count: usize, + pub configured_transport_targets: Vec<SyncTransportTargetSummary>, + pub transport_statuses: Vec<SyncTransportStatusSummary>, +} + +#[cfg(feature = "runtime")] +impl SyncTransportProfileSummary { + fn from_transport_profile(profile: &TransportProfile) -> Result<Self, RadrootsSdkError> { + let configured_transport_targets = profile + .configured_transport_targets()? + .iter() + .map(SyncTransportTargetSummary::from_transport_target) + .collect::<Vec<_>>(); + Ok(Self { + transport_profile_id: profile.transport_profile_id().to_owned(), + configured_transport_target_count: configured_transport_targets.len(), + configured_transport_targets, + transport_statuses: profile + .transport_statuses() + .into_iter() + .map(SyncTransportStatusSummary::from_transport_status) + .collect(), + }) + } +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct SyncTransportTargetSummary { + pub transport_kind: String, + pub endpoint_uri: String, + pub endpoint_fingerprint: String, +} + +#[cfg(feature = "runtime")] +impl SyncTransportTargetSummary { + fn from_transport_target(target: &RadrootsTransportTarget) -> Self { + Self { + transport_kind: target.kind.canonical_label(), + endpoint_uri: target.uri.as_str().to_owned(), + endpoint_fingerprint: target.fingerprint.as_str().to_owned(), + } + } +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct SyncTransportStatusSummary { + pub transport_kind: String, + pub profile_id: Option<String>, + pub endpoint_uri: Option<String>, + pub implementation_state: String, + pub readiness: String, + pub publish_usable: bool, + pub fetch_usable: bool, + pub redacted_message: Option<String>, +} + +#[cfg(feature = "runtime")] +impl SyncTransportStatusSummary { + fn from_transport_status(status: RadrootsTransportStatus) -> Self { + Self { + transport_kind: status.kind.canonical_label(), + profile_id: status.profile_id, + endpoint_uri: status.endpoint_uri, + implementation_state: transport_implementation_state_label(status.implementation_state) + .to_owned(), + readiness: transport_readiness_state_label(status.readiness).to_owned(), + publish_usable: status.publish_usable, + fetch_usable: status.fetch_usable, + redacted_message: status.redacted_message, + } + } +} + +#[cfg(feature = "runtime")] +fn transport_implementation_state_label( + state: RadrootsTransportImplementationState, +) -> &'static str { + match state { + RadrootsTransportImplementationState::Available => "available", + RadrootsTransportImplementationState::Disabled => "disabled", + RadrootsTransportImplementationState::Misconfigured => "misconfigured", + RadrootsTransportImplementationState::PreviewUnavailable => "preview_unavailable", + } +} + +#[cfg(feature = "runtime")] +fn transport_readiness_state_label(state: RadrootsTransportReadinessState) -> &'static str { + match state { + RadrootsTransportReadinessState::Ready => "ready", + RadrootsTransportReadinessState::Disabled => "disabled", + RadrootsTransportReadinessState::Misconfigured => "misconfigured", + RadrootsTransportReadinessState::PreviewUnavailable => "preview_unavailable", + } } #[cfg(feature = "runtime")] @@ -492,15 +588,9 @@ impl<'sdk> SyncClient<'sdk> { observed_at_ms, event_store: event_store.into(), outbox: outbox.into(), - transport_profile: SyncTransportProfileSummary { - transport_profile_id: self - .sdk - .transport_profile() - .transport_profile_id() - .to_owned(), - configured_nostr_relay_count: self.sdk.configured_nostr_relay_urls().len(), - configured_nostr_relays: self.sdk.configured_nostr_relay_urls(), - }, + transport_profile: SyncTransportProfileSummary::from_transport_profile( + self.sdk.transport_profile(), + )?, }) } diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -1,8 +1,10 @@ use crate::RadrootsSdkError; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RadrootsTransportDeliveryReceipt, - RadrootsTransportKind, RadrootsTransportSatisfactionClass, RadrootsTransportTarget, - RadrootsTransportTargetFingerprint, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + RadrootsTransportDeliveryReceipt, RadrootsTransportImplementationState, RadrootsTransportKind, + RadrootsTransportReadinessState, RadrootsTransportSatisfactionClass, RadrootsTransportStatus, + RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetReceipt, + RadrootsTransportTargetSet, }; use radroots_transport_nostr::{RadrootsRelayUrl, RadrootsRelayUrlPolicy}; use serde::ser::{SerializeStruct, Serializer}; @@ -497,6 +499,63 @@ impl TransportProfile { } } + pub(crate) fn configured_transport_targets( + &self, + ) -> Result<Vec<RadrootsTransportTarget>, RadrootsSdkError> { + Ok(self + .target_set()? + .map(TargetSet::into_targets) + .unwrap_or_default()) + } + + pub(crate) fn transport_statuses(&self) -> Vec<RadrootsTransportStatus> { + match self { + Self::LocalOnly => vec![ + RadrootsTransportStatus::new( + RadrootsTransportKind::Local, + RadrootsTransportImplementationState::Available, + RadrootsTransportReadinessState::Ready, + ) + .with_profile_id(self.transport_profile_id()), + ], + Self::Nostr { .. } => vec![nostr_transport_status(self.transport_profile_id())], + Self::ReticulumPreview { profile } => { + vec![reticulum_preview_transport_status( + self.transport_profile_id(), + profile.endpoint_uri(), + )] + } + Self::Hybrid { profile } => vec![ + nostr_transport_status(self.transport_profile_id()), + reticulum_preview_transport_status( + self.transport_profile_id(), + profile.reticulum_preview().endpoint_uri(), + ), + ], + Self::Proxy { profile } => { + let auth_configured = matches!(profile.auth(), ProxyAuth::BearerToken(_)); + vec![ + RadrootsTransportStatus::new( + RadrootsTransportKind::Proxy, + if auth_configured { + RadrootsTransportImplementationState::Available + } else { + RadrootsTransportImplementationState::Misconfigured + }, + if auth_configured { + RadrootsTransportReadinessState::Ready + } else { + RadrootsTransportReadinessState::Misconfigured + }, + ) + .with_profile_id(self.transport_profile_id()) + .with_endpoint_uri(profile.endpoint_url()) + .with_publish_usable(auth_configured), + ] + } + } + } + pub(crate) fn configured_nostr_relay_urls(&self) -> Vec<String> { match self { Self::Nostr { profile } => profile.relay_urls(), @@ -506,6 +565,31 @@ impl TransportProfile { } } +fn nostr_transport_status(profile_id: &str) -> RadrootsTransportStatus { + RadrootsTransportStatus::new( + RadrootsTransportKind::Nostr, + RadrootsTransportImplementationState::Available, + RadrootsTransportReadinessState::Ready, + ) + .with_profile_id(profile_id) + .with_publish_usable(true) + .with_fetch_usable(true) +} + +fn reticulum_preview_transport_status( + profile_id: &str, + endpoint_uri: &str, +) -> RadrootsTransportStatus { + RadrootsTransportStatus::new( + RadrootsTransportKind::Reticulum, + RadrootsTransportImplementationState::PreviewUnavailable, + RadrootsTransportReadinessState::PreviewUnavailable, + ) + .with_profile_id(profile_id) + .with_endpoint_uri(endpoint_uri) + .with_redacted_message(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE) +} + impl From<RadrootsTransportDeliveryReceipt> for TransportReceipt { fn from(value: RadrootsTransportDeliveryReceipt) -> Self { Self { diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs @@ -23,8 +23,8 @@ use radroots_outbox::{ #[cfg(feature = "radrootsd-proxy")] use radroots_sdk::ProxyProfile; use radroots_sdk::{ - BackupRequest, IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, ListingEnqueuePublishRequest, - ListingPreparePublishRequest, NostrProfile, NostrRelayUrlPolicy, + BackupRequest, HybridProfile, IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, + ListingEnqueuePublishRequest, ListingPreparePublishRequest, NostrProfile, NostrRelayUrlPolicy, PUSH_OUTBOX_DEFAULT_CLAIM_TTL_MS, PUSH_OUTBOX_DEFAULT_LIMIT, PUSH_OUTBOX_DEFAULT_NEXT_ATTEMPT_DELAY_MS, PUSH_OUTBOX_MAX_LIMIT, PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRequest, PushOutboxTargetOutcomeKind, @@ -497,6 +497,22 @@ async fn reticulum_preview_directory_sdk( (tempdir, sdk) } +async fn hybrid_directory_sdk(relays: &[&str]) -> (tempfile::TempDir, RadrootsClient) { + let tempdir = tempfile::tempdir().expect("tempdir"); + let sdk = RadrootsClient::builder() + .directory_storage(tempdir.path().join("sdk")) + .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) + .transport_profile(TransportProfile::hybrid(HybridProfile::new( + NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + ReticulumPreviewProfile::preview_unavailable(), + ))) + .build() + .await + .expect("sdk"); + (tempdir, sdk) +} + async fn system_clock_directory_sdk(relays: &[&str]) -> (tempfile::TempDir, RadrootsClient) { let tempdir = tempfile::tempdir().expect("tempdir"); let mut builder = RadrootsClient::builder().directory_storage(tempdir.path().join("sdk")); @@ -576,7 +592,7 @@ async fn enqueue_listing_with_policy( } #[tokio::test] -async fn sync_status_empty_store_reports_canonical_sources_and_configured_relays() { +async fn sync_status_empty_store_reports_canonical_sources_and_transport_targets() { let (_tempdir, sdk) = directory_sdk(&[RELAY_B, RELAY_A]).await; let receipt = sdk @@ -598,10 +614,27 @@ async fn sync_status_empty_store_reports_canonical_sources_and_configured_relays assert_eq!(receipt.outbox.failed_terminal_events, 0); assert_eq!(receipt.outbox.ready_signed_events, 0); assert_eq!(receipt.transport_profile.transport_profile_id, "nostr"); - assert_eq!(receipt.transport_profile.configured_nostr_relay_count, 2); assert_eq!( - receipt.transport_profile.configured_nostr_relays, - vec![RELAY_B.to_owned(), RELAY_A.to_owned()] + receipt.transport_profile.configured_transport_target_count, + 2 + ); + assert_eq!( + receipt + .transport_profile + .configured_transport_targets + .iter() + .map(|target| target.endpoint_uri.as_str()) + .collect::<Vec<_>>(), + vec![RELAY_B, RELAY_A] + ); + assert_eq!(receipt.transport_profile.transport_statuses.len(), 1); + assert_eq!( + receipt.transport_profile.transport_statuses[0].transport_kind, + "nostr" + ); + assert_eq!( + receipt.transport_profile.transport_statuses[0].readiness, + "ready" ); assert_eq!( serde_json::to_value(&receipt).expect("status json"), @@ -630,14 +663,84 @@ async fn sync_status_empty_store_reports_canonical_sources_and_configured_relays }, "transport_profile": { "transport_profile_id": "nostr", - "configured_nostr_relay_count": 2, - "configured_nostr_relays": [RELAY_B, RELAY_A] + "configured_transport_target_count": 2, + "configured_transport_targets": [ + { + "transport_kind": "nostr", + "endpoint_uri": RELAY_B, + "endpoint_fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05" + }, + { + "transport_kind": "nostr", + "endpoint_uri": RELAY_A, + "endpoint_fingerprint": "fc957b234632cc52e2be19cba88bc85c69966ee5a2df61742b5875ff717fd6fa" + } + ], + "transport_statuses": [{ + "transport_kind": "nostr", + "profile_id": "nostr", + "endpoint_uri": null, + "implementation_state": "available", + "readiness": "ready", + "publish_usable": true, + "fetch_usable": true, + "redacted_message": null + }] } }) ); } #[tokio::test] +async fn sync_status_reports_hybrid_transport_targets_and_statuses() { + let (_tempdir, sdk) = hybrid_directory_sdk(&[RELAY_A, RELAY_B]).await; + + let receipt = sdk + .sync() + .status(SyncStatusRequest::new()) + .await + .expect("status"); + + assert_eq!(receipt.transport_profile.transport_profile_id, "hybrid"); + assert_eq!( + receipt.transport_profile.configured_transport_target_count, + 3 + ); + assert_eq!( + receipt + .transport_profile + .configured_transport_targets + .iter() + .map(|target| { (target.transport_kind.as_str(), target.endpoint_uri.as_str(),) }) + .collect::<Vec<_>>(), + vec![ + ("nostr", RELAY_A), + ("nostr", RELAY_B), + ("reticulum", "reticulum:preview-unavailable") + ] + ); + assert_eq!( + receipt + .transport_profile + .transport_statuses + .iter() + .map(|status| { + ( + status.transport_kind.as_str(), + status.readiness.as_str(), + status.publish_usable, + status.fetch_usable, + ) + }) + .collect::<Vec<_>>(), + vec![ + ("nostr", "ready", true, true), + ("reticulum", "preview_unavailable", false, false) + ] + ); +} + +#[tokio::test] async fn sync_status_reports_pending_retryable_terminal_and_last_attempt_metadata() { let (_tempdir, sdk) = directory_sdk(&[RELAY_A, RELAY_B, RELAY_C]).await; enqueue_listing(&sdk, LISTING_A_D_TAG, "Retryable Coffee", &[RELAY_A]).await;