sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 5811685dc028eee45e1e0087b06ecd1a0b59fb1e
parent ab124d4a7476f5fd139504c32a77df0cdec51b54
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Jul 2026 09:01:12 +0000

sync: preserve proxy preview outcomes

Diffstat:
Mcrates/sdk/src/sync_runtime.rs | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 100++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 163 insertions(+), 28 deletions(-)

diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -391,6 +391,8 @@ pub enum PushOutboxEventState { Published, SignRetryable, PublishRetryable, + DeferredUntilImplemented, + PreviewUnavailable, FailedTerminal, Cancelled, } @@ -631,7 +633,7 @@ impl<'sdk> SyncClient<'sdk> { claimed.outbox_event_id, push_event_final_state(&publish.publish), publish.publish, - )); + )?); } Ok(receipt) } @@ -666,7 +668,7 @@ impl<'sdk> SyncClient<'sdk> { publish_now_ms, ) .await?; - receipt.push_event(push_proxy_event_receipt(claimed.outbox_event_id, publish)); + receipt.push_event(push_proxy_event_receipt(claimed.outbox_event_id, publish)?); } Ok(receipt) } @@ -975,6 +977,34 @@ async fn complete_proxy_delivery_target( now_ms, ) .await?; + } else if outcome.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented { + sync.sdk + ._outbox + .mark_delivery_target_deferred_until_implemented( + claimed.outbox_event_id, + claimed.claim_token.as_str(), + target.delivery_target_id, + outcome + .message + .as_deref() + .unwrap_or("radrootsd proxy publish deferred until implemented"), + now_ms, + ) + .await?; + } else if outcome.outcome_kind == TransportPublishOutcomeKind::PreviewUnavailable { + sync.sdk + ._outbox + .mark_delivery_target_preview_unavailable( + claimed.outbox_event_id, + claimed.claim_token.as_str(), + target.delivery_target_id, + outcome + .message + .as_deref() + .unwrap_or("radrootsd proxy publish preview unavailable"), + now_ms, + ) + .await?; } else { sync.sdk ._outbox @@ -1011,6 +1041,28 @@ async fn complete_missing_proxy_delivery_target( now_ms, ) .await?; + } else if publish.status == TransportPublishJobStatus::DeliveryDeferred { + sync.sdk + ._outbox + .mark_delivery_target_deferred_until_implemented( + claimed.outbox_event_id, + claimed.claim_token.as_str(), + target.delivery_target_id, + "radrootsd proxy publish deferred until implemented", + now_ms, + ) + .await?; + } else if publish.status == TransportPublishJobStatus::DeliveryPreviewUnavailable { + sync.sdk + ._outbox + .mark_delivery_target_preview_unavailable( + claimed.outbox_event_id, + claimed.claim_token.as_str(), + target.delivery_target_id, + "radrootsd proxy publish preview unavailable", + now_ms, + ) + .await?; } else if publish.retryable_count > 0 || !publish.terminal || target.status == RadrootsOutboxDeliveryTargetStatus::FailedRetryable @@ -1077,6 +1129,10 @@ fn proxy_error_message(error: &RadrootsdError) -> String { fn proxy_push_event_final_state(publish: &TransportPublishJobView) -> PushOutboxEventState { if publish.delivery_satisfied { PushOutboxEventState::Published + } else if publish.status == TransportPublishJobStatus::DeliveryDeferred { + PushOutboxEventState::DeferredUntilImplemented + } else if publish.status == TransportPublishJobStatus::DeliveryPreviewUnavailable { + PushOutboxEventState::PreviewUnavailable } else if publish.retryable_count > 0 || !publish.terminal { PushOutboxEventState::PublishRetryable } else { @@ -1088,13 +1144,15 @@ fn proxy_push_event_final_state(publish: &TransportPublishJobView) -> PushOutbox fn push_proxy_event_receipt( outbox_event_id: i64, publish: TransportPublishJobView, -) -> PushOutboxEventReceipt { - let event_id = RadrootsEventId::parse(publish.event_id.as_str()) - .expect("transport publish daemon job uses signed event id"); +) -> Result<PushOutboxEventReceipt, RadrootsSdkError> { + let event_id = push_receipt_event_id( + publish.event_id.as_str(), + "transport publish daemon job event id", + )?; let quorum = publish .delivery_policy .required_target_count(publish.target_count); - PushOutboxEventReceipt { + Ok(PushOutboxEventReceipt { event_id, outbox_event_id, final_state: proxy_push_event_final_state(&publish), @@ -1113,7 +1171,7 @@ fn push_proxy_event_receipt( .into_iter() .map(push_proxy_target_receipt) .collect(), - } + }) } #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] @@ -1189,10 +1247,12 @@ fn push_event_receipt( outbox_event_id: i64, final_state: PushOutboxEventState, publish: RadrootsRelayPublishReceipt, -) -> PushOutboxEventReceipt { - let event_id = RadrootsEventId::parse(publish.event_id.as_str()) - .expect("relay transport publish receipt uses signed event id"); - PushOutboxEventReceipt { +) -> Result<PushOutboxEventReceipt, RadrootsSdkError> { + let event_id = push_receipt_event_id( + publish.event_id.as_str(), + "relay transport publish receipt event id", + )?; + Ok(PushOutboxEventReceipt { event_id, outbox_event_id, final_state, @@ -1207,7 +1267,14 @@ fn push_event_receipt( .into_iter() .map(push_target_receipt) .collect(), - } + }) +} + +#[cfg(feature = "runtime")] +fn push_receipt_event_id(value: &str, field: &str) -> Result<RadrootsEventId, RadrootsSdkError> { + RadrootsEventId::parse(value).map_err(|error| RadrootsSdkError::InvalidRequest { + message: format!("{field} is invalid: {error}"), + }) } #[cfg(feature = "runtime")] diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs @@ -2,7 +2,7 @@ use super::{ CLAIM_OWNER, complete_proxy_publish_attempt, proxy_delivery_policy_from_satisfaction, proxy_error_message, proxy_outbox_idempotency_key, proxy_transport_error_job, - push_proxy_claimed_outbox_event, + push_proxy_claimed_outbox_event, push_proxy_event_receipt, }; use super::{ PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxTargetOutcomeKind, @@ -36,7 +36,7 @@ use radroots_nostr::prelude::{ RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft, }; #[cfg(feature = "radrootsd-proxy")] -use radroots_outbox::RadrootsOutboxClaimedEvent; +use radroots_outbox::{RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryTargetStatus}; use radroots_outbox::{RadrootsOutboxEventState, RadrootsOutboxStatusSummary}; #[cfg(feature = "radrootsd-proxy")] use radroots_transport::RadrootsTransportSatisfactionPolicy; @@ -177,15 +177,20 @@ fn proxy_job(event_id: &str, outcome_kind: TransportPublishOutcomeKind) -> Trans let delivery_satisfied = outcome_kind.counts_toward_satisfaction(); let retryable = outcome_kind.is_retryable(); let terminal_failure = outcome_kind.is_terminal_failure(); + let status = if delivery_satisfied { + TransportPublishJobStatus::DeliverySatisfied + } else if retryable { + TransportPublishJobStatus::DeliveryUnsatisfiedRetryable + } else if outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented { + TransportPublishJobStatus::DeliveryDeferred + } else if outcome_kind == TransportPublishOutcomeKind::PreviewUnavailable { + TransportPublishJobStatus::DeliveryPreviewUnavailable + } else { + TransportPublishJobStatus::DeliveryUnsatisfiedTerminal + }; TransportPublishJobView { job_id: "proxy-unit-job".to_owned(), - status: if delivery_satisfied { - TransportPublishJobStatus::DeliverySatisfied - } else if retryable { - TransportPublishJobStatus::DeliveryUnsatisfiedRetryable - } else { - TransportPublishJobStatus::DeliveryUnsatisfiedTerminal - }, + status, terminal: !retryable, delivery_satisfied, event_id: event_id.to_owned(), @@ -232,7 +237,8 @@ fn push_event_receipt_parses_typed_event_id() { 1, PushOutboxEventState::Published, relay_publish_receipt(event_id.as_str()).with_relay(), - ); + ) + .expect("receipt"); assert_eq!( receipt.event_id, @@ -245,13 +251,18 @@ fn push_event_receipt_parses_typed_event_id() { } #[test] -#[should_panic(expected = "relay transport publish receipt uses signed event id")] -fn push_event_receipt_panics_on_invalid_internal_event_id() { - let _ = push_event_receipt( +fn push_event_receipt_returns_typed_error_for_invalid_internal_event_id() { + let error = push_event_receipt( 1, PushOutboxEventState::Published, relay_publish_receipt("not-a-valid-event-id"), - ); + ) + .expect_err("invalid event id"); + assert!(matches!( + error, + RadrootsSdkError::InvalidRequest { message } + if message.contains("relay transport publish receipt event id is invalid") + )); } #[test] @@ -701,21 +712,48 @@ async fn proxy_completion_updates_outbox_for_success_retryable_and_terminal_rece ( "proxy-complete-success", PushOutboxEventState::Published, + PushOutboxEventState::Published, + RadrootsOutboxDeliveryTargetStatus::Accepted, TransportPublishOutcomeKind::Accepted, ), ( "proxy-complete-retryable", PushOutboxEventState::PublishRetryable, + PushOutboxEventState::PublishRetryable, + RadrootsOutboxDeliveryTargetStatus::FailedRetryable, TransportPublishOutcomeKind::Timeout, ), ( "proxy-complete-terminal", PushOutboxEventState::FailedTerminal, + PushOutboxEventState::FailedTerminal, + RadrootsOutboxDeliveryTargetStatus::FailedTerminal, TransportPublishOutcomeKind::Blocked, ), + ( + "proxy-complete-deferred", + PushOutboxEventState::DeferredUntilImplemented, + PushOutboxEventState::Signed, + RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented, + TransportPublishOutcomeKind::DeferredUntilImplemented, + ), + ( + "proxy-complete-preview-unavailable", + PushOutboxEventState::PreviewUnavailable, + PushOutboxEventState::Signed, + RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable, + TransportPublishOutcomeKind::PreviewUnavailable, + ), ]; - for (d_tag, expected_state, outcome_kind) in cases { + for ( + d_tag, + expected_receipt_state, + expected_stored_state, + expected_target_status, + outcome_kind, + ) in cases + { let (sdk, claimed) = claimed_proxy_event(d_tag).await; let publish = proxy_job( claimed @@ -743,6 +781,9 @@ async fn proxy_completion_updates_outbox_for_success_retryable_and_terminal_rece publish.event_kind, claimed.signed_event.as_ref().expect("signed event").kind ); + let proxy_receipt = + push_proxy_event_receipt(claimed.outbox_event_id, publish.clone()).expect("receipt"); + assert_eq!(proxy_receipt.final_state, expected_receipt_state); let sync = sdk.sync(); complete_proxy_publish_attempt(&sync, &claimed, &publish, 60_000, 1_700_000_000_000) .await @@ -753,11 +794,38 @@ async fn proxy_completion_updates_outbox_for_success_retryable_and_terminal_rece .await .expect("stored") .expect("stored"); - assert_eq!(PushOutboxEventState::from(stored.state), expected_state); + assert_eq!( + PushOutboxEventState::from(stored.state), + expected_stored_state + ); assert!(stored.claim_token.is_none()); + let targets = sdk + ._outbox + .delivery_targets(claimed.outbox_event_id) + .await + .expect("targets"); + assert_eq!(targets[0].status, expected_target_status); } } +#[cfg(feature = "radrootsd-proxy")] +#[test] +fn push_proxy_event_receipt_returns_typed_error_for_invalid_daemon_event_id() { + let error = push_proxy_event_receipt( + 1, + proxy_job( + "not-a-valid-event-id", + TransportPublishOutcomeKind::Accepted, + ), + ) + .expect_err("invalid daemon event id"); + assert!(matches!( + error, + RadrootsSdkError::InvalidRequest { message } + if message.contains("transport publish daemon job event id is invalid") + )); +} + fn relay_publish_receipt(event_id: &str) -> RadrootsRelayPublishReceipt { RadrootsRelayPublishReceipt { event_id: event_id.to_owned(),