Skip to main content

hyperstack_sdk/
client.rs

1use crate::auth::{AuthConfig, AuthToken, TokenTransport};
2use crate::config::{ConnectionConfig, HyperStackConfig};
3use crate::connection::{ConnectionManager, ConnectionState};
4use crate::entity::Stack;
5use crate::error::{HyperStackError, SocketIssue};
6use crate::frame::Frame;
7use crate::store::{SharedStore, StoreConfig};
8use crate::view::Views;
9use std::future::Future;
10use std::marker::PhantomData;
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::{broadcast, mpsc};
14
15/// HyperStack client with typed views access.
16///
17/// ```ignore
18/// use hyperstack_sdk::prelude::*;
19/// use hyperstack_stacks::ore::OreStack;
20///
21/// let hs = HyperStack::<OreStack>::connect().await?;
22/// let rounds = hs.views.latest().get().await;
23/// ```
24pub struct HyperStack<S: Stack> {
25    connection: ConnectionManager,
26    store: SharedStore,
27    #[allow(dead_code)]
28    config: HyperStackConfig,
29    pub views: S::Views,
30    _stack: PhantomData<S>,
31}
32
33impl<S: Stack> HyperStack<S> {
34    /// Connect to the stack's default URL.
35    pub async fn connect() -> Result<Self, HyperStackError> {
36        Self::builder().connect().await
37    }
38
39    /// Connect with custom URL.
40    pub async fn connect_url(url: &str) -> Result<Self, HyperStackError> {
41        Self::builder().url(url).connect().await
42    }
43
44    /// Create a builder for custom configuration.
45    pub fn builder() -> HyperStackBuilder<S> {
46        HyperStackBuilder::new()
47    }
48
49    pub async fn connection_state(&self) -> ConnectionState {
50        self.connection.state().await
51    }
52
53    pub async fn last_error(&self) -> Option<Arc<HyperStackError>> {
54        self.connection.last_error().await
55    }
56
57    pub async fn last_socket_issue(&self) -> Option<SocketIssue> {
58        self.connection.last_socket_issue().await
59    }
60
61    pub fn subscribe_socket_issues(&self) -> broadcast::Receiver<SocketIssue> {
62        self.connection.subscribe_socket_issues()
63    }
64
65    pub async fn disconnect(&self) {
66        self.connection.disconnect().await;
67    }
68
69    pub fn store(&self) -> &SharedStore {
70        &self.store
71    }
72}
73
74/// Builder for HyperStack with custom configuration.
75pub struct HyperStackBuilder<S: Stack> {
76    url: String,
77    config: HyperStackConfig,
78    _stack: PhantomData<S>,
79}
80
81impl<S: Stack> HyperStackBuilder<S> {
82    fn new() -> Self {
83        Self {
84            url: S::url().to_string(),
85            config: HyperStackConfig::default(),
86            _stack: PhantomData,
87        }
88    }
89
90    pub fn url(mut self, url: &str) -> Self {
91        self.url = url.to_string();
92        self
93    }
94
95    pub fn auto_reconnect(mut self, enabled: bool) -> Self {
96        self.config.auto_reconnect = enabled;
97        self
98    }
99
100    pub fn reconnect_intervals(mut self, intervals: Vec<Duration>) -> Self {
101        self.config.reconnect_intervals = intervals;
102        self
103    }
104
105    pub fn max_reconnect_attempts(mut self, max: u32) -> Self {
106        self.config.max_reconnect_attempts = max;
107        self
108    }
109
110    pub fn ping_interval(mut self, interval: Duration) -> Self {
111        self.config.ping_interval = interval;
112        self
113    }
114
115    pub fn initial_data_timeout(mut self, timeout: Duration) -> Self {
116        self.config.initial_data_timeout = timeout;
117        self
118    }
119
120    pub fn max_entries_per_view(mut self, max: usize) -> Self {
121        self.config.max_entries_per_view = Some(max);
122        self
123    }
124
125    pub fn unlimited_entries(mut self) -> Self {
126        self.config.max_entries_per_view = None;
127        self
128    }
129
130    pub fn auth(mut self, auth: AuthConfig) -> Self {
131        self.config.auth = Some(auth);
132        self
133    }
134
135    pub fn auth_token(mut self, token: impl Into<String>) -> Self {
136        let auth = self
137            .config
138            .auth
139            .take()
140            .unwrap_or_default()
141            .with_token(token);
142        self.config.auth = Some(auth);
143        self
144    }
145
146    pub fn publishable_key(mut self, publishable_key: impl Into<String>) -> Self {
147        let auth = self
148            .config
149            .auth
150            .take()
151            .unwrap_or_default()
152            .with_publishable_key(publishable_key);
153        self.config.auth = Some(auth);
154        self
155    }
156
157    /// Alias for `publishable_key` - use this for server-side code where
158    /// the key could be either a secret key or a publishable key.
159    pub fn api_key(self, api_key: impl Into<String>) -> Self {
160        self.publishable_key(api_key)
161    }
162
163    pub fn token_endpoint(mut self, token_endpoint: impl Into<String>) -> Self {
164        let auth = self
165            .config
166            .auth
167            .take()
168            .unwrap_or_default()
169            .with_token_endpoint(token_endpoint);
170        self.config.auth = Some(auth);
171        self
172    }
173
174    pub fn token_endpoint_header(
175        mut self,
176        key: impl Into<String>,
177        value: impl Into<String>,
178    ) -> Self {
179        let auth = self
180            .config
181            .auth
182            .take()
183            .unwrap_or_default()
184            .with_token_endpoint_header(key, value);
185        self.config.auth = Some(auth);
186        self
187    }
188
189    pub fn token_transport(mut self, transport: TokenTransport) -> Self {
190        let auth = self
191            .config
192            .auth
193            .take()
194            .unwrap_or_default()
195            .with_token_transport(transport);
196        self.config.auth = Some(auth);
197        self
198    }
199
200    pub fn get_token<F, Fut>(mut self, provider: F) -> Self
201    where
202        F: Fn() -> Fut + Send + Sync + 'static,
203        Fut: Future<Output = Result<AuthToken, HyperStackError>> + Send + 'static,
204    {
205        let auth = self
206            .config
207            .auth
208            .take()
209            .unwrap_or_default()
210            .with_token_provider(provider);
211        self.config.auth = Some(auth);
212        self
213    }
214
215    pub async fn connect(self) -> Result<HyperStack<S>, HyperStackError> {
216        let HyperStackBuilder {
217            url,
218            config,
219            _stack: _,
220        } = self;
221
222        let store_config = StoreConfig {
223            max_entries_per_view: config.max_entries_per_view,
224        };
225        let store = SharedStore::with_config(store_config);
226        let store_clone = store.clone();
227
228        let (frame_tx, mut frame_rx) = mpsc::channel::<Frame>(1000);
229
230        let connection_config: ConnectionConfig = config.clone().into();
231        let connection = ConnectionManager::new(url, connection_config, frame_tx).await?;
232
233        tokio::spawn(async move {
234            while let Some(frame) = frame_rx.recv().await {
235                store_clone.apply_frame(frame).await;
236            }
237        });
238
239        let view_builder = crate::view::ViewBuilder::new(
240            connection.clone(),
241            store.clone(),
242            config.initial_data_timeout,
243        );
244        let views = S::Views::from_builder(view_builder);
245
246        Ok(HyperStack {
247            connection,
248            store,
249            config,
250            views,
251            _stack: PhantomData,
252        })
253    }
254}