diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 6e670b331..5db15d242 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -72,6 +72,7 @@ - Add `Output::new` constructor and `Output::into_inner` method - Add idle timeout for negentropy sync (https://github.com/rust-nostr/nostr/pull/1131) - Add `GossipAllowedRelays` to `GossipOptions` to filter relays during selection (https://github.com/rust-nostr/nostr/pull/1128) +- Add support for blocking APIs (https://github.com/rust-nostr/nostr/pull/1246) ## v0.44.1 - 2025/11/09 diff --git a/sdk/examples/blocking.rs b/sdk/examples/blocking.rs new file mode 100644 index 000000000..3e72687f4 --- /dev/null +++ b/sdk/examples/blocking.rs @@ -0,0 +1,57 @@ +use std::collections::HashMap; +use std::time::Duration; + +use nostr_sdk::prelude::*; + +fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let client: Client = Client::default(); + + client + .add_relay("wss://relay.damus.io") + .and_connect() + .blocking()?; + + client + .add_relay("wss://relay.nostr.band") + .and_connect() + .blocking()?; + + let public_key = + PublicKey::from_bech32("npub1080l37pfvdpyuzasyuy2ytjykjvq3ylr5jlqlg7tvzjrh9r8vn3sf5yaph")?; + + // Example 1: Subscribe with a single filter (broadcast to all relays) + let filter = Filter::new().author(public_key).kind(Kind::Metadata); + let output = client.subscribe(filter).blocking()?; + println!("{:?}", output); + + // Example 2: Subscribe with multiple filters + let filters = vec![ + Filter::new().author(public_key).kind(Kind::TextNote), + Filter::new().author(public_key).kind(Kind::Metadata), + ]; + let output = client.subscribe(filters).blocking()?; + println!("{:?}", output); + + // Example 3: Targeted subscription with HashMap<&str, Filter> + let mut targets = HashMap::new(); + targets.insert("wss://relay.damus.io", Filter::new().kind(Kind::TextNote)); + targets.insert("wss://relay.nostr.band", Filter::new().kind(Kind::Metadata)); + let output = client.subscribe(targets).blocking()?; + println!("{:?}", output); + + // Example 4: Targeted subscription with HashMap> + let mut targets = HashMap::new(); + targets.insert( + "wss://relay.damus.io".to_string(), + vec![Filter::new().kind(Kind::TextNote)], + ); + let output = client.subscribe(targets).blocking()?; + + println!("{:?}", output); + + loop { + std::thread::sleep(Duration::from_secs(60)); + } +} diff --git a/sdk/src/blocking.rs b/sdk/src/blocking.rs new file mode 100644 index 000000000..ea877ff31 --- /dev/null +++ b/sdk/src/blocking.rs @@ -0,0 +1,101 @@ +/// Implements an inherent `blocking(self)` method for a type that already implements `IntoFuture`. +/// +/// - On `wasm32`, the method is not generated (blocking the thread isn't supported). +/// - The return type is inferred as `::Output`. +macro_rules! impl_blocking { + // ---- Generic form: accepts explicit generics (lifetimes, types, consts) ---- + (for<$($gen:tt),+> $ty:ty where $($where:tt)+) => { + impl<$($gen),+> $ty + where + $($where)+ + { + /// Execute the operation synchronously. + /// + /// # Important + /// + /// Avoid calling this from within an `async` context, as it may lead to + /// "resource starvation" or a deadlock. Prefer `.await` whenever possible. + /// + /// This is runtime-agnostic and uses a local executor to drive the future + /// to completion. + #[inline] + #[cfg(not(target_family = "wasm"))] + pub fn blocking(self) -> ::Output + where + Self: ::std::future::IntoFuture, + { + ::futures::executor::block_on(::std::future::IntoFuture::into_future(self)) + } + } + }; + + (for<$($gen:tt),+> $ty:ty) => { + impl<$($gen),+> $ty { + /// Execute the operation synchronously. + /// + /// # Important + /// + /// Avoid calling this from within an `async` context, as it may lead to + /// "resource starvation" or a deadlock. Prefer `.await` whenever possible. + /// + /// This is runtime-agnostic and uses a local executor to drive the future + /// to completion. + #[inline] + #[cfg(not(target_family = "wasm"))] + pub fn blocking(self) -> ::Output + where + Self: ::std::future::IntoFuture, + { + ::futures::executor::block_on(::std::future::IntoFuture::into_future(self)) + } + } + }; + + // ---- Non-generic form: works for concrete types or elided lifetimes ---- + ($ty:ty where $($where:tt)+) => { + impl $ty + where + $($where)+ + { + /// Execute the operation synchronously. + /// + /// # Important + /// + /// Avoid calling this from within an `async` context, as it may lead to + /// "resource starvation" or a deadlock. Prefer `.await` whenever possible. + /// + /// This is runtime-agnostic and uses a local executor to drive the future + /// to completion. + #[inline] + #[cfg(not(target_family = "wasm"))] + pub fn blocking(self) -> ::Output + where + Self: ::std::future::IntoFuture, + { + ::futures::executor::block_on(::std::future::IntoFuture::into_future(self)) + } + } + }; + + ($ty:ty) => { + impl $ty { + /// Execute the operation synchronously. + /// + /// # Important + /// + /// Avoid calling this from within an `async` context, as it may lead to + /// "resource starvation" or a deadlock. Prefer `.await` whenever possible. + /// + /// This is runtime-agnostic and uses a local executor to drive the future + /// to completion. + #[inline] + #[cfg(not(target_family = "wasm"))] + pub fn blocking(self) -> ::Output + where + Self: ::std::future::IntoFuture, + { + ::futures::executor::block_on(::std::future::IntoFuture::into_future(self)) + } + } + }; +} diff --git a/sdk/src/client/api/add.rs b/sdk/src/client/api/add.rs index 86e226d6e..581979005 100644 --- a/sdk/src/client/api/add.rs +++ b/sdk/src/client/api/add.rs @@ -158,6 +158,8 @@ where } } +impl_blocking!(for<'client, 'url> AddRelay<'client, 'url> where 'url: 'client); + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/src/client/api/connect.rs b/sdk/src/client/api/connect.rs index a8436bc72..44526112d 100644 --- a/sdk/src/client/api/connect.rs +++ b/sdk/src/client/api/connect.rs @@ -47,3 +47,5 @@ impl<'client> IntoFuture for Connect<'client> { }) } } + +impl_blocking!(Connect<'_>); diff --git a/sdk/src/client/api/fetch_events.rs b/sdk/src/client/api/fetch_events.rs index 6070bd60b..144ade341 100644 --- a/sdk/src/client/api/fetch_events.rs +++ b/sdk/src/client/api/fetch_events.rs @@ -92,3 +92,5 @@ where }) } } + +impl_blocking!(for<'client, 'url> FetchEvents<'client, 'url> where 'url: 'client); diff --git a/sdk/src/client/api/relays.rs b/sdk/src/client/api/relays.rs index 9d010d643..217d7990f 100644 --- a/sdk/src/client/api/relays.rs +++ b/sdk/src/client/api/relays.rs @@ -69,6 +69,8 @@ impl<'client> IntoFuture for GetRelays<'client> { } } +impl_blocking!(GetRelays<'_>); + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/src/client/api/remove.rs b/sdk/src/client/api/remove.rs index e681a307a..a98a9dde9 100644 --- a/sdk/src/client/api/remove.rs +++ b/sdk/src/client/api/remove.rs @@ -49,6 +49,8 @@ where } } +impl_blocking!(for<'client, 'url> RemoveRelay<'client, 'url> where 'url: 'client); + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/src/client/api/remove_all.rs b/sdk/src/client/api/remove_all.rs index 9213742f4..80e9c24df 100644 --- a/sdk/src/client/api/remove_all.rs +++ b/sdk/src/client/api/remove_all.rs @@ -38,6 +38,8 @@ impl<'client> IntoFuture for RemoveAllRelays<'client> { } } +impl_blocking!(RemoveAllRelays<'_>); + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/src/client/api/send_event.rs b/sdk/src/client/api/send_event.rs index 4dc7bdf94..744dc00ff 100644 --- a/sdk/src/client/api/send_event.rs +++ b/sdk/src/client/api/send_event.rs @@ -370,6 +370,8 @@ where } } +impl_blocking!(for<'client, 'event, 'url> SendEvent<'client, 'event, 'url> where 'event: 'client, 'url: 'client); + #[cfg(test)] mod tests { use nostr::prelude::*; diff --git a/sdk/src/client/api/send_msg.rs b/sdk/src/client/api/send_msg.rs index 38bfac6de..af59ad364 100644 --- a/sdk/src/client/api/send_msg.rs +++ b/sdk/src/client/api/send_msg.rs @@ -109,6 +109,8 @@ where } } +impl_blocking!(for<'client, 'msg, 'url> SendMessage<'client, 'msg, 'url> where 'msg: 'client, 'url: 'client); + #[cfg(test)] mod tests { use nostr::prelude::*; diff --git a/sdk/src/client/api/subscribe.rs b/sdk/src/client/api/subscribe.rs index 6996582e3..c8c510253 100644 --- a/sdk/src/client/api/subscribe.rs +++ b/sdk/src/client/api/subscribe.rs @@ -66,3 +66,5 @@ where }) } } + +impl_blocking!(for<'client, 'url> Subscribe<'client, 'url> where 'url: 'client); diff --git a/sdk/src/client/api/sync.rs b/sdk/src/client/api/sync.rs index 060bd1502..18d7e2309 100644 --- a/sdk/src/client/api/sync.rs +++ b/sdk/src/client/api/sync.rs @@ -190,3 +190,5 @@ where }) } } + +impl_blocking!(for<'client, 'url> SyncEvents<'client, 'url> where 'url: 'client); diff --git a/sdk/src/client/api/try_connect.rs b/sdk/src/client/api/try_connect.rs index 48f11424b..8ea7ed869 100644 --- a/sdk/src/client/api/try_connect.rs +++ b/sdk/src/client/api/try_connect.rs @@ -43,3 +43,5 @@ impl<'client> IntoFuture for TryConnect<'client> { Box::pin(async move { self.client.pool.try_connect(self.timeout).await }) } } + +impl_blocking!(TryConnect<'_>); diff --git a/sdk/src/client/api/unsubscribe.rs b/sdk/src/client/api/unsubscribe.rs index ca1f47610..e452a5cde 100644 --- a/sdk/src/client/api/unsubscribe.rs +++ b/sdk/src/client/api/unsubscribe.rs @@ -35,3 +35,5 @@ where }) } } + +impl_blocking!(for<'client, 'id> Unsubscribe<'client, 'id> where 'id: 'client); diff --git a/sdk/src/client/api/unsubscribe_all.rs b/sdk/src/client/api/unsubscribe_all.rs index 109b0b36c..3ee4b2679 100644 --- a/sdk/src/client/api/unsubscribe_all.rs +++ b/sdk/src/client/api/unsubscribe_all.rs @@ -29,3 +29,5 @@ impl<'client> IntoFuture for UnsubscribeAll<'client> { }) } } + +impl_blocking!(UnsubscribeAll<'_>); diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 2e57950a6..557b61f41 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -7,6 +7,8 @@ #![allow(clippy::mutable_key_type)] // TODO: remove when possible. Needed to suppress false positive for `BTreeSet` #![doc = include_str!("../README.md")] +#[macro_use] +mod blocking; pub mod client; pub mod monitor; pub mod policy; diff --git a/sdk/src/relay/api/fetch_events.rs b/sdk/src/relay/api/fetch_events.rs index 5f25e1950..c1aadab05 100644 --- a/sdk/src/relay/api/fetch_events.rs +++ b/sdk/src/relay/api/fetch_events.rs @@ -91,6 +91,8 @@ impl<'relay> IntoFuture for FetchEvents<'relay> { } } +impl_blocking!(FetchEvents<'_>); + #[cfg(test)] mod tests { use nostr::message::MachineReadablePrefix; diff --git a/sdk/src/relay/api/send_event.rs b/sdk/src/relay/api/send_event.rs index 42d99a285..a82111fee 100644 --- a/sdk/src/relay/api/send_event.rs +++ b/sdk/src/relay/api/send_event.rs @@ -143,6 +143,8 @@ where } } +impl_blocking!(for<'relay, 'event> SendEvent<'relay, 'event> where 'event: 'relay); + #[cfg(test)] mod tests { use std::time::Duration; diff --git a/sdk/src/relay/api/send_msg.rs b/sdk/src/relay/api/send_msg.rs index d80dc9929..f6a5d55ff 100644 --- a/sdk/src/relay/api/send_msg.rs +++ b/sdk/src/relay/api/send_msg.rs @@ -53,3 +53,5 @@ where }) } } + +impl_blocking!(for<'relay, 'msg> SendMessage<'relay, 'msg> where 'msg: 'relay); diff --git a/sdk/src/relay/api/subscribe.rs b/sdk/src/relay/api/subscribe.rs index c02853b6d..95187093d 100644 --- a/sdk/src/relay/api/subscribe.rs +++ b/sdk/src/relay/api/subscribe.rs @@ -136,6 +136,8 @@ impl<'relay> IntoFuture for Subscribe<'relay> { } } +impl_blocking!(Subscribe<'_>); + #[cfg(test)] mod tests { use std::time::Duration; diff --git a/sdk/src/relay/api/sync.rs b/sdk/src/relay/api/sync.rs index 339fdbca5..0ef2762d2 100644 --- a/sdk/src/relay/api/sync.rs +++ b/sdk/src/relay/api/sync.rs @@ -614,6 +614,8 @@ impl<'relay> IntoFuture for SyncEvents<'relay> { } } +impl_blocking!(SyncEvents<'_>); + #[cfg(test)] mod tests { use std::collections::{HashMap, HashSet}; diff --git a/sdk/src/relay/api/try_connect.rs b/sdk/src/relay/api/try_connect.rs index 1df5f5448..338aa823d 100644 --- a/sdk/src/relay/api/try_connect.rs +++ b/sdk/src/relay/api/try_connect.rs @@ -78,6 +78,8 @@ impl<'relay> IntoFuture for TryConnect<'relay> { } } +impl_blocking!(TryConnect<'_>); + #[cfg(test)] mod tests { use async_utility::time; diff --git a/sdk/src/relay/api/unsubscribe.rs b/sdk/src/relay/api/unsubscribe.rs index 966dec2e4..306d9e91f 100644 --- a/sdk/src/relay/api/unsubscribe.rs +++ b/sdk/src/relay/api/unsubscribe.rs @@ -30,3 +30,5 @@ where Box::pin(async move { self.relay.inner.unsubscribe(self.id).await }) } } + +impl_blocking!(for<'relay, 'id> Unsubscribe<'relay, 'id> where 'id: 'relay); diff --git a/sdk/src/relay/api/unsubscribe_all.rs b/sdk/src/relay/api/unsubscribe_all.rs index 63225c741..88ae79c93 100644 --- a/sdk/src/relay/api/unsubscribe_all.rs +++ b/sdk/src/relay/api/unsubscribe_all.rs @@ -24,3 +24,5 @@ impl<'relay> IntoFuture for UnsubscribeAll<'relay> { Box::pin(async move { self.relay.inner.unsubscribe_all().await }) } } + +impl_blocking!(UnsubscribeAll<'_>);