From a19c76a59575409391e8cc05f016bb5a650c42f2 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Wed, 14 Aug 2024 14:53:29 -0400 Subject: [PATCH] sdk: allow to specify the source of events for `Client::get_events_of` method Signed-off-by: Yuki Kishimoto --- CHANGELOG.md | 2 + .../nostr-sdk-ffi/bindings-python/README.md | 6 +- .../bindings-python/examples/blacklist.py | 5 +- .../bindings-python/examples/client.py | 6 +- .../bindings-python/examples/metadata.py | 5 +- bindings/nostr-sdk-ffi/src/client/mod.rs | 6 +- bindings/nostr-sdk-ffi/src/client/options.rs | 56 +++++++++++++ .../nostr-sdk-js/examples/get-events-of.js | 5 +- bindings/nostr-sdk-js/src/client/mod.rs | 7 +- bindings/nostr-sdk-js/src/client/options.rs | 55 ++++++++++++- crates/nostr-sdk/examples/blacklist.rs | 5 +- crates/nostr-sdk/examples/get-events-of.rs | 5 +- crates/nostr-sdk/examples/nip65.rs | 5 +- crates/nostr-sdk/src/client/mod.rs | 82 ++++++++++++++++--- crates/nostr-sdk/src/client/options.rs | 65 +++++++++++++++ crates/nostr-sdk/src/client/zapper.rs | 6 +- crates/nostr-sdk/src/lib.rs | 3 +- 17 files changed, 285 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32cab6f3..40af21ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ * database: set a default logic for `NostrDatabase::negentropy_items` ([Yuki Kishimoto]) * sdk: rename `Proxy` and `ProxyTarget` to `Connection` and `ConnectionTarget` ([Yuki Kishimoto]) * sdk: allow to skip slow relays ([Yuki Kishimoto]) +* sdk: allow to specify the source of events for `Client::get_events_of` method ([Yuki Kishimoto]) +* sdk: deprecate `Client::get_events_of_with_opts` ([Yuki Kishimoto]) * sqlite: use `ValueRef` instead of owned one ([Yuki Kishimoto]) * cli: improve `sync` command ([Yuki Kishimoto]) * cli: allow to specify relays in `open` command ([Yuki Kishimoto]) diff --git a/bindings/nostr-sdk-ffi/bindings-python/README.md b/bindings/nostr-sdk-ffi/bindings-python/README.md index 819ac782d..321953a4b 100644 --- a/bindings/nostr-sdk-ffi/bindings-python/README.md +++ b/bindings/nostr-sdk-ffi/bindings-python/README.md @@ -19,8 +19,7 @@ pip install nostr-sdk ```python import asyncio from datetime import timedelta -from nostr_sdk import Keys, Client, NostrSigner, EventBuilder, Filter, Metadata, Nip46Signer, init_logger, LogLevel -import time +from nostr_sdk import Keys, Client, NostrSigner, EventBuilder, Filter, Metadata, EventSource, init_logger, LogLevel async def main(): @@ -65,7 +64,8 @@ async def main(): # Get events from relays print("Getting events from relays...") f = Filter().authors([keys.public_key(), custom_keys.public_key()]) - events = await client.get_events_of([f], timedelta(seconds=10)) + source = EventSource.relays(timedelta(seconds=10)) + events = await client.get_events_of([f], source) for event in events: print(event.as_json()) diff --git a/bindings/nostr-sdk-ffi/bindings-python/examples/blacklist.py b/bindings/nostr-sdk-ffi/bindings-python/examples/blacklist.py index c8bf0e816..23bd8f730 100644 --- a/bindings/nostr-sdk-ffi/bindings-python/examples/blacklist.py +++ b/bindings/nostr-sdk-ffi/bindings-python/examples/blacklist.py @@ -1,5 +1,5 @@ import asyncio -from nostr_sdk import PublicKey, Client, Filter, Kind, init_logger, LogLevel +from nostr_sdk import PublicKey, Client, Filter, Kind, init_logger, LogLevel, EventSource from datetime import timedelta @@ -20,7 +20,8 @@ async def main(): # Get events f = Filter().authors([muted_public_key, other_public_key]).kind(Kind(0)) - events = await client.get_events_of([f], timedelta(seconds=10)) + source = EventSource.relays(timedelta(seconds=10)) + events = await client.get_events_of([f], source) print(f"Received {events.__len__()} events") diff --git a/bindings/nostr-sdk-ffi/bindings-python/examples/client.py b/bindings/nostr-sdk-ffi/bindings-python/examples/client.py index ce3d34c0c..7d87603bb 100644 --- a/bindings/nostr-sdk-ffi/bindings-python/examples/client.py +++ b/bindings/nostr-sdk-ffi/bindings-python/examples/client.py @@ -1,7 +1,6 @@ import asyncio from datetime import timedelta -from nostr_sdk import Keys, Client, NostrSigner, EventBuilder, Filter, Metadata, Nip46Signer, init_logger, LogLevel -import time +from nostr_sdk import Keys, Client, NostrSigner, EventBuilder, Filter, Metadata, EventSource, init_logger, LogLevel async def main(): @@ -48,7 +47,8 @@ async def main(): # Get events from relays print("Getting events from relays...") f = Filter().authors([keys.public_key(), custom_keys.public_key()]) - events = await client.get_events_of([f], timedelta(seconds=10)) + source = EventSource.relays(timedelta(seconds=10)) + events = await client.get_events_of([f], source) for event in events: print(event.as_json()) diff --git a/bindings/nostr-sdk-ffi/bindings-python/examples/metadata.py b/bindings/nostr-sdk-ffi/bindings-python/examples/metadata.py index 4dac0031f..3a51d9a14 100644 --- a/bindings/nostr-sdk-ffi/bindings-python/examples/metadata.py +++ b/bindings/nostr-sdk-ffi/bindings-python/examples/metadata.py @@ -1,5 +1,5 @@ import asyncio -from nostr_sdk import Metadata, Client, NostrSigner, Keys, Filter, PublicKey, Kind +from nostr_sdk import Metadata, Client, NostrSigner, Keys, Filter, PublicKey, Kind, EventSource from datetime import timedelta @@ -30,7 +30,8 @@ async def main(): pk = PublicKey.from_bech32("npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet") print(f"\nGetting profile metadata for {pk.to_bech32()}...") f = Filter().kind(Kind(0)).author(pk).limit(1) - events = await client.get_events_of([f], timedelta(seconds=10)) + source = EventSource.relays(timedelta(seconds=10)) + events = await client.get_events_of([f], source) for event in events: metadata = Metadata.from_json(event.content()) print(f"Name: {metadata.get_name()}") diff --git a/bindings/nostr-sdk-ffi/src/client/mod.rs b/bindings/nostr-sdk-ffi/src/client/mod.rs index f620b90ed..3e79f5f5b 100644 --- a/bindings/nostr-sdk-ffi/src/client/mod.rs +++ b/bindings/nostr-sdk-ffi/src/client/mod.rs @@ -23,7 +23,7 @@ pub mod signer; pub mod zapper; pub use self::builder::ClientBuilder; -pub use self::options::Options; +pub use self::options::{EventSource, Options}; pub use self::signer::NostrSigner; use self::zapper::{ZapDetails, ZapEntity}; use crate::error::Result; @@ -356,7 +356,7 @@ impl Client { pub async fn get_events_of( &self, filters: Vec>, - timeout: Option, + source: &EventSource, ) -> Result>> { let filters = filters .into_iter() @@ -365,7 +365,7 @@ impl Client { Ok(self .inner - .get_events_of(filters, timeout) + .get_events_of(filters, source.deref().clone()) .await? .into_iter() .map(|e| Arc::new(e.into())) diff --git a/bindings/nostr-sdk-ffi/src/client/options.rs b/bindings/nostr-sdk-ffi/src/client/options.rs index d97b59535..a49b69fcd 100644 --- a/bindings/nostr-sdk-ffi/src/client/options.rs +++ b/bindings/nostr-sdk-ffi/src/client/options.rs @@ -215,3 +215,59 @@ impl Connection { builder } } + +#[derive(Object)] +pub struct EventSource { + inner: nostr_sdk::EventSource, +} + +impl Deref for EventSource { + type Target = nostr_sdk::EventSource; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[uniffi::export] +impl EventSource { + /// Database only + #[uniffi::constructor] + pub fn database() -> Self { + Self { + inner: nostr_sdk::EventSource::Database, + } + } + + /// Relays only + #[uniffi::constructor(default(timeout = None))] + pub fn relays(timeout: Option) -> Self { + Self { + inner: nostr_sdk::EventSource::relays(timeout), + } + } + + /// From specific relays only + #[uniffi::constructor(default(timeout = None))] + pub fn specific_relays(urls: Vec, timeout: Option) -> Self { + Self { + inner: nostr_sdk::EventSource::specific_relays(urls, timeout), + } + } + + /// Both from database and relays + #[uniffi::constructor(default(timeout = None))] + pub fn both(timeout: Option) -> Self { + Self { + inner: nostr_sdk::EventSource::both(timeout), + } + } + + /// Both from database and specific relays + #[uniffi::constructor(default(timeout = None))] + pub fn both_with_specific_relays(urls: Vec, timeout: Option) -> Self { + Self { + inner: nostr_sdk::EventSource::both_with_specific_relays(urls, timeout), + } + } +} diff --git a/bindings/nostr-sdk-js/examples/get-events-of.js b/bindings/nostr-sdk-js/examples/get-events-of.js index ed3297718..0cdc556cf 100644 --- a/bindings/nostr-sdk-js/examples/get-events-of.js +++ b/bindings/nostr-sdk-js/examples/get-events-of.js @@ -1,4 +1,4 @@ -const { Keys, Client, Filter, loadWasmAsync, Timestamp, Duration } = require("../"); +const { Keys, Client, Filter, loadWasmAsync, Timestamp, Duration, EventSource } = require("../"); async function main() { await loadWasmAsync(); @@ -14,7 +14,8 @@ async function main() { const filter = new Filter().author(keys.publicKey).kind(4).until(Timestamp.now()).limit(10); console.log('filter', filter.asJson()); - let events = await client.getEventsOf([filter], Duration.fromSecs(10)); + let source = EventSource.relays(Duration.fromSecs(10)); + let events = await client.getEventsOf([filter], source); events.forEach((e) => { console.log(e.asJson()) }) diff --git a/bindings/nostr-sdk-js/src/client/mod.rs b/bindings/nostr-sdk-js/src/client/mod.rs index c04ef6840..dc6b9b2b2 100644 --- a/bindings/nostr-sdk-js/src/client/mod.rs +++ b/bindings/nostr-sdk-js/src/client/mod.rs @@ -22,7 +22,7 @@ pub mod signer; pub mod zapper; pub use self::builder::JsClientBuilder; -use self::options::JsOptions; +use self::options::{JsEventSource, JsOptions}; pub use self::signer::JsNostrSigner; use self::zapper::{JsZapDetails, JsZapEntity}; use crate::abortable::JsAbortHandle; @@ -340,13 +340,12 @@ impl JsClient { pub async fn get_events_of( &self, filters: Vec, - timeout: Option, + source: &JsEventSource, ) -> Result { let filters: Vec = filters.into_iter().map(|f| f.into()).collect(); - let timeout: Option = timeout.map(|d| *d); let events: Vec = self .inner - .get_events_of(filters, timeout) + .get_events_of(filters, source.deref().clone()) .await .map_err(into_err)?; let events: JsEventArray = events diff --git a/bindings/nostr-sdk-js/src/client/options.rs b/bindings/nostr-sdk-js/src/client/options.rs index 708218644..32570306c 100644 --- a/bindings/nostr-sdk-js/src/client/options.rs +++ b/bindings/nostr-sdk-js/src/client/options.rs @@ -4,7 +4,7 @@ use std::ops::Deref; -use nostr_sdk::Options; +use nostr_sdk::prelude::*; use wasm_bindgen::prelude::*; use crate::duration::JsDuration; @@ -110,3 +110,56 @@ impl JsOptions { self.inner.relay_limits(limits.deref().clone()).into() } } + +#[wasm_bindgen(js_name = EventSource)] +pub struct JsEventSource { + inner: EventSource, +} + +impl Deref for JsEventSource { + type Target = EventSource; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[wasm_bindgen(js_class = EventSource)] +impl JsEventSource { + /// Database only + pub fn database() -> Self { + Self { + inner: nostr_sdk::EventSource::Database, + } + } + + /// Relays only + pub fn relays(timeout: Option) -> Self { + Self { + inner: nostr_sdk::EventSource::relays(timeout.map(|t| *t)), + } + } + + /// From specific relays only + #[wasm_bindgen(js_name = specificRelays)] + pub fn specific_relays(urls: Vec, timeout: Option) -> Self { + Self { + inner: nostr_sdk::EventSource::specific_relays(urls, timeout.map(|t| *t)), + } + } + + /// Both from database and relays + pub fn both(timeout: Option) -> Self { + Self { + inner: nostr_sdk::EventSource::both(timeout.map(|t| *t)), + } + } + + /// Both from database and specific relays + #[wasm_bindgen(js_name = bothWithSpecificRelays)] + pub fn both_with_specific_relays(urls: Vec, timeout: Option) -> Self { + Self { + inner: nostr_sdk::EventSource::both_with_specific_relays(urls, timeout.map(|t| *t)), + } + } +} diff --git a/crates/nostr-sdk/examples/blacklist.rs b/crates/nostr-sdk/examples/blacklist.rs index 9ddd27da4..59b8c7beb 100644 --- a/crates/nostr-sdk/examples/blacklist.rs +++ b/crates/nostr-sdk/examples/blacklist.rs @@ -27,7 +27,10 @@ async fn main() -> Result<()> { .authors([muted_public_key, public_key]) .kind(Kind::Metadata); let events = client - .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .get_events_of( + vec![filter], + EventSource::relays(Some(Duration::from_secs(10))), + ) .await?; println!("Received {} events.", events.len()); diff --git a/crates/nostr-sdk/examples/get-events-of.rs b/crates/nostr-sdk/examples/get-events-of.rs index 43053b1a1..b8eea3de3 100644 --- a/crates/nostr-sdk/examples/get-events-of.rs +++ b/crates/nostr-sdk/examples/get-events-of.rs @@ -22,7 +22,10 @@ async fn main() -> Result<()> { // Get events from all connected relays let filter = Filter::new().author(public_key).kind(Kind::Metadata); let events = client - .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .get_events_of( + vec![filter], + EventSource::relays(Some(Duration::from_secs(10))), + ) .await?; println!("{events:#?}"); diff --git a/crates/nostr-sdk/examples/nip65.rs b/crates/nostr-sdk/examples/nip65.rs index 276ca8559..a3824422c 100644 --- a/crates/nostr-sdk/examples/nip65.rs +++ b/crates/nostr-sdk/examples/nip65.rs @@ -19,7 +19,10 @@ async fn main() -> Result<()> { let filter = Filter::new().author(public_key).kind(Kind::RelayList); let events: Vec = client - .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .get_events_of( + vec![filter], + EventSource::relays(Some(Duration::from_secs(10))), + ) .await?; let event = events.first().unwrap(); println!("Found relay list metadata:"); diff --git a/crates/nostr-sdk/src/client/mod.rs b/crates/nostr-sdk/src/client/mod.rs index 377c8a6f6..1c13fc221 100644 --- a/crates/nostr-sdk/src/client/mod.rs +++ b/crates/nostr-sdk/src/client/mod.rs @@ -4,8 +4,10 @@ //! Client -use std::collections::HashMap; +use std::collections::btree_set::IntoIter; +use std::collections::{BTreeSet, HashMap}; use std::future::Future; +use std::iter::Rev; use std::sync::Arc; use std::time::Duration; @@ -26,9 +28,9 @@ pub mod options; mod zapper; pub use self::builder::ClientBuilder; -pub use self::options::Options; #[cfg(not(target_arch = "wasm32"))] pub use self::options::{Connection, ConnectionTarget}; +pub use self::options::{EventSource, Options}; #[cfg(feature = "nip57")] pub use self::zapper::{ZapDetails, ZapEntity}; @@ -41,6 +43,9 @@ pub enum Error { /// [`RelayPool`] error #[error(transparent)] RelayPool(#[from] pool::Error), + /// Database error + #[error(transparent)] + Database(#[from] DatabaseError), /// Signer error #[error(transparent)] Signer(#[from] nostr_signer::Error), @@ -811,8 +816,6 @@ impl Client { /// Get events of filters /// - /// If timeout is set to `None`, the default from [`Options`] will be used. - /// /// # Example /// ```rust,no_run /// use std::time::Duration; @@ -827,9 +830,9 @@ impl Client { /// .pubkeys(vec![my_keys.public_key()]) /// .since(Timestamp::now()); /// - /// let timeout = Duration::from_secs(10); + /// let timeout = Some(Duration::from_secs(10)); /// let _events = client - /// .get_events_of(vec![subscription], Some(timeout)) + /// .get_events_of(vec![subscription], EventSource::both(timeout)) /// .await /// .unwrap(); /// # } @@ -838,16 +841,61 @@ impl Client { pub async fn get_events_of( &self, filters: Vec, - timeout: Option, + source: EventSource, ) -> Result, Error> { - self.get_events_of_with_opts(filters, timeout, FilterOptions::ExitOnEOSE) - .await + match source { + EventSource::Database => Ok(self.database().query(filters, Order::Desc).await?), + EventSource::Relays { + timeout, + specific_relays, + } => match specific_relays { + Some(urls) => self.get_events_from(urls, filters, timeout).await, + None => { + let timeout: Duration = timeout.unwrap_or(self.opts.timeout); + Ok(self + .pool + .get_events_of(filters, timeout, FilterOptions::ExitOnEOSE) + .await?) + } + }, + EventSource::Both { + timeout, + specific_relays, + } => { + // Check how many filters are passed and return the limit + let limit: Option = match (filters.len(), filters.first()) { + (1, Some(filter)) => filter.limit, + _ => None, + }; + + let stored = self.database().query(filters.clone(), Order::Desc).await?; + let mut events: BTreeSet = stored.into_iter().collect(); + + let mut stream: ReceiverStream = match specific_relays { + Some(urls) => self.stream_events_from(urls, filters, timeout).await?, + None => self.stream_events_of(filters, timeout).await?, + }; + + while let Some(event) = stream.next().await { + events.insert(event); + } + + let iter: Rev> = events.into_iter().rev(); + + // Check limit + match limit { + Some(limit) => Ok(iter.take(limit).collect()), + None => Ok(iter.collect()), + } + } + } } /// Get events of filters with [`FilterOptions`] /// /// If timeout is set to `None`, the default from [`Options`] will be used. #[inline] + #[deprecated(since = "0.34.0", note = "Use `client.pool().get_events_of(..)`.")] pub async fn get_events_of_with_opts( &self, filters: Vec, @@ -1061,7 +1109,9 @@ impl Client { .author(public_key) .kind(Kind::Metadata) .limit(1); - let events: Vec = self.get_events_of(vec![filter], None).await?; // TODO: add timeout? + let events: Vec = self + .get_events_of(vec![filter], EventSource::both(None)) + .await?; // TODO: add timeout? match events.first() { Some(event) => Ok(Metadata::from_json(event.content())?), None => Err(Error::MetadataNotFound), @@ -1183,7 +1233,9 @@ impl Client { pub async fn get_contact_list(&self, timeout: Option) -> Result, Error> { let mut contact_list: Vec = Vec::new(); let filters: Vec = self.get_contact_list_filters().await?; - let events: Vec = self.get_events_of(filters, timeout).await?; + let events: Vec = self + .get_events_of(filters, EventSource::both(timeout)) + .await?; // Get first event (result of `get_events_of` is sorted DESC by timestamp) if let Some(event) = events.into_iter().next() { @@ -1211,7 +1263,9 @@ impl Client { ) -> Result, Error> { let mut pubkeys: Vec = Vec::new(); let filters: Vec = self.get_contact_list_filters().await?; - let events: Vec = self.get_events_of(filters, timeout).await?; + let events: Vec = self + .get_events_of(filters, EventSource::both(timeout)) + .await?; for event in events.into_iter() { pubkeys.extend(event.public_keys()); @@ -1240,7 +1294,9 @@ impl Client { .limit(1), ); } - let events: Vec = self.get_events_of(filters, timeout).await?; + let events: Vec = self + .get_events_of(filters, EventSource::both(timeout)) + .await?; for event in events.into_iter() { let metadata = Metadata::from_json(event.content())?; if let Some(m) = contacts.get_mut(&event.author()) { diff --git a/crates/nostr-sdk/src/client/options.rs b/crates/nostr-sdk/src/client/options.rs index 0bc778c90..bfb9241dc 100644 --- a/crates/nostr-sdk/src/client/options.rs +++ b/crates/nostr-sdk/src/client/options.rs @@ -308,3 +308,68 @@ impl Connection { self } } + +/// Source of the events +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum EventSource { + /// Database only + Database, + /// Relays only + Relays { + /// Optional timeout + timeout: Option, + /// Specific relays + specific_relays: Option>, + }, + /// Both from database and relays + Both { + /// Optional timeout for relays + timeout: Option, + /// Specific relays + specific_relays: Option>, + }, +} + +impl EventSource { + /// Relays only + #[inline] + pub fn relays(timeout: Option) -> Self { + Self::Relays { + timeout, + specific_relays: None, + } + } + + /// From specific relays only + pub fn specific_relays(urls: I, timeout: Option) -> Self + where + I: IntoIterator, + S: Into, + { + Self::Relays { + timeout, + specific_relays: Some(urls.into_iter().map(|u| u.into()).collect()), + } + } + + /// Both from database and relays + #[inline] + pub fn both(timeout: Option) -> Self { + Self::Both { + timeout, + specific_relays: None, + } + } + + /// Both from database and specific relays + pub fn both_with_specific_relays(urls: I, timeout: Option) -> Self + where + I: IntoIterator, + S: Into, + { + Self::Both { + timeout, + specific_relays: Some(urls.into_iter().map(|u| u.into()).collect()), + } + } +} diff --git a/crates/nostr-sdk/src/client/zapper.rs b/crates/nostr-sdk/src/client/zapper.rs index b69a105c5..909b334e3 100644 --- a/crates/nostr-sdk/src/client/zapper.rs +++ b/crates/nostr-sdk/src/client/zapper.rs @@ -8,7 +8,7 @@ use lnurl_pay::api::Lud06OrLud16; use lnurl_pay::{LightningAddress, LnUrl}; use nostr::prelude::*; -use super::{Client, Error}; +use super::{Client, Error, EventSource}; /// Zap entity #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -100,7 +100,9 @@ impl Client { ZapEntity::Event(event_id) => { // Get event let filter: Filter = Filter::new().id(event_id); - let events: Vec = self.get_events_of(vec![filter], None).await?; + let events: Vec = self + .get_events_of(vec![filter], EventSource::both(Some(self.opts.timeout))) + .await?; let event: &Event = events.first().ok_or(Error::EventNotFound(event_id))?; let public_key: PublicKey = event.author(); let metadata: Metadata = self.metadata(public_key).await?; diff --git a/crates/nostr-sdk/src/lib.rs b/crates/nostr-sdk/src/lib.rs index 17399cbb1..7b5ec85a9 100644 --- a/crates/nostr-sdk/src/lib.rs +++ b/crates/nostr-sdk/src/lib.rs @@ -9,6 +9,7 @@ #![warn(rustdoc::bare_urls)] #![allow(unknown_lints)] // TODO: remove when MSRV >= 1.72.0, required for `clippy::arc_with_non_send_sync` #![allow(clippy::arc_with_non_send_sync)] +#![allow(clippy::mutable_key_type)] // TODO: remove when possible. Needed to suppress false positive for `BTreeSet` #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(feature = "all-nips", doc = include_str!("../README.md"))] @@ -51,4 +52,4 @@ pub use nwc::{self, NostrWalletConnectOptions, NWC}; pub mod client; pub mod prelude; -pub use self::client::{Client, ClientBuilder, Options}; +pub use self::client::{Client, ClientBuilder, EventSource, Options};