1use std::{future::Future, time::Duration};
13
14use cynic::SubscriptionBuilder;
15use futures::{Stream, StreamExt};
16use iota_types::{SignedTransaction, TransactionDigest};
17use reqwest::Url;
18
19use crate::{
20 GraphQLClient,
21 client::response_to_result,
22 error::{GraphQLError, GraphQLResult, query_error},
23 query_types::{
24 Event, EventSubscriptionPayload, EventsSubscription, EventsSubscriptionArgs,
25 SubscriptionEventFilter, SubscriptionTransactionFilter,
26 TransactionBlockSubscriptionPayload, TransactionsSubscription,
27 TransactionsSubscriptionArgs,
28 },
29};
30
31const INITIAL_BACKOFF: Duration = Duration::from_millis(250);
35const MAX_BACKOFF: Duration = Duration::from_secs(5);
37const WS_PROTOCOL: &str = "graphql-transport-ws";
39
40enum Outcome<T> {
42 Item { value: T, cursor: Option<String> },
45 Lagged(i32),
47 Skip,
50}
51
52fn subscription_response_to_result<T>(response: cynic::GraphQlResponse<T>) -> GraphQLResult<T> {
56 response_to_result(cynic::GraphQlResponse {
57 data: response.data,
58 errors: response
59 .errors
60 .map(|errors| errors.into_iter().map(query_error).collect()),
61 })
62}
63
64#[must_use]
67pub struct EventsSubscriptionBuilder {
68 client: GraphQLClient,
69 filter: Option<SubscriptionEventFilter>,
70 start_after: Option<String>,
71}
72
73impl EventsSubscriptionBuilder {
74 pub fn filter(mut self, filter: SubscriptionEventFilter) -> Self {
76 self.filter = Some(filter);
77 self
78 }
79
80 pub fn start_after(mut self, start_after: TransactionDigest) -> Self {
83 self.start_after = Some(start_after.to_string());
84 self
85 }
86
87 pub fn subscribe(self) -> impl Stream<Item = GraphQLResult<Event>> + Unpin {
90 let Self {
91 client,
92 filter,
93 start_after,
94 } = self;
95 reconnecting_subscription(
96 move |cursor| {
97 let filter = filter.clone().map(Into::into);
98 let client = client.clone();
99 async move {
100 let operation = EventsSubscription::build(EventsSubscriptionArgs {
101 start_after: cursor,
102 filter,
103 });
104 let subscription = client.open_subscription(operation).await?;
105
106 let mut current_tx: Option<String> = None;
111 let mapped = subscription.map(move |item| -> GraphQLResult<Outcome<Event>> {
112 let data = subscription_response_to_result(
113 item.map_err(GraphQLError::subscription)?,
114 )?;
115 Ok(match data.events {
116 EventSubscriptionPayload::Event(event) => {
117 let digest = event.transaction_digest();
118 let mut cursor = None;
119 if let Some(new) = &digest
120 && current_tx.as_deref() != Some(new.as_str())
121 {
122 cursor = current_tx.take();
123 current_tx = Some(new.clone());
124 }
125 Outcome::Item {
126 value: Event::from(*event),
127 cursor,
128 }
129 }
130 EventSubscriptionPayload::Lagged(lagged) => {
131 Outcome::Lagged(lagged.count)
132 }
133 EventSubscriptionPayload::Unknown => Outcome::Skip,
134 })
135 });
136 Ok(mapped.boxed())
137 }
138 },
139 start_after,
140 )
141 }
142}
143
144#[must_use]
147pub struct TransactionsSubscriptionBuilder {
148 client: GraphQLClient,
149 filter: Option<SubscriptionTransactionFilter>,
150 start_after: Option<String>,
151}
152
153impl TransactionsSubscriptionBuilder {
154 pub fn filter(mut self, filter: SubscriptionTransactionFilter) -> Self {
156 self.filter = Some(filter);
157 self
158 }
159
160 pub fn start_after(mut self, start_after: TransactionDigest) -> Self {
163 self.start_after = Some(start_after.to_string());
164 self
165 }
166
167 pub fn subscribe(self) -> impl Stream<Item = GraphQLResult<SignedTransaction>> + Unpin {
170 let Self {
171 client,
172 filter,
173 start_after,
174 } = self;
175 reconnecting_subscription(
176 move |cursor| {
177 let filter = filter.clone().map(Into::into);
178 let client = client.clone();
179 async move {
180 let operation = TransactionsSubscription::build(TransactionsSubscriptionArgs {
181 start_after: cursor,
182 filter,
183 });
184 let subscription = client.open_subscription(operation).await?;
185
186 let mapped =
187 subscription.map(|item| -> GraphQLResult<Outcome<SignedTransaction>> {
188 let data = subscription_response_to_result(
189 item.map_err(GraphQLError::subscription)?,
190 )?;
191 Ok(match data.transactions {
192 TransactionBlockSubscriptionPayload::TransactionBlock(block) => {
193 let cursor = block.digest.clone();
194 Outcome::Item {
195 value: SignedTransaction::try_from(block)?,
196 cursor,
197 }
198 }
199 TransactionBlockSubscriptionPayload::Lagged(lagged) => {
200 Outcome::Lagged(lagged.count)
201 }
202 TransactionBlockSubscriptionPayload::Unknown => Outcome::Skip,
203 })
204 });
205 Ok(mapped.boxed())
206 }
207 },
208 start_after,
209 )
210 }
211}
212
213impl GraphQLClient {
214 pub fn events_subscription(&self) -> EventsSubscriptionBuilder {
220 EventsSubscriptionBuilder {
221 client: self.clone(),
222 filter: None,
223 start_after: None,
224 }
225 }
226
227 pub fn transactions_subscription(&self) -> TransactionsSubscriptionBuilder {
233 TransactionsSubscriptionBuilder {
234 client: self.clone(),
235 filter: None,
236 start_after: None,
237 }
238 }
239
240 fn ws_url(&self) -> GraphQLResult<Url> {
243 let mut url = self.rpc.clone();
244 match url.scheme() {
245 "https" => url.set_scheme("wss"),
246 "http" => url.set_scheme("ws"),
247 "ws" | "wss" => Ok(()),
248 other => {
249 return Err(GraphQLError::UnsupportedSubscriptionScheme(
250 other.to_owned(),
251 ));
252 }
253 }
254 .map_err(|_| GraphQLError::subscription("failed to derive the WebSocket URL"))?;
255 url.set_path("/subscriptions");
256 Ok(url)
257 }
258
259 async fn open_subscription<Operation>(
261 &self,
262 operation: Operation,
263 ) -> GraphQLResult<graphql_ws_client::Subscription<Operation>>
264 where
265 Operation: graphql_ws_client::graphql::GraphqlOperation + Unpin + Send + 'static,
266 {
267 let connection = connect(&self.ws_url()?).await?;
268 graphql_ws_client::Client::build(connection)
269 .subscribe(operation)
270 .await
271 .map_err(GraphQLError::subscription)
272 }
273}
274
275#[cfg(not(target_arch = "wasm32"))]
282async fn connect(url: &Url) -> GraphQLResult<impl graphql_ws_client::Connection + Send + 'static> {
283 use tokio_tungstenite::tungstenite::{client::IntoClientRequest, http::HeaderValue};
284
285 let mut request = url
286 .as_str()
287 .into_client_request()
288 .map_err(GraphQLError::subscription)?;
289 request.headers_mut().insert(
290 "Sec-WebSocket-Protocol",
291 HeaderValue::from_static(WS_PROTOCOL),
292 );
293 let (connection, _response) = tokio_tungstenite::connect_async(request)
294 .await
295 .map_err(GraphQLError::subscription)?;
296 Ok(connection)
297}
298
299#[cfg(target_arch = "wasm32")]
300async fn connect(url: &Url) -> GraphQLResult<impl graphql_ws_client::Connection + Send + 'static> {
301 let connection = ws_stream_wasm::WsMeta::connect(url.as_str(), Some(vec![WS_PROTOCOL]))
302 .await
303 .map_err(GraphQLError::subscription)?;
304 Ok(graphql_ws_client::ws_stream_wasm::Connection::new(connection).await)
305}
306
307fn reconnecting_subscription<'a, T, C, Fut, S>(
314 connect: C,
315 initial_cursor: Option<String>,
316) -> impl Stream<Item = GraphQLResult<T>> + Unpin + 'a
317where
318 T: 'a,
319 C: Fn(Option<String>) -> Fut + 'a,
320 Fut: Future<Output = GraphQLResult<S>> + 'a,
321 S: Stream<Item = GraphQLResult<Outcome<T>>> + Unpin + 'a,
322{
323 Box::pin(async_stream::stream! {
324 let mut cursor = initial_cursor;
325 let mut backoff = INITIAL_BACKOFF;
326 loop {
327 match connect(cursor.clone()).await {
328 Ok(mut subscription) => {
329 while let Some(item) = subscription.next().await {
330 match item {
331 Ok(Outcome::Item { value, cursor: next }) => {
332 if next.is_some() {
333 cursor = next;
334 }
335 backoff = INITIAL_BACKOFF;
336 yield Ok(value);
337 }
338 Ok(Outcome::Lagged(count)) => yield Err(GraphQLError::Lagged {
341 count: u32::try_from(count).unwrap_or(0),
342 }),
343 Ok(Outcome::Skip) => {}
344 Err(error) => {
345 yield Err(error);
346 break;
347 }
348 }
349 }
350 }
351 Err(error) => yield Err(error),
352 }
353 crate::wait::sleep(backoff).await;
354 backoff = (backoff * 2).min(MAX_BACKOFF);
355 }
356 })
357}
358
359#[cfg(all(test, not(target_arch = "wasm32")))]
360mod tests {
361 use cynic::{GraphQlError as CynicError, GraphQlErrorPathSegment, GraphQlResponse};
362
363 use super::*;
364
365 #[test]
366 fn errors_surface_as_query_errors_without_a_code() {
367 let response = GraphQlResponse {
368 data: None::<()>,
369 errors: Some(vec![CynicError::new(
370 "boom".to_owned(),
371 None,
372 Some(vec![GraphQlErrorPathSegment::Field("events".to_owned())]),
373 Some(Default::default()),
374 )]),
375 };
376
377 let GraphQLError::Query(errors) = subscription_response_to_result(response).unwrap_err()
378 else {
379 panic!("expected GraphQLError::Query");
380 };
381 assert_eq!(errors.len(), 1);
382 assert_eq!(errors[0].message, "boom");
383 assert_eq!(
384 errors[0].path,
385 Some(vec![GraphQlErrorPathSegment::Field("events".to_owned())])
386 );
387 assert_eq!(errors[0].extensions, None);
388 }
389
390 #[test]
391 fn data_without_errors_is_returned() {
392 let response = GraphQlResponse {
393 data: Some(1),
394 errors: None,
395 };
396 assert_eq!(subscription_response_to_result(response).unwrap(), 1);
397 }
398}