commit 391dfcc065bdf6b21bb28b9d90b54d7fb7402f2e
parent 78a7efa47971856939798226fe443c2b3769c972
Author: triesap <tyson@radroots.org>
Date: Mon, 15 Jun 2026 16:57:03 -0700
sdk: add product outbox push API
- rename the adapter-based sync method to push_outbox_with_adapter
- add product push_outbox for relay-runtime builds
- return a structured unsupported-feature error without relay-runtime
- cover configured-relay and adapter sync paths in runtime tests
Diffstat:
4 files changed, 86 insertions(+), 9 deletions(-)
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -60,7 +60,12 @@ runtime = [
"radroots_trade/event_store",
]
local-signer = ["runtime", "radroots_authority/local_signer"]
-relay-runtime = ["runtime", "radroots_relay_transport/client"]
+relay-runtime = [
+ "runtime",
+ "dep:radroots_nostr",
+ "radroots_nostr/client",
+ "radroots_relay_transport/client",
+]
[dependencies]
radroots_authority = { workspace = true, optional = true, default-features = false }
diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs
@@ -96,7 +96,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;
let push = sdk
.sync()
- .push_outbox(
+ .push_outbox_with_adapter(
&RadrootsMockRelayPublishAdapter::new(),
PushOutboxRequest::new().with_limit(1),
)
diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs
@@ -1,7 +1,11 @@
#[cfg(feature = "runtime")]
use crate::{RadrootsSdkError, SyncClient};
+#[cfg(all(feature = "runtime", feature = "relay-runtime"))]
+use radroots_nostr::prelude::RadrootsNostrClient;
#[cfg(feature = "runtime")]
use radroots_outbox::RadrootsOutboxEventState;
+#[cfg(all(feature = "runtime", feature = "relay-runtime"))]
+use radroots_relay_transport::RadrootsNostrClientPublishAdapter;
#[cfg(feature = "runtime")]
use radroots_relay_transport::{
RadrootsOutboxPublishPolicy, RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter,
@@ -181,7 +185,32 @@ impl From<RadrootsRelayOutcomeKind> for PushOutboxRelayOutcomeKind {
#[cfg(feature = "runtime")]
impl<'sdk> SyncClient<'sdk> {
- pub async fn push_outbox<A>(
+ pub async fn push_outbox(
+ &self,
+ request: PushOutboxRequest,
+ ) -> Result<PushOutboxReceipt, RadrootsSdkError> {
+ #[cfg(feature = "relay-runtime")]
+ {
+ if self.sdk.relay_urls().is_empty() {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: "sync push requires configured relay URLs".to_owned(),
+ });
+ }
+ let adapter =
+ RadrootsNostrClientPublishAdapter::new(RadrootsNostrClient::new_signerless());
+ self.push_outbox_with_adapter(&adapter, request).await
+ }
+
+ #[cfg(not(feature = "relay-runtime"))]
+ {
+ let _ = request;
+ Err(RadrootsSdkError::RelayTransport {
+ message: "sync push requires the relay-runtime feature".to_owned(),
+ })
+ }
+ }
+
+ pub async fn push_outbox_with_adapter<A>(
&self,
adapter: &A,
request: PushOutboxRequest,
diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs
@@ -181,7 +181,7 @@ async fn push_outbox_empty_queue_returns_zero_counts() {
let receipt = sdk
.sync()
- .push_outbox(&adapter, request)
+ .push_outbox_with_adapter(&adapter, request)
.await
.expect("push");
@@ -190,6 +190,49 @@ async fn push_outbox_empty_queue_returns_zero_counts() {
assert!(adapter.captured_raw_events().is_empty());
}
+#[cfg(not(feature = "relay-runtime"))]
+#[tokio::test]
+async fn product_push_outbox_without_relay_runtime_returns_structured_error() {
+ let (_tempdir, sdk) = directory_sdk(&[RELAY_A]).await;
+
+ let error = sdk
+ .sync()
+ .push_outbox(PushOutboxRequest::new())
+ .await
+ .expect_err("unsupported product push");
+
+ assert!(matches!(error, RadrootsSdkError::RelayTransport { .. }));
+}
+
+#[cfg(feature = "relay-runtime")]
+#[tokio::test]
+async fn product_push_outbox_empty_queue_uses_configured_relays() {
+ let (_tempdir, sdk) = directory_sdk(&[RELAY_A]).await;
+
+ let receipt = sdk
+ .sync()
+ .push_outbox(PushOutboxRequest::default())
+ .await
+ .expect("product push");
+
+ assert_eq!(receipt.attempted_events, 0);
+ assert!(receipt.events.is_empty());
+}
+
+#[cfg(feature = "relay-runtime")]
+#[tokio::test]
+async fn product_push_outbox_requires_configured_relays() {
+ let (_tempdir, sdk) = directory_sdk(&[]).await;
+
+ let error = sdk
+ .sync()
+ .push_outbox(PushOutboxRequest::new())
+ .await
+ .expect_err("missing configured relays");
+
+ assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
+}
+
#[tokio::test]
async fn push_outbox_rejects_invalid_limits_before_claiming() {
let (_tempdir, sdk) = directory_sdk(&[RELAY_A]).await;
@@ -197,12 +240,12 @@ async fn push_outbox_rejects_invalid_limits_before_claiming() {
let zero = sdk
.sync()
- .push_outbox(&adapter, PushOutboxRequest::new().with_limit(0))
+ .push_outbox_with_adapter(&adapter, PushOutboxRequest::new().with_limit(0))
.await
.expect_err("zero limit");
let too_large = sdk
.sync()
- .push_outbox(
+ .push_outbox_with_adapter(
&adapter,
PushOutboxRequest::new().with_limit(PUSH_OUTBOX_MAX_LIMIT + 1),
)
@@ -222,7 +265,7 @@ async fn push_outbox_publishes_signed_listing_and_marks_outbox_published() {
let receipt = sdk
.sync()
- .push_outbox(&adapter, PushOutboxRequest::new().with_limit(1))
+ .push_outbox_with_adapter(&adapter, PushOutboxRequest::new().with_limit(1))
.await
.expect("push");
@@ -284,7 +327,7 @@ async fn push_outbox_preserves_retryable_and_terminal_relay_outcomes() {
let receipt = sdk
.sync()
- .push_outbox(&adapter, PushOutboxRequest::new().with_limit(1))
+ .push_outbox_with_adapter(&adapter, PushOutboxRequest::new().with_limit(1))
.await
.expect("push");
@@ -353,7 +396,7 @@ async fn push_outbox_does_not_claim_unsigned_outbox_work() {
let receipt = sdk
.sync()
- .push_outbox(&adapter, PushOutboxRequest::new().with_limit(1))
+ .push_outbox_with_adapter(&adapter, PushOutboxRequest::new().with_limit(1))
.await
.expect("push");