oanda_rs/streaming/
mod.rs1mod json_lines;
28mod managed;
29
30use std::pin::Pin;
31use std::task::{Context, Poll};
32use std::time::Duration;
33
34use futures_core::Stream;
35use futures_util::StreamExt;
36use reqwest::{Method, Url};
37
38pub use managed::StreamStats;
39
40use crate::client::Client;
41use crate::error::Error;
42use crate::models::transaction::TransactionStreamItem;
43use crate::models::{AccountId, PriceStreamItem};
44pub(crate) use managed::StreamKind;
45use managed::{ByteStream, ManagedStream};
46
47#[derive(Debug, Clone)]
49pub(crate) struct StreamConfig {
50 pub(crate) auto_reconnect: bool,
51 pub(crate) heartbeat_timeout: Duration,
52 pub(crate) backoff_initial: Duration,
53 pub(crate) backoff_max: Duration,
54 pub(crate) backoff_reset_after: Duration,
55 pub(crate) max_reconnect_attempts: Option<u32>,
56}
57
58impl Default for StreamConfig {
59 fn default() -> Self {
60 StreamConfig {
61 auto_reconnect: true,
62 heartbeat_timeout: Duration::from_secs(10),
63 backoff_initial: Duration::from_secs(1),
64 backoff_max: Duration::from_secs(300),
65 backoff_reset_after: Duration::from_secs(60),
66 max_reconnect_attempts: None,
67 }
68 }
69}
70
71pub(crate) async fn open_stream(client: Client, url: Url) -> Result<ByteStream, Error> {
74 client.acquire_connection_slot().await;
75 let response = client.request(Method::GET, url).send().await?;
76 if !response.status().is_success() {
77 return Err(crate::transport::error_from_response(response).await);
78 }
79 Ok(response.bytes_stream().boxed())
80}
81
82pub(crate) struct PricingKind {
83 pub(crate) client: Client,
84 pub(crate) account_id: AccountId,
85 pub(crate) instruments: String,
86 pub(crate) snapshot: Option<bool>,
87}
88
89impl PricingKind {
90 fn url(&self, reconnect: bool) -> Url {
91 let mut url =
92 self.client
93 .stream_url(&["accounts", self.account_id.as_str(), "pricing", "stream"]);
94 {
95 let mut query = url.query_pairs_mut();
96 query.append_pair("instruments", &self.instruments);
97 let snapshot = if reconnect { Some(true) } else { self.snapshot };
100 if let Some(snapshot) = snapshot {
101 query.append_pair("snapshot", if snapshot { "true" } else { "false" });
102 }
103 }
104 url
105 }
106}
107
108impl StreamKind for PricingKind {
109 type Item = PriceStreamItem;
110
111 fn connect(
112 &mut self,
113 reconnect: bool,
114 ) -> futures_core::future::BoxFuture<'static, Result<ByteStream, Error>> {
115 let client = self.client.clone();
116 let url = self.url(reconnect);
117 Box::pin(open_stream(client, url))
118 }
119}
120
121pub struct PricingStream {
124 inner: ManagedStream<PricingKind>,
125}
126
127impl PricingStream {
128 pub(crate) fn new(kind: PricingKind, config: StreamConfig, initial: ByteStream) -> Self {
129 PricingStream {
130 inner: ManagedStream::new(kind, config, initial),
131 }
132 }
133
134 pub fn stats(&self) -> StreamStats {
136 self.inner.stats()
137 }
138}
139
140impl Stream for PricingStream {
141 type Item = Result<PriceStreamItem, Error>;
142
143 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
144 Pin::new(&mut self.inner).poll_next(cx)
145 }
146}
147
148impl std::fmt::Debug for PricingStream {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("PricingStream")
151 .field("stats", &self.stats())
152 .finish_non_exhaustive()
153 }
154}
155
156pub(crate) struct TransactionKind {
157 pub(crate) client: Client,
158 pub(crate) account_id: AccountId,
159 pub(crate) last_seen: Option<u64>,
162}
163
164impl StreamKind for TransactionKind {
165 type Item = TransactionStreamItem;
166
167 fn connect(
168 &mut self,
169 _reconnect: bool,
170 ) -> futures_core::future::BoxFuture<'static, Result<ByteStream, Error>> {
171 let client = self.client.clone();
172 let url = self.client.stream_url(&[
173 "accounts",
174 self.account_id.as_str(),
175 "transactions",
176 "stream",
177 ]);
178 Box::pin(open_stream(client, url))
179 }
180
181 fn filter(&mut self, item: &TransactionStreamItem) -> bool {
182 if let TransactionStreamItem::Transaction(tx) = item {
183 if let Some(id) = tx.id().and_then(|id| id.as_str().parse::<u64>().ok()) {
184 if self.last_seen.is_some_and(|seen| id <= seen) {
185 return false; }
187 self.last_seen = Some(id);
188 }
189 }
190 true
191 }
192
193 fn backfill(
194 &mut self,
195 ) -> Option<futures_core::future::BoxFuture<'static, Result<Vec<TransactionStreamItem>, Error>>>
196 {
197 let last_seen = self.last_seen?;
198 let client = self.client.clone();
199 let account_id = self.account_id.clone();
200 Some(Box::pin(async move {
201 let response = client
202 .transactions_since_id(account_id, last_seen.to_string())
203 .await?;
204 Ok(response
205 .transactions
206 .into_iter()
207 .map(TransactionStreamItem::Transaction)
208 .collect())
209 }))
210 }
211}
212
213pub struct TransactionStream {
217 inner: ManagedStream<TransactionKind>,
218}
219
220impl TransactionStream {
221 pub(crate) fn new(kind: TransactionKind, config: StreamConfig, initial: ByteStream) -> Self {
222 TransactionStream {
223 inner: ManagedStream::new(kind, config, initial),
224 }
225 }
226
227 pub fn stats(&self) -> StreamStats {
229 self.inner.stats()
230 }
231}
232
233impl Stream for TransactionStream {
234 type Item = Result<TransactionStreamItem, Error>;
235
236 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
237 Pin::new(&mut self.inner).poll_next(cx)
238 }
239}
240
241impl std::fmt::Debug for TransactionStream {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 f.debug_struct("TransactionStream")
244 .field("stats", &self.stats())
245 .finish_non_exhaustive()
246 }
247}
248
249macro_rules! stream_config_setters {
252 () => {
253 pub fn auto_reconnect(mut self, enabled: bool) -> Self {
257 self.config.auto_reconnect = enabled;
258 self
259 }
260
261 pub fn heartbeat_timeout(mut self, timeout: std::time::Duration) -> Self {
264 self.config.heartbeat_timeout = timeout;
265 self
266 }
267
268 pub fn backoff(mut self, initial: std::time::Duration, max: std::time::Duration) -> Self {
273 self.config.backoff_initial = initial;
274 self.config.backoff_max = max;
275 self
276 }
277
278 pub fn backoff_reset_after(mut self, stable: std::time::Duration) -> Self {
282 self.config.backoff_reset_after = stable;
283 self
284 }
285
286 pub fn max_reconnect_attempts(mut self, attempts: u32) -> Self {
290 self.config.max_reconnect_attempts = Some(attempts);
291 self
292 }
293 };
294}
295
296pub(crate) use stream_config_setters;