lib

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

commit 958b9f6c1c056fe4a8a73ad80b3da69d0f50fda8
parent 2d157f928e7cccad7a60d060718db448ec5905e8
Author: triesap <tyson@radroots.org>
Date:   Thu,  9 Jul 2026 04:17:40 +0000

sync: report Reticulum preview push receipts

- Return Reticulum preview push receipts with zero attempted events.
- Map preview and deferred outbox records into typed event and target outcomes.
- Remove the unused Reticulum push error constructor and update receipt tests.
- Validate with extbuild fmt, SDK check, source-boundary, and focused sync test.

Diffstat:
Mcrates/sdk/src/error.rs | 12------------
Mcrates/sdk/src/sync_runtime.rs | 177++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/sdk/tests/source_boundary.rs | 16+++++++++++++---
Mcrates/sdk/tests/sync_runtime.rs | 84++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mcrates/sdk/tests/unit/error_tests.rs | 20++++++++++----------
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 8++++----
6 files changed, 197 insertions(+), 120 deletions(-)

diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -560,18 +560,6 @@ impl RadrootsSdkError { } } - pub(crate) fn reticulum_preview_transport_unavailable( - operation: impl Into<String>, - endpoint_uri: impl Into<String>, - behavior: ReticulumPreviewBehavior, - ) -> Self { - Self::ReticulumPreviewTransportUnavailable { - operation: operation.into(), - endpoint_uri: endpoint_uri.into(), - behavior, - } - } - pub(crate) fn trade_status_limit_invalid(limit: u32, min: u32, max: u32) -> Self { Self::TradeStatusLimitInvalid { limit, min, max } } diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -16,13 +16,14 @@ use radroots_events::ids::RadrootsEventId; #[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))] use radroots_nostr::prelude::RadrootsNostrClient; #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +use radroots_outbox::RadrootsOutboxClaimedEvent; +#[cfg(feature = "runtime")] use radroots_outbox::{ - RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryTargetRecord, - RadrootsOutboxDeliveryTargetStatus, + RadrootsOutboxDeliveryTargetRecord, RadrootsOutboxDeliveryTargetStatus, + RadrootsOutboxEventState, RadrootsOutboxReticulumPreviewEventRecord, + RadrootsOutboxStatusSummary, }; #[cfg(feature = "runtime")] -use radroots_outbox::{RadrootsOutboxEventState, RadrootsOutboxStatusSummary}; -#[cfg(feature = "runtime")] use radroots_trade::projection::{ RADROOTS_PRODUCT_PROJECTION_ID, RADROOTS_PRODUCT_PROJECTION_VERSION, RadrootsProjectionRefreshReceipt, RadrootsProjectionRefreshRequest, @@ -32,8 +33,9 @@ use radroots_trade::projection::{ use radroots_transport::RadrootsTransportSatisfactionPolicy; #[cfg(feature = "runtime")] use radroots_transport::{ - RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportReadinessState, - RadrootsTransportStatus, RadrootsTransportTarget, + RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportImplementationState, + RadrootsTransportKind, RadrootsTransportReadinessState, RadrootsTransportStatus, + RadrootsTransportTarget, }; #[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))] use radroots_transport_nostr::RadrootsNostrClientPublishAdapter; @@ -442,8 +444,12 @@ pub struct PushOutboxReceipt { #[cfg(feature = "runtime")] impl PushOutboxReceipt { - fn push_event(&mut self, event: PushOutboxEventReceipt) { + fn push_attempted_event(&mut self, event: PushOutboxEventReceipt) { self.attempted_events += 1; + self.push_reported_event(event); + } + + fn push_reported_event(&mut self, event: PushOutboxEventReceipt) { match event.final_state { PushOutboxEventState::Published => self.published_events += 1, PushOutboxEventState::PublishRetryable => self.retryable_events += 1, @@ -632,18 +638,8 @@ impl<'sdk> SyncClient<'sdk> { required_feature: "delivery-capable transport profile", }) } - TransportProfile::ReticulumPreview { profile } => { - if self - .push_outbox_has_no_reticulum_preview_work(&request) - .await? - { - return Ok(PushOutboxReceipt::default()); - } - Err(RadrootsSdkError::reticulum_preview_transport_unavailable( - "sync.push_outbox", - profile.endpoint_uri(), - profile.behavior(), - )) + TransportProfile::ReticulumPreview { .. } => { + self.reticulum_preview_push_receipt(request).await } } } @@ -658,37 +654,6 @@ impl<'sdk> SyncClient<'sdk> { Ok(summary.ready_signed_events == 0) } - async fn push_outbox_has_no_reticulum_preview_work( - &self, - request: &PushOutboxRequest, - ) -> Result<bool, RadrootsSdkError> { - request.validate()?; - if let Some(outbox_event_id) = request.outbox_event_id { - let Some(event) = self.sdk._outbox.get_event(outbox_event_id).await? else { - return Ok(true); - }; - if !matches!( - event.state, - RadrootsOutboxEventState::Signed | RadrootsOutboxEventState::PublishRetryable - ) || event.signed_event.is_none() - { - return Ok(true); - } - let targets = self.sdk._outbox.delivery_targets(outbox_event_id).await?; - return Ok(!targets.iter().any(|target| { - target.transport_kind == RadrootsTransportKind::Reticulum - && (target.status.is_deferred_preview() || target.status.is_ready_for_attempt()) - })); - } - let now_ms = sdk_now_ms(self.sdk)?; - let summary = self.sdk._outbox.status_summary(now_ms).await?; - Ok(summary.ready_signed_events == 0 - && summary.pending_events == 0 - && summary.retryable_events == 0 - && summary.preview_unavailable_events == 0 - && summary.deferred_until_implemented_events == 0) - } - pub async fn push_outbox_with_adapter<A>( &self, adapter: &A, @@ -733,7 +698,7 @@ impl<'sdk> SyncClient<'sdk> { publish_now_ms, ) .await?; - receipt.push_event(push_event_receipt( + receipt.push_attempted_event(push_event_receipt( claimed.outbox_event_id, push_event_final_state(&publish.publish), publish.publish, @@ -778,7 +743,24 @@ impl<'sdk> SyncClient<'sdk> { publish_now_ms, ) .await?; - receipt.push_event(publish); + receipt.push_attempted_event(publish); + } + Ok(receipt) + } + + async fn reticulum_preview_push_receipt( + &self, + request: PushOutboxRequest, + ) -> Result<PushOutboxReceipt, RadrootsSdkError> { + request.validate()?; + let records = self + .sdk + ._outbox + .reticulum_preview_events(request.outbox_event_id, request.limit) + .await?; + let mut receipt = PushOutboxReceipt::default(); + for record in records { + receipt.push_reported_event(reticulum_preview_event_receipt(record)?); } Ok(receipt) } @@ -836,6 +818,97 @@ async fn claim_ready_signed_event_for_push( } #[cfg(feature = "runtime")] +fn reticulum_preview_event_receipt( + record: RadrootsOutboxReticulumPreviewEventRecord, +) -> Result<PushOutboxEventReceipt, RadrootsSdkError> { + let event_id = push_receipt_event_id( + record.event.event_id.as_str(), + "Reticulum preview outbox event id", + )?; + let final_state = reticulum_preview_event_final_state(record.event.state, &record.targets); + let quorum = record.targets.len(); + Ok(PushOutboxEventReceipt { + event_id, + outbox_event_id: record.event.outbox_event_id, + final_state, + attempted_count: 0, + accepted_count: 0, + retryable_count: 0, + terminal_count: 0, + quorum, + quorum_met: false, + targets: record + .targets + .into_iter() + .map(reticulum_preview_target_receipt) + .collect(), + }) +} + +#[cfg(feature = "runtime")] +fn reticulum_preview_event_final_state( + event_state: RadrootsOutboxEventState, + targets: &[RadrootsOutboxDeliveryTargetRecord], +) -> PushOutboxEventState { + if event_state == RadrootsOutboxEventState::PreviewUnavailable + || targets.iter().any(|target| { + matches!( + target.status, + RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + | RadrootsOutboxDeliveryTargetStatus::Pending + | RadrootsOutboxDeliveryTargetStatus::FailedRetryable + ) + }) + { + PushOutboxEventState::PreviewUnavailable + } else { + PushOutboxEventState::DeferredUntilImplemented + } +} + +#[cfg(feature = "runtime")] +fn reticulum_preview_target_receipt( + target: RadrootsOutboxDeliveryTargetRecord, +) -> PushOutboxTargetReceipt { + PushOutboxTargetReceipt { + transport_kind: target.transport_kind.canonical_label(), + endpoint_uri: target.endpoint_uri.as_str().to_owned(), + outcome_kind: reticulum_preview_target_outcome_kind(target.status), + attempted: false, + message: Some( + target + .last_error + .unwrap_or_else(|| RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()), + ), + } +} + +#[cfg(feature = "runtime")] +fn reticulum_preview_target_outcome_kind( + status: RadrootsOutboxDeliveryTargetStatus, +) -> PushOutboxTargetOutcomeKind { + match status { + RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented => { + PushOutboxTargetOutcomeKind::DeferredUntilImplemented + } + RadrootsOutboxDeliveryTargetStatus::Pending + | RadrootsOutboxDeliveryTargetStatus::FailedRetryable + | RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable => { + PushOutboxTargetOutcomeKind::PreviewUnavailable + } + RadrootsOutboxDeliveryTargetStatus::Accepted + | RadrootsOutboxDeliveryTargetStatus::Delivered + | RadrootsOutboxDeliveryTargetStatus::Forwarded + | RadrootsOutboxDeliveryTargetStatus::StoredByGateway + | RadrootsOutboxDeliveryTargetStatus::Seen + | RadrootsOutboxDeliveryTargetStatus::SkippedPolicyDenied + | RadrootsOutboxDeliveryTargetStatus::FailedTerminal => { + PushOutboxTargetOutcomeKind::Unknown + } + } +} + +#[cfg(feature = "runtime")] pub(crate) async fn refresh_product_projections_for_sdk( sdk: &RadrootsClient, request: SyncProjectionRefreshRequest, diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -1605,15 +1605,25 @@ fn sdk_transport_sources_keep_reticulum_preview_push_boundary() { let sync_runtime = read_source(manifest_dir.join("src/sync_runtime.rs").as_path()); for required in [ - "TransportProfile::ReticulumPreview { profile }", - "push_outbox_has_no_reticulum_preview_work", - "RadrootsSdkError::reticulum_preview_transport_unavailable", + "TransportProfile::ReticulumPreview { .. }", + "reticulum_preview_push_receipt", + "reticulum_preview_event_receipt", + "push_reported_event", + "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE", ] { assert!( sync_runtime.contains(required), "src/sync_runtime.rs must retain Reticulum preview push boundary `{required}`" ); } + assert!( + !sync_runtime.contains("push_outbox_has_no_reticulum_preview_work"), + "src/sync_runtime.rs must not revive the Reticulum preview ready-work error probe" + ); + assert!( + !sync_runtime.contains("RadrootsSdkError::reticulum_preview_transport_unavailable(\n"), + "src/sync_runtime.rs must not return Reticulum preview unavailable errors from push_outbox" + ); let error_source = read_source(manifest_dir.join("src/error.rs").as_path()); for required in [ diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs @@ -28,11 +28,12 @@ use radroots_sdk::{ 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, - PushOutboxTargetReceipt, RadrootsClient, RadrootsSdkError, RadrootsSdkErrorClass, - RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, RestoreRequest, ReticulumPreviewBehavior, - ReticulumPreviewProfile, SdkBackupManifestKind, SdkRelayAuthPolicy, SdkRestoreState, - StorageStatusRequest, SyncStatusRequest, SyncStatusSource, TargetPolicy, TransportProfile, + PushOutboxTargetReceipt, RadrootsClient, RadrootsSdkError, RadrootsSdkTimestamp, + RestoreRequest, ReticulumPreviewBehavior, ReticulumPreviewProfile, SdkBackupManifestKind, + SdkRelayAuthPolicy, SdkRestoreState, StorageStatusRequest, SyncStatusRequest, SyncStatusSource, + TargetPolicy, TransportProfile, }; +use radroots_transport::RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE; use radroots_transport_nostr::{ RadrootsMockRelayPublishAdapter, RadrootsRelayOutcome, RadrootsRelayPublishAdapter, RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTransportError, @@ -2143,19 +2144,22 @@ async fn product_push_outbox_empty_queue_does_not_require_builder_relays() { } #[tokio::test] -async fn sync_runtime_product_push_outbox_reticulum_preview_reports_specific_ready_work_error() { +async fn sync_runtime_product_push_outbox_reticulum_preview_reports_zero_attempts_with_preview_work() + { let cases = [ ( ReticulumPreviewBehavior::RejectDeliveryAttempts, - "reticulum_preview_transport_unavailable", + PushOutboxEventState::PreviewUnavailable, + PushOutboxTargetOutcomeKind::PreviewUnavailable, ), ( ReticulumPreviewBehavior::DeferDeliveryPlans, - "reticulum_preview_transport_deferred", + PushOutboxEventState::DeferredUntilImplemented, + PushOutboxTargetOutcomeKind::DeferredUntilImplemented, ), ]; - for (behavior, expected_code) in cases { + for (behavior, expected_state, expected_outcome) in cases { let (_tempdir, sdk) = reticulum_preview_directory_sdk(behavior).await; let empty = sdk .sync() @@ -2178,41 +2182,34 @@ async fn sync_runtime_product_push_outbox_reticulum_preview_reports_specific_rea .await .expect("enqueue"); - let error = sdk + let receipt = sdk .sync() .push_outbox(PushOutboxRequest::new().with_limit(1)) .await - .expect_err("Reticulum preview is not delivery-capable"); - - assert!(matches!( - error, - RadrootsSdkError::ReticulumPreviewTransportUnavailable { - ref operation, - ref endpoint_uri, - behavior: returned_behavior, - } if operation == "sync.push_outbox" - && endpoint_uri == "reticulum:preview-unavailable" - && returned_behavior == behavior - )); - assert_eq!(error.code(), expected_code); - assert_eq!(error.class(), RadrootsSdkErrorClass::Unsupported); - assert!(!error.retryable()); - assert_eq!( - error.recovery_actions(), - vec![RadrootsSdkRecoveryAction::ConfigureTransportTargets] - ); - let detail = error.detail_json(); - assert_eq!(detail["detail"]["operation"], "sync.push_outbox"); + .expect("Reticulum preview push receipt"); + assert_eq!(receipt.attempted_events, 0); + assert_eq!(receipt.published_events, 0); + assert_eq!(receipt.retryable_events, 0); + assert_eq!(receipt.terminal_events, 0); + assert_eq!(receipt.events.len(), 1); + let event = &receipt.events[0]; + assert_eq!(event.outbox_event_id, enqueue.outbox_event_id); + assert_eq!(event.final_state, expected_state); + assert_eq!(event.attempted_count, 0); + assert_eq!(event.accepted_count, 0); + assert_eq!(event.retryable_count, 0); + assert_eq!(event.terminal_count, 0); + assert_eq!(event.quorum, 1); + assert!(!event.quorum_met); + assert_eq!(event.targets.len(), 1); + let target = &event.targets[0]; + assert_eq!(target.transport_kind, "reticulum"); + assert_eq!(target.endpoint_uri, "reticulum:preview-unavailable"); + assert_eq!(target.outcome_kind, expected_outcome); + assert!(!target.attempted); assert_eq!( - detail["detail"]["endpoint_uri"], - "reticulum:preview-unavailable" - ); - assert_eq!(detail["detail"]["behavior"], behavior.as_str()); - assert!( - !detail["message"] - .as_str() - .expect("message") - .contains("Nostr") + target.message.as_deref(), + Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE) ); let status = sdk @@ -2239,6 +2236,15 @@ async fn sync_runtime_product_push_outbox_reticulum_preview_reports_specific_rea ); assert_eq!(status.outbox.total_events, 1); assert_eq!(enqueue.outbox_event_id, 1); + + let specific = sdk + .sync() + .push_outbox(PushOutboxRequest::new().with_outbox_event_id(enqueue.outbox_event_id)) + .await + .expect("specific Reticulum preview push receipt"); + assert_eq!(specific.attempted_events, 0); + assert_eq!(specific.events.len(), 1); + assert_eq!(specific.events[0].outbox_event_id, enqueue.outbox_event_id); } } diff --git a/crates/sdk/tests/unit/error_tests.rs b/crates/sdk/tests/unit/error_tests.rs @@ -367,16 +367,16 @@ fn sdk_error_contract_methods_cover_representative_classes_and_details() { operation: "sync.push_outbox", required_feature: "transport-nostr-runtime", }, - RadrootsSdkError::reticulum_preview_transport_unavailable( - "sync.push_outbox", - "reticulum:preview-unavailable", - ReticulumPreviewBehavior::RejectDeliveryAttempts, - ), - RadrootsSdkError::reticulum_preview_transport_unavailable( - "sync.push_outbox", - "reticulum:preview-unavailable", - ReticulumPreviewBehavior::DeferDeliveryPlans, - ), + RadrootsSdkError::ReticulumPreviewTransportUnavailable { + operation: "sync.push_outbox".to_owned(), + endpoint_uri: "reticulum:preview-unavailable".to_owned(), + behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts, + }, + RadrootsSdkError::ReticulumPreviewTransportUnavailable { + operation: "sync.push_outbox".to_owned(), + endpoint_uri: "reticulum:preview-unavailable".to_owned(), + behavior: ReticulumPreviewBehavior::DeferDeliveryPlans, + }, RadrootsSdkError::ProductSyncTransportSetupFailure { message: "offline".to_owned(), }, diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs @@ -461,10 +461,10 @@ fn auth_policy_defaults_and_outbox_state_mappings_cover_all_public_states() { ); let mut receipt = PushOutboxReceipt::default(); - receipt.push_event(push_receipt(PushOutboxEventState::Published)); - receipt.push_event(push_receipt(PushOutboxEventState::PublishRetryable)); - receipt.push_event(push_receipt(PushOutboxEventState::FailedTerminal)); - receipt.push_event(push_receipt(PushOutboxEventState::Cancelled)); + receipt.push_attempted_event(push_receipt(PushOutboxEventState::Published)); + receipt.push_attempted_event(push_receipt(PushOutboxEventState::PublishRetryable)); + receipt.push_attempted_event(push_receipt(PushOutboxEventState::FailedTerminal)); + receipt.push_attempted_event(push_receipt(PushOutboxEventState::Cancelled)); assert_eq!(receipt.attempted_events, 4); assert_eq!(receipt.terminal_events, 1); assert_eq!(receipt.published_events, 1);