commit 86dba23e631c9cc50d3cf03785e16cdd14150a4e
parent ec2d26f050a21b4d41de1fe67dca6add9a7d4253
Author: triesap <tyson@radroots.org>
Date: Wed, 24 Jun 2026 00:57:14 +0000
relay_transport: connect write relays before publish
- Connect configured write relays with deterministic try_connect before sending events.
- Publish only to connected targets while preserving connection failure receipts for the full target set.
- Validate with cargo fmt --all --check and cargo test -p radroots_relay_transport.
Diffstat:
1 file changed, 52 insertions(+), 4 deletions(-)
diff --git a/crates/relay_transport/src/publish.rs b/crates/relay_transport/src/publish.rs
@@ -1,10 +1,12 @@
#![forbid(unsafe_code)]
use crate::{RadrootsRelayOutcome, RadrootsRelayTargetSet, RadrootsRelayTransportError};
+#[cfg(feature = "client")]
+use core::time::Duration;
use futures::future::BoxFuture;
use radroots_events::draft::RadrootsSignedNostrEvent;
use serde::{Deserialize, Serialize};
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, Mutex, PoisonError};
#[cfg(feature = "client")]
@@ -14,6 +16,9 @@ use nostr::JsonUtil;
#[cfg(feature = "client")]
use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrEvent};
+#[cfg(feature = "client")]
+const RELAY_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
+
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RadrootsRelayPublishRequest {
pub signed_event: RadrootsSignedNostrEvent,
@@ -216,11 +221,47 @@ impl RadrootsRelayPublishAdapter for RadrootsNostrClientPublishAdapter {
.await
.map_err(|error| RadrootsRelayTransportError::Transport(error.to_string()))?;
}
- let output = match self
+ let connection_output = self.client.try_connect(RELAY_CONNECT_TIMEOUT).await;
+ let target_url_set = target_strings
+ .iter()
+ .map(|relay_url| relay_url.trim_end_matches('/').to_owned())
+ .collect::<BTreeSet<_>>();
+ let connected_strings = self
.client
- .send_event_to(target_strings.clone(), &event)
+ .relays()
.await
- {
+ .into_values()
+ .filter(|relay| relay.is_connected())
+ .map(|relay| relay.url().to_string())
+ .filter(|relay_url| target_url_set.contains(relay_url.trim_end_matches('/')))
+ .collect::<Vec<_>>();
+ let connection_failures = connection_output
+ .failed
+ .iter()
+ .map(|(relay, reason)| {
+ (
+ relay.to_string().trim_end_matches('/').to_owned(),
+ reason.clone(),
+ )
+ })
+ .collect::<BTreeMap<_, _>>();
+ if connected_strings.is_empty() {
+ return Ok(target_strings
+ .into_iter()
+ .map(|relay_url| {
+ let target_url = relay_url.trim_end_matches('/');
+ let reason = connection_failures
+ .get(target_url)
+ .cloned()
+ .unwrap_or_else(|| "relay did not connect".to_owned());
+ RadrootsRelayPublishRelayReceipt::attempted(
+ relay_url,
+ RadrootsRelayOutcome::connection_failed(reason),
+ )
+ })
+ .collect());
+ }
+ let output = match self.client.send_event_to(connected_strings, &event).await {
Ok(output) => output,
Err(error) => {
let message = error.to_string();
@@ -254,6 +295,13 @@ impl RadrootsRelayPublishAdapter for RadrootsNostrClientPublishAdapter {
));
continue;
}
+ if let Some(reason) = connection_failures.get(target_url) {
+ receipts.push(RadrootsRelayPublishRelayReceipt::attempted(
+ relay_url,
+ RadrootsRelayOutcome::connection_failed(reason.clone()),
+ ));
+ continue;
+ }
let failed = output.failed.iter().find_map(|(failed_url, message)| {
if failed_url.to_string().trim_end_matches('/') == target_url {
Some(message.clone())