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:
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);