Skip to main content

fmp_rust_sdk/
websocket.rs

1//! Current market-stream and legacy cluster WebSocket connections.
2//!
3//! [`StandardWebSocketClient`] supports FMP's current native WebSocket at
4//! `wss://socket.financialmodelingprep.com`, with approved exchange, synthetic,
5//! and delayed streams. [`StreamEntitlements`] explicitly records the approved
6//! streams, while [`Subscription`] records the separately configured plan.
7//!
8//! The legacy [`WebSocketClient`] supports stock, crypto, and forex clusters.
9//! Legacy public docs omit successful login acknowledgments, so its config
10//! requires an account-specific [`LoginConfirmation`]. Both connectors wait
11//! for confirmed authentication before allowing subscriptions and report
12//! active connection failures without silently reconnecting.
13
14mod connection;
15mod protocol;
16mod standard;
17mod streams;
18mod types;
19
20pub use types::{
21    CloseInfo, ControlEvent, LoginConfirmation, MarketUpdate, QuoteUpdate, TradeUpdate,
22    WebSocketCluster, WebSocketConfig, WebSocketError, WebSocketEvent,
23};
24
25use crate::plan::Subscription;
26use connection::{Connection, Socket};
27use protocol::{TickerCommand, TickerData, normalize_symbols, validate_connection};
28use url::Url;
29
30pub use standard::StandardWebSocketClient;
31pub use streams::{
32    AuthenticationStatus, Heartbeat, MarketStream, StreamConfig, StreamEntitlements, StreamQuote,
33};
34
35/// An authenticated connector to FMP's legacy stock, crypto, or forex cluster.
36///
37/// For current market streams, use [`StandardWebSocketClient`].
38#[derive(Debug)]
39pub struct WebSocketClient {
40    connection: Connection,
41    cluster: WebSocketCluster,
42}
43
44impl WebSocketClient {
45    /// Connects to the documented legacy cluster endpoint and confirms login.
46    pub async fn connect(
47        api_key: &str,
48        subscription: &Subscription,
49        cluster: WebSocketCluster,
50        config: WebSocketConfig,
51    ) -> Result<Self, WebSocketError> {
52        let endpoint =
53            Url::parse(cluster.endpoint()).map_err(|error| WebSocketError::Configuration {
54                reason: format!("invalid documented cluster URL: {error}"),
55            })?;
56        Self::connect_to(api_key, subscription, cluster, endpoint, config).await
57    }
58
59    /// Connects to a trusted explicit legacy endpoint; ws is loopback-only.
60    pub async fn connect_to(
61        api_key: &str,
62        subscription: &Subscription,
63        cluster: WebSocketCluster,
64        endpoint: Url,
65        config: WebSocketConfig,
66    ) -> Result<Self, WebSocketError> {
67        validate_connection(api_key, subscription, cluster, &endpoint)?;
68        Ok(Self {
69            connection: Connection::connect(api_key, endpoint, config).await?,
70            cluster,
71        })
72    }
73
74    /// Returns the legacy cluster used by this connector.
75    pub const fn cluster(&self) -> WebSocketCluster {
76        self.cluster
77    }
78
79    /// Subscribes to validated lowercase legacy ticker symbols.
80    pub async fn subscribe(&mut self, symbols: &[&str]) -> Result<(), WebSocketError> {
81        let command = TickerCommand {
82            event: "subscribe",
83            data: TickerData {
84                ticker: normalize_symbols(symbols)?,
85            },
86        };
87        let json = serde_json::to_string(&command).map_err(|error| {
88            self.connection
89                .protocol_error(&error.to_string(), "subscribe serialization")
90        })?;
91        self.connection.send_text(json, "subscribe").await
92    }
93
94    /// Unsubscribes from validated lowercase legacy ticker symbols.
95    pub async fn unsubscribe(&mut self, symbols: &[&str]) -> Result<(), WebSocketError> {
96        let command = TickerCommand {
97            event: "unsubscribe",
98            data: TickerData {
99                ticker: normalize_symbols(symbols)?,
100            },
101        };
102        let json = serde_json::to_string(&command).map_err(|error| {
103            self.connection
104                .protocol_error(&error.to_string(), "unsubscribe serialization")
105        })?;
106        self.connection.send_text(json, "unsubscribe").await
107    }
108
109    /// Receives a typed update, control, pong, or remote close event.
110    pub async fn next_event(&mut self) -> Result<WebSocketEvent, WebSocketError> {
111        self.connection.next_event().await
112    }
113
114    /// Completes a close handshake within the configured timeout.
115    pub async fn close(self) -> Result<(), WebSocketError> {
116        self.connection.close().await
117    }
118}