1use std::{
6 future::Future,
7 pin::Pin,
8 task::{Context, Poll},
9};
10
11use futures::Stream;
12
13use crate::{
14 error,
15 pagination::{Direction, Page, PaginationFilter},
16 query_types::PageInfo,
17};
18
19pub struct PageStream<T, F, Fut> {
22 query_fn: F,
23 direction: Direction,
24 limit: Option<i32>,
25 start_cursor: Option<String>,
26 current_page: Option<(PageInfo, std::vec::IntoIter<T>)>,
27 current_future: Option<Pin<Box<Fut>>>,
28 finished: bool,
29}
30
31impl<T, F, Fut> PageStream<T, F, Fut> {
32 pub fn new(query_fn: F, pagination: PaginationFilter) -> Self {
33 Self {
34 query_fn,
35 direction: pagination.direction,
36 limit: pagination.limit,
37 start_cursor: pagination.cursor,
38 current_page: None,
39 current_future: None,
40 finished: false,
41 }
42 }
43}
44
45impl<T, F, Fut> Stream for PageStream<T, F, Fut>
46where
47 T: Clone + Unpin,
48 F: Fn(PaginationFilter) -> Fut,
49 F: Unpin,
50 Fut: Future<Output = Result<Page<T>, error::GraphQLError>>,
51{
52 type Item = Result<T, error::GraphQLError>;
53
54 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
55 if self.finished {
56 return Poll::Ready(None);
57 }
58
59 loop {
60 let direction = self.direction.clone();
61 if let Some((page_info, iter)) = &mut self.current_page {
63 if let Some(item) = iter.next() {
64 return Poll::Ready(Some(Ok(item)));
65 }
66
67 let should_continue = match direction {
68 Direction::Forward => page_info.has_next_page,
69 Direction::Backward => page_info.has_previous_page,
70 };
71 if !should_continue {
72 self.finished = true;
73 return Poll::Ready(None);
74 }
75 }
76
77 let current_cursor = self
79 .current_page
80 .as_ref()
81 .and_then(|(page_info, _iter)| match self.direction {
82 Direction::Forward => page_info
83 .has_next_page
84 .then(|| page_info.end_cursor.clone()),
85 Direction::Backward => page_info
86 .has_previous_page
87 .then(|| page_info.start_cursor.clone()),
88 })
89 .flatten();
90
91 if self.current_future.is_none() {
93 let current_cursor = if self.current_page.is_none() {
94 self.start_cursor.take()
95 } else {
96 current_cursor
97 };
98 let filter = PaginationFilter {
99 direction: self.direction.clone(),
100 cursor: current_cursor,
101 limit: self.limit,
102 };
103 let future = (self.query_fn)(filter);
104 self.current_future = Some(Box::pin(future));
105 }
106
107 match self.current_future.as_mut().unwrap().as_mut().poll(cx) {
109 Poll::Ready(Ok(page)) => {
110 self.current_future = None;
111
112 if page.is_empty() {
113 self.finished = true;
114 return Poll::Ready(None);
115 }
116
117 let (page_info, data) = page.into_parts();
118 let iter = match self.direction {
120 Direction::Forward => data.into_iter(),
121 Direction::Backward => {
122 let mut vec = data;
123 vec.reverse();
124 vec.into_iter()
125 }
126 };
127 self.current_page = Some((page_info, iter));
128 }
129 Poll::Ready(Err(e)) => {
130 self.finished = true;
131 self.current_future = None;
132 return Poll::Ready(Some(Err(e)));
133 }
134 Poll::Pending => return Poll::Pending,
135 }
136 }
137 }
138}
139
140pub fn stream_paginated_query<T, F, Fut>(
142 query_fn: F,
143 pagination: PaginationFilter,
144) -> PageStream<T, F, Fut>
145where
146 F: Fn(PaginationFilter) -> Fut,
147 Fut: Future<Output = Result<Page<T>, error::GraphQLError>>,
148{
149 PageStream::new(query_fn, pagination)
150}
151
152#[cfg(all(test, not(target_arch = "wasm32")))]
153mod tests {
154 use futures::StreamExt;
155 use iota_types::Address;
156
157 use super::*;
158 use crate::{
159 error::GraphQLResult,
160 test_utils::{
161 assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
162 },
163 };
164
165 macro_rules! first_request_of {
166 ($operation:expr, $query:expr) => {
167 first_request_of!($operation, $query, backward_page())
168 };
169 ($operation:expr, $query:expr, $page:expr) => {
170 sent_variables($operation, |client| async move {
171 let _ = $query(client).pagination($page).stream().next().await;
172 })
173 .await
174 };
175 }
176
177 fn page(items: Vec<i32>, has_more: bool, cursor: Option<&str>) -> Page<i32> {
178 Page::new(
179 PageInfo {
180 has_previous_page: has_more,
181 has_next_page: has_more,
182 start_cursor: cursor.map(|c| format!("{c}-backward")),
183 end_cursor: cursor.map(|c| format!("{c}-forward")),
184 },
185 items,
186 )
187 }
188
189 async fn run(
191 pagination: PaginationFilter,
192 pages: Vec<GraphQLResult<Page<i32>>>,
193 ) -> (Vec<GraphQLResult<i32>>, Vec<PaginationFilter>) {
194 let pages = std::sync::Mutex::new(pages.into_iter());
195 let requests = std::sync::Mutex::new(Vec::new());
196 let items = stream_paginated_query(
197 |filter: PaginationFilter| {
198 requests.lock().unwrap().push(filter);
199 let page = pages.lock().unwrap().next().expect("no page left");
200 async move { page }
201 },
202 pagination,
203 )
204 .collect()
205 .await;
206 (items, requests.into_inner().unwrap())
207 }
208
209 fn values(items: Vec<GraphQLResult<i32>>) -> Vec<i32> {
210 items.into_iter().map(Result::unwrap).collect()
211 }
212
213 #[tokio::test]
214 async fn forward_pages_continue_from_the_end_cursor_with_the_same_limit() {
215 let pagination = PaginationFilter {
216 direction: Direction::Forward,
217 cursor: Some("start".to_owned()),
218 limit: Some(3),
219 };
220 let (items, requests) = run(
221 pagination,
222 vec![
223 Ok(page(vec![1, 2], true, Some("second"))),
224 Ok(page(vec![3], false, None)),
225 ],
226 )
227 .await;
228
229 assert_eq!(values(items), [1, 2, 3]);
230 assert_eq!(requests.len(), 2);
231 assert_eq!(requests[0].cursor.as_deref(), Some("start"));
232 assert_eq!(requests[1].cursor.as_deref(), Some("second-forward"));
233 assert!(requests.iter().all(|r| r.limit == Some(3)));
234 assert!(
235 requests
236 .iter()
237 .all(|r| matches!(r.direction, Direction::Forward))
238 );
239 }
240
241 #[tokio::test]
242 async fn backward_pages_continue_from_the_start_cursor_newest_first() {
243 let pagination = PaginationFilter {
244 direction: Direction::Backward,
245 cursor: Some("start".to_owned()),
246 limit: Some(3),
247 };
248 let (items, requests) = run(
249 pagination,
250 vec![
251 Ok(page(vec![3, 4], true, Some("second"))),
252 Ok(page(vec![1, 2], false, None)),
253 ],
254 )
255 .await;
256
257 assert_eq!(values(items), [4, 3, 2, 1]);
258 assert_eq!(requests.len(), 2);
259 assert_eq!(requests[0].cursor.as_deref(), Some("start"));
260 assert_eq!(requests[1].cursor.as_deref(), Some("second-backward"));
261 assert!(requests.iter().all(|r| r.limit == Some(3)));
262 assert!(
263 requests
264 .iter()
265 .all(|r| matches!(r.direction, Direction::Backward))
266 );
267 }
268
269 #[tokio::test]
270 async fn an_empty_page_ends_the_stream() {
271 let (items, requests) = run(
272 PaginationFilter::default(),
273 vec![Ok(page(Vec::new(), true, Some("next")))],
274 )
275 .await;
276
277 assert!(items.is_empty());
278 assert_eq!(requests.len(), 1);
279 }
280
281 #[tokio::test]
282 async fn an_error_is_yielded_once_and_ends_the_stream() {
283 let (items, requests) = run(
284 PaginationFilter::default(),
285 vec![Err(crate::GraphQLError::Timeout)],
286 )
287 .await;
288
289 assert!(matches!(
290 items.as_slice(),
291 [Err(crate::GraphQLError::Timeout)]
292 ));
293 assert_eq!(requests.len(), 1);
294 }
295
296 #[tokio::test]
297 async fn every_list_query_streams_from_its_pagination() {
298 assert_backward_page(&first_request_of!(
299 "CheckpointsQueryFragment",
300 |c: crate::GraphQLClient| c.checkpoints()
301 ));
302 assert_forward_page(&first_request_of!(
303 "CheckpointsQueryFragment",
304 |c: crate::GraphQLClient| c.checkpoints(),
305 forward_page()
306 ));
307 assert_backward_page(&first_request_of!(
308 "ObjectsQueryFragment",
309 |c: crate::GraphQLClient| c.coins(Address::STD)
310 ));
311 assert_forward_page(&first_request_of!(
312 "ObjectsQueryFragment",
313 |c: crate::GraphQLClient| c.coins(Address::STD),
314 forward_page()
315 ));
316 assert_backward_page(&first_request_of!(
317 "ObjectsQueryFragment",
318 |c: crate::GraphQLClient| c.gas_coins(Address::STD)
319 ));
320 assert_forward_page(&first_request_of!(
321 "ObjectsQueryFragment",
322 |c: crate::GraphQLClient| c.gas_coins(Address::STD),
323 forward_page()
324 ));
325 assert_backward_page(&first_request_of!(
326 "DynamicFieldsOwnerQueryFragment",
327 |c: crate::GraphQLClient| c.dynamic_fields(Address::STD)
328 ));
329 assert_forward_page(&first_request_of!(
330 "DynamicFieldsOwnerQueryFragment",
331 |c: crate::GraphQLClient| c.dynamic_fields(Address::STD),
332 forward_page()
333 ));
334 assert_backward_page(&first_request_of!(
335 "EventsQueryFragment",
336 |c: crate::GraphQLClient| c.events()
337 ));
338 assert_forward_page(&first_request_of!(
339 "EventsQueryFragment",
340 |c: crate::GraphQLClient| c.events(),
341 forward_page()
342 ));
343 assert_backward_page(&first_request_of!(
344 "IotaNamesAddressRegistrationsQueryFragment",
345 |c: crate::GraphQLClient| c.iota_names_registrations(Address::STD)
346 ));
347 assert_forward_page(&first_request_of!(
348 "IotaNamesAddressRegistrationsQueryFragment",
349 |c: crate::GraphQLClient| c.iota_names_registrations(Address::STD),
350 forward_page()
351 ));
352 assert_backward_page(&first_request_of!(
353 "ActiveValidatorsQueryFragment",
354 |c: crate::GraphQLClient| c.active_validators()
355 ));
356 assert_forward_page(&first_request_of!(
357 "ActiveValidatorsQueryFragment",
358 |c: crate::GraphQLClient| c.active_validators(),
359 forward_page()
360 ));
361 assert_backward_page(&first_request_of!(
362 "ObjectsQueryFragment",
363 |c: crate::GraphQLClient| c.objects()
364 ));
365 assert_forward_page(&first_request_of!(
366 "ObjectsQueryFragment",
367 |c: crate::GraphQLClient| c.objects(),
368 forward_page()
369 ));
370 assert_backward_page(&first_request_of!(
371 "PackageVersionsQueryFragment",
372 |c: crate::GraphQLClient| c.package_versions(Address::FRAMEWORK)
373 ));
374 assert_forward_page(&first_request_of!(
375 "PackageVersionsQueryFragment",
376 |c: crate::GraphQLClient| c.package_versions(Address::FRAMEWORK),
377 forward_page()
378 ));
379 assert_backward_page(&first_request_of!(
380 "PackagesQueryFragment",
381 |c: crate::GraphQLClient| c.packages()
382 ));
383 assert_forward_page(&first_request_of!(
384 "PackagesQueryFragment",
385 |c: crate::GraphQLClient| c.packages(),
386 forward_page()
387 ));
388 assert_backward_page(&first_request_of!(
389 "TransactionBlocksQueryFragment",
390 |c: crate::GraphQLClient| c.transactions()
391 ));
392 assert_forward_page(&first_request_of!(
393 "TransactionBlocksQueryFragment",
394 |c: crate::GraphQLClient| c.transactions(),
395 forward_page()
396 ));
397 assert_backward_page(&first_request_of!(
398 "AddressTransactionsQueryFragment",
399 |c: crate::GraphQLClient| c.address_transactions(Address::STD)
400 ));
401 assert_forward_page(&first_request_of!(
402 "AddressTransactionsQueryFragment",
403 |c: crate::GraphQLClient| c.address_transactions(Address::STD),
404 forward_page()
405 ));
406 assert_backward_page(&first_request_of!(
407 "TransactionBlocksEffectsQueryFragment",
408 |c: crate::GraphQLClient| c.transactions_effects()
409 ));
410 assert_forward_page(&first_request_of!(
411 "TransactionBlocksEffectsQueryFragment",
412 |c: crate::GraphQLClient| c.transactions_effects(),
413 forward_page()
414 ));
415 assert_backward_page(&first_request_of!(
416 "TransactionBlocksWithEffectsQueryFragment",
417 |c: crate::GraphQLClient| c.transactions_data_effects()
418 ));
419 assert_forward_page(&first_request_of!(
420 "TransactionBlocksWithEffectsQueryFragment",
421 |c: crate::GraphQLClient| c.transactions_data_effects(),
422 forward_page()
423 ));
424 }
425
426 #[cfg(feature = "move-types")]
427 #[tokio::test]
428 async fn move_objects_streams_from_its_pagination() {
429 use iota_move_types::iota_framework::{coin::Coin, iota::IOTA};
430
431 assert_backward_page(&first_request_of!(
432 "ObjectsQueryFragment",
433 |c: crate::GraphQLClient| c.move_objects::<Coin<IOTA>>()
434 ));
435 assert_forward_page(&first_request_of!(
436 "ObjectsQueryFragment",
437 |c: crate::GraphQLClient| c.move_objects::<Coin<IOTA>>(),
438 forward_page()
439 ));
440 }
441}