use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use futures::SinkExt;
use tokio::sync::{RwLock, broadcast, mpsc};
use tokio::time::interval;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use tracing::{debug, error, info, warn};
use super::client::{StreamError, StreamResult};
use super::pricing::{PriceUpdate, PricingData, PricingDecodeError};
use super::source::{StreamCommand, StreamSource};
const YAHOO_WS_URL: &str = "wss://streamer.finance.yahoo.com/?version=2";
const HEARTBEAT_INTERVAL_SECS: u64 = 15;
pub(crate) struct YahooStreamSource;
#[async_trait::async_trait]
impl StreamSource for YahooStreamSource {
fn id(&self) -> &'static str {
"yahoo"
}
async fn run_session(
&self,
subscriptions: &Arc<RwLock<HashSet<String>>>,
broadcast_tx: &broadcast::Sender<PriceUpdate>,
command_rx: &mut mpsc::Receiver<StreamCommand>,
) -> StreamResult<()> {
connect_and_stream(subscriptions, broadcast_tx, command_rx).await
}
}
async fn connect_and_stream(
subscriptions: &Arc<RwLock<HashSet<String>>>,
broadcast_tx: &broadcast::Sender<PriceUpdate>,
command_rx: &mut mpsc::Receiver<StreamCommand>,
) -> StreamResult<()> {
use futures::StreamExt;
info!("Connecting to Yahoo Finance WebSocket...");
let (ws_stream, _) = connect_async(YAHOO_WS_URL)
.await
.map_err(|e| StreamError::ConnectionFailed(e.to_string()))?;
info!("Connected to Yahoo Finance WebSocket");
let (mut write, mut read) = ws_stream.split();
{
let subs = subscriptions.read().await;
if !subs.is_empty() {
let symbols: Vec<&str> = subs.iter().map(|s| s.as_str()).collect();
let msg = serde_json::json!({ "subscribe": symbols });
write
.send(Message::Text(msg.to_string().into()))
.await
.map_err(|e| StreamError::WebSocketError(e.to_string()))?;
info!("Subscribed to {} symbols", symbols.len());
}
}
let heartbeat_subs = Arc::clone(subscriptions);
let (heartbeat_tx, mut heartbeat_rx) = mpsc::channel::<Message>(32);
tokio::spawn(async move {
let mut interval = interval(Duration::from_secs(HEARTBEAT_INTERVAL_SECS));
loop {
interval.tick().await;
let subs = heartbeat_subs.read().await;
if !subs.is_empty() {
let symbols: Vec<&str> = subs.iter().map(|s| s.as_str()).collect();
let msg = serde_json::json!({ "subscribe": symbols });
if heartbeat_tx
.send(Message::Text(msg.to_string().into()))
.await
.is_err()
{
break;
}
debug!("Heartbeat subscription sent for {} symbols", symbols.len());
}
}
});
loop {
tokio::select! {
Some(msg) = read.next() => {
match msg {
Ok(Message::Text(text)) => {
if let Err(e) = handle_text_message(&text, broadcast_tx) {
warn!("Failed to handle message: {}", e);
}
}
Ok(Message::Binary(data)) => {
debug!("Received binary message: {} bytes", data.len());
}
Ok(Message::Close(_)) => {
info!("Received close frame");
break;
}
Ok(Message::Ping(data)) => {
let _ = write.send(Message::Pong(data)).await;
}
Ok(_) => {}
Err(e) => {
error!("WebSocket read error: {}", e);
return Err(StreamError::WebSocketError(e.to_string()));
}
}
}
Some(msg) = heartbeat_rx.recv() => {
if let Err(e) = write.send(msg).await {
error!("Failed to send heartbeat: {}", e);
return Err(StreamError::WebSocketError(e.to_string()));
}
}
Some(cmd) = command_rx.recv() => {
match cmd {
StreamCommand::Subscribe(symbols) => {
let mut newly_added = Vec::new();
{
let mut subs = subscriptions.write().await;
for s in &symbols {
if subs.insert(s.clone()) {
newly_added.push(s.clone());
}
}
}
if !newly_added.is_empty() {
let msg = serde_json::json!({ "subscribe": newly_added });
let _ = write.send(Message::Text(msg.to_string().into())).await;
info!("Added subscriptions: {:?}", newly_added);
}
}
StreamCommand::Unsubscribe(symbols) => {
let mut actually_removed = Vec::new();
{
let mut subs = subscriptions.write().await;
for s in &symbols {
if subs.remove(s) {
actually_removed.push(s.clone());
}
}
}
if !actually_removed.is_empty() {
let msg = serde_json::json!({ "unsubscribe": actually_removed });
let _ = write.send(Message::Text(msg.to_string().into())).await;
info!("Removed subscriptions: {:?}", actually_removed);
}
}
StreamCommand::Close => {
info!("Received close command");
let _ = write.send(Message::Close(None)).await;
return Ok(());
}
}
}
else => break,
}
}
Ok(())
}
fn handle_text_message(
text: &str,
broadcast_tx: &broadcast::Sender<PriceUpdate>,
) -> std::result::Result<(), PricingDecodeError> {
let json: serde_json::Value =
serde_json::from_str(text).map_err(|e| PricingDecodeError::Base64(e.to_string()))?;
if let Some(encoded) = json.get("message").and_then(|v| v.as_str()) {
let pricing_data = PricingData::from_base64(encoded)?;
let price_update: PriceUpdate = pricing_data.into();
if broadcast_tx.receiver_count() > 0 {
let _ = broadcast_tx.send(price_update);
}
}
Ok(())
}