lib

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

commit b014dbfc4a96149d5e027deefe3655f780e401d0
parent 958b9f6c1c056fe4a8a73ad80b3da69d0f50fda8
Author: triesap <tyson@radroots.org>
Date:   Thu,  9 Jul 2026 04:31:50 +0000

workflow: preserve no-wait delivery semantics

Diffstat:
Mcrates/sdk/src/sync_runtime.rs | 21++++++++++++++-------
Mcrates/sdk/src/workflow_runtime.rs | 35+++++++++++++++++++++--------------
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 5+++++
Mcrates/sdk/tests/unit/workflow_runtime_tests.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 114 insertions(+), 21 deletions(-)

diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -1047,14 +1047,20 @@ async fn proxy_delivery_policy( .iter() .filter(|target| target.delivery_plan_id == active_delivery_plan_id) .collect::<Vec<_>>(); - let satisfied_count = active_targets - .iter() - .filter(|target| { - target - .status - .counts_as_transport_satisfaction(plan.satisfaction_policy.class()) + let satisfied_count = plan + .satisfaction_policy + .target_satisfaction_class() + .map(|satisfaction_class| { + active_targets + .iter() + .filter(|target| { + target + .status + .counts_as_transport_satisfaction(satisfaction_class) + }) + .count() }) - .count(); + .unwrap_or(0); let ready_target_count = active_targets .iter() .filter(|target| target.status.is_ready_for_attempt()) @@ -1077,6 +1083,7 @@ fn proxy_delivery_policy_from_remaining( return Ok(TransportPublishDeliveryPolicy::Any); } Ok(match satisfaction_policy { + RadrootsTransportSatisfactionPolicy::NoWait => TransportPublishDeliveryPolicy::Any, RadrootsTransportSatisfactionPolicy::Any { .. } => TransportPublishDeliveryPolicy::Any, RadrootsTransportSatisfactionPolicy::All { .. } => TransportPublishDeliveryPolicy::All, RadrootsTransportSatisfactionPolicy::Quorum { .. } => { diff --git a/crates/sdk/src/workflow_runtime.rs b/crates/sdk/src/workflow_runtime.rs @@ -17,7 +17,9 @@ use radroots_outbox::{ RadrootsOutboxDeliveryPlanInput, RadrootsOutboxEnqueueStatus, RadrootsOutboxReticulumPreviewBehavior, RadrootsOutboxSignedOperationInput, }; -use radroots_transport::{RadrootsTransportKind, RadrootsTransportSatisfactionPolicy}; +use radroots_transport::{ + RadrootsTransportKind, RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, +}; const SDK_LOCAL_EVENT_ENDPOINT_URI: &str = "local:sdk"; @@ -166,20 +168,26 @@ fn resolved_delivery_plan( satisfaction_policy: SatisfactionPolicy, ) -> Result<SdkResolvedDeliveryPlan, RadrootsSdkError> { match target_policy { - TargetPolicy::Explicit(target_policy) => delivery_plan_from_target_set( + TargetPolicy::Explicit(target_policy) => delivery_plan_from_targets( "explicit", - target_policy.clone(), + target_policy.clone().into_targets(), satisfaction_policy, RadrootsOutboxReticulumPreviewBehavior::RejectDeliveryAttempts, ), TargetPolicy::UseTransportProfile => { let transport_profile = sdk.transport_profile(); - let target_set = transport_profile.target_set()?.ok_or_else(|| { - RadrootsSdkError::empty_transport_targets("publish transport profile") - })?; - delivery_plan_from_target_set( + let targets = transport_profile + .target_set()? + .map(TargetSet::into_targets) + .unwrap_or_default(); + if targets.is_empty() && satisfaction_policy != SatisfactionPolicy::NoWait { + return Err(RadrootsSdkError::empty_transport_targets( + "publish transport profile", + )); + } + delivery_plan_from_targets( transport_profile.transport_profile_id(), - target_set, + targets, satisfaction_policy, outbox_reticulum_preview_behavior(transport_profile), ) @@ -187,9 +195,9 @@ fn resolved_delivery_plan( } } -fn delivery_plan_from_target_set( +fn delivery_plan_from_targets( transport_profile_id: impl Into<String>, - target_set: TargetSet, + targets: Vec<RadrootsTransportTarget>, satisfaction_policy: SatisfactionPolicy, reticulum_preview_behavior: RadrootsOutboxReticulumPreviewBehavior, ) -> Result<SdkResolvedDeliveryPlan, RadrootsSdkError> { @@ -197,7 +205,7 @@ fn delivery_plan_from_target_set( transport_profile_id, 1, transport_satisfaction_policy(satisfaction_policy), - target_set.into_targets(), + targets, ) .with_reticulum_preview_behavior(reticulum_preview_behavior); Ok(SdkResolvedDeliveryPlan { delivery_plan }) @@ -207,9 +215,8 @@ fn transport_satisfaction_policy( satisfaction_policy: SatisfactionPolicy, ) -> RadrootsTransportSatisfactionPolicy { match satisfaction_policy { - SatisfactionPolicy::NoWait | SatisfactionPolicy::AllTargets => { - RadrootsTransportSatisfactionPolicy::all_accepted() - } + SatisfactionPolicy::NoWait => RadrootsTransportSatisfactionPolicy::no_wait(), + SatisfactionPolicy::AllTargets => RadrootsTransportSatisfactionPolicy::all_accepted(), SatisfactionPolicy::AtLeastOneTarget => RadrootsTransportSatisfactionPolicy::any_accepted(), SatisfactionPolicy::AtLeast { required } => { RadrootsTransportSatisfactionPolicy::quorum_accepted(required) diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs @@ -671,6 +671,11 @@ async fn proxy_push_empty_queue_and_private_helpers_are_deterministic() { assert_eq!(receipt.attempted_events, 0); assert_eq!( + proxy_delivery_policy_from_remaining(0, 0, &RadrootsTransportSatisfactionPolicy::no_wait()) + .expect("no-wait proxy policy"), + TransportPublishDeliveryPolicy::Any + ); + assert_eq!( proxy_delivery_policy_from_remaining( 0, 0, diff --git a/crates/sdk/tests/unit/workflow_runtime_tests.rs b/crates/sdk/tests/unit/workflow_runtime_tests.rs @@ -239,6 +239,80 @@ async fn default_operation_idempotency_ignores_target_policy() { } #[tokio::test] +async fn enqueue_signed_workflow_maps_no_wait_directly_and_allows_local_only_profile() { + let sdk = crate::RadrootsClient::builder() + .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds( + 1_700_000_013, + )) + .build() + .await + .expect("sdk"); + let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer]) + .expect("actor"); + let signer = WorkflowSigner::new(); + let draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-no-wait"); + + let receipt = enqueue_signed_workflow( + &sdk, + SdkWorkflowEnqueueRequest { + operation_kind: "workflow.test.v1", + actor: &actor, + frozen_draft: &draft, + target_policy: TargetPolicy::use_transport_profile(), + satisfaction_policy: SatisfactionPolicy::NoWait, + idempotency_key: None, + }, + &signer, + ) + .await + .expect("no-wait enqueue"); + + let event = sdk + ._outbox + .get_event(receipt.outbox_event_id) + .await + .expect("event") + .expect("event"); + let plans = sdk + ._outbox + .delivery_plans(receipt.outbox_event_id) + .await + .expect("plans"); + let targets = sdk + ._outbox + .delivery_targets(receipt.outbox_event_id) + .await + .expect("targets"); + + assert_eq!( + event.state, + radroots_outbox::RadrootsOutboxEventState::Published + ); + assert_eq!(plans.len(), 1); + assert_eq!( + plans[0].satisfaction_policy, + radroots_transport::RadrootsTransportSatisfactionPolicy::no_wait() + ); + assert_ne!( + plans[0].satisfaction_policy, + radroots_transport::RadrootsTransportSatisfactionPolicy::all_accepted() + ); + assert_eq!(plans[0].required_success_count, 0); + assert_eq!( + plans[0].status, + radroots_outbox::RadrootsOutboxDeliveryPlanStatus::Complete + ); + assert!(targets.is_empty()); + assert!( + sdk._outbox + .claim_next_ready_signed_event("publisher", "claim-a", 2_000, 1_000) + .await + .expect("claim") + .is_none() + ); +} + +#[tokio::test] async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_conflicts() { let sdk = crate::RadrootsClient::builder() .transport_profile(nostr_profile("wss://relay.example.com"))