commit 5811685dc028eee45e1e0087b06ecd1a0b59fb1e
parent ab124d4a7476f5fd139504c32a77df0cdec51b54
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Jul 2026 09:01:12 +0000
sync: preserve proxy preview outcomes
Diffstat:
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(),