finance_query/streaming/
client.rs1use std::pin::Pin;
8use std::sync::Arc;
9use std::task::{Context, Poll};
10use std::time::Duration;
11
12use futures::stream::Stream;
13use tokio::sync::{broadcast, mpsc};
14use tokio_stream::wrappers::BroadcastStream;
15use tracing::warn;
16
17use super::pricing::PriceUpdate;
18use super::source::{StreamCommand, StreamSource, run_stream_loop};
19use super::yahoo::YahooStreamSource;
20use crate::error::FinanceError;
21
22pub type StreamResult<T> = std::result::Result<T, StreamError>;
24
25#[derive(Debug, Clone)]
27pub enum StreamError {
28 ConnectionFailed(String),
30 WebSocketError(String),
32 DecodeError(String),
34}
35
36impl std::fmt::Display for StreamError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 StreamError::ConnectionFailed(e) => write!(f, "Connection failed: {}", e),
40 StreamError::WebSocketError(e) => write!(f, "WebSocket error: {}", e),
41 StreamError::DecodeError(e) => write!(f, "Decode error: {}", e),
42 }
43 }
44}
45
46impl std::error::Error for StreamError {}
47
48impl From<StreamError> for FinanceError {
49 fn from(e: StreamError) -> Self {
50 FinanceError::ResponseStructureError {
51 field: "streaming".to_string(),
52 context: e.to_string(),
53 }
54 }
55}
56
57const RECONNECT_BACKOFF_SECS: u64 = 3;
59
60const CHANNEL_CAPACITY: usize = 1024;
62
63pub struct PriceStream {
90 inner: BroadcastStream<PriceUpdate>,
91 _handle: Arc<StreamHandle>,
92}
93
94struct StreamHandle {
96 command_tx: mpsc::Sender<StreamCommand>,
97 broadcast_tx: broadcast::Sender<PriceUpdate>,
98}
99
100impl PriceStream {
101 pub async fn subscribe(symbols: &[&str]) -> StreamResult<Self> {
118 Self::subscribe_with_source(
119 Arc::new(YahooStreamSource),
120 symbols,
121 Duration::from_secs(RECONNECT_BACKOFF_SECS),
122 )
123 .await
124 }
125
126 pub(crate) async fn subscribe_with_source(
131 source: Arc<dyn StreamSource>,
132 symbols: &[&str],
133 retry_delay: Duration,
134 ) -> StreamResult<Self> {
135 let (broadcast_tx, broadcast_rx) = broadcast::channel(CHANNEL_CAPACITY);
136 let (command_tx, command_rx) = mpsc::channel(32);
137
138 let initial_symbols: Vec<String> = symbols.iter().map(|s| s.to_string()).collect();
139
140 let tx_clone = broadcast_tx.clone();
141
142 tokio::spawn(run_stream_loop(
144 source,
145 initial_symbols,
146 broadcast_tx,
147 command_rx,
148 retry_delay,
149 ));
150
151 let handle = Arc::new(StreamHandle {
152 command_tx,
153 broadcast_tx: tx_clone,
154 });
155
156 Ok(PriceStream {
157 inner: BroadcastStream::new(broadcast_rx),
158 _handle: handle,
159 })
160 }
161
162 pub fn resubscribe(&self) -> Self {
166 PriceStream {
167 inner: BroadcastStream::new(self._handle.broadcast_tx.subscribe()),
168 _handle: Arc::clone(&self._handle),
169 }
170 }
171
172 pub async fn add_symbols(&self, symbols: &[&str]) {
186 let symbols: Vec<String> = symbols.iter().map(|s| s.to_string()).collect();
187 let _ = self
188 ._handle
189 .command_tx
190 .send(StreamCommand::Subscribe(symbols))
191 .await;
192 }
193
194 pub async fn remove_symbols(&self, symbols: &[&str]) {
208 let symbols: Vec<String> = symbols.iter().map(|s| s.to_string()).collect();
209 let _ = self
210 ._handle
211 .command_tx
212 .send(StreamCommand::Unsubscribe(symbols))
213 .await;
214 }
215
216 pub async fn close(&self) {
218 let _ = self._handle.command_tx.send(StreamCommand::Close).await;
219 }
220}
221
222impl Stream for PriceStream {
223 type Item = PriceUpdate;
224
225 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
226 match Pin::new(&mut self.inner).poll_next(cx) {
227 Poll::Ready(Some(Ok(data))) => Poll::Ready(Some(data)),
228 Poll::Ready(Some(Err(e))) => {
229 warn!("Broadcast error: {:?}", e);
230 cx.waker().wake_by_ref();
232 Poll::Pending
233 }
234 Poll::Ready(None) => Poll::Ready(None),
235 Poll::Pending => Poll::Pending,
236 }
237 }
238}
239
240pub struct PriceStreamBuilder {
242 symbols: Vec<String>,
243 retry_delay: Duration,
244}
245
246impl PriceStreamBuilder {
247 pub fn new() -> Self {
249 Self {
250 symbols: Vec::new(),
251 retry_delay: Duration::from_secs(RECONNECT_BACKOFF_SECS),
252 }
253 }
254
255 pub fn symbols(mut self, symbols: &[&str]) -> Self {
257 self.symbols.extend(symbols.iter().map(|s| s.to_string()));
258 self
259 }
260
261 pub fn retry(mut self, delay: Duration) -> Self {
263 self.retry_delay = delay;
264 self
265 }
266
267 pub async fn build(self) -> StreamResult<PriceStream> {
269 let symbol_refs: Vec<&str> = self.symbols.iter().map(|s| s.as_str()).collect();
270 PriceStream::subscribe_with_source(
271 Arc::new(YahooStreamSource),
272 &symbol_refs,
273 self.retry_delay,
274 )
275 .await
276 }
277}
278
279impl Default for PriceStreamBuilder {
280 fn default() -> Self {
281 Self::new()
282 }
283}