1use crate::ws::types::Cursor;
3use std::collections::HashSet;
4
5pub const MAX_PAGE_SIZE: u32 = 200;
7
8#[derive(Default)]
9pub(crate) struct Continuation {
10 seen: HashSet<String>,
11}
12impl Continuation {
13 pub(crate) fn advance(&mut self, next: Option<&str>) -> Result<Option<Cursor>, InvalidPage> {
14 match next {
15 None => Ok(None),
16 Some(id) if id.is_empty() || !self.seen.insert(id.to_owned()) => Err(InvalidPage),
17 Some(id) => Ok(Some(Cursor::After { id: id.to_owned() })),
18 }
19 }
20}
21#[derive(Debug, thiserror::Error)]
22#[error("page returned an empty or repeated continuation")]
23pub struct InvalidPage;
24
25pub async fn fetch_all<T, E, F, Fut>(
27 mut load: F,
28 next: impl Fn(&T) -> Option<&str>,
29 append: impl Fn(&mut T, T),
30) -> Result<T, E>
31where
32 F: FnMut(Cursor) -> Fut,
33 Fut: std::future::Future<Output = Result<T, E>>,
34 E: From<InvalidPage>,
35{
36 let mut all = load(Cursor::Start {}).await?;
37 let mut continuation = Continuation::default();
38 while let Some(cursor) = continuation.advance(next(&all))? {
39 let page = load(cursor).await?;
40 append(&mut all, page);
41 }
42 Ok(all)
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 #[derive(Debug)]
49 struct Page {
50 rows: Vec<u8>,
51 next: Option<String>,
52 }
53 fn append(all: &mut Page, page: Page) {
54 all.rows.extend(page.rows);
55 all.next = page.next;
56 }
57 #[tokio::test]
58 async fn follows_opaque_continuation_and_rejects_cycles() {
59 let mut requests = Vec::new();
60 let result: Result<Page, InvalidPage> = fetch_all(
61 |cursor| {
62 requests.push(cursor.clone());
63 std::future::ready(Ok(match cursor {
64 Cursor::Start {} => Page {
65 rows: vec![1],
66 next: Some("opaque".into()),
67 },
68 Cursor::After { id } => {
69 assert_eq!(id, "opaque");
70 Page {
71 rows: vec![2],
72 next: None,
73 }
74 }
75 }))
76 },
77 |page| page.next.as_deref(),
78 append,
79 )
80 .await;
81 assert_eq!(result.unwrap().rows, [1, 2]);
82 assert_eq!(requests.len(), 2);
83 let result: Result<Page, InvalidPage> = fetch_all(
84 |_| {
85 std::future::ready(Ok(Page {
86 rows: vec![],
87 next: Some("cycle".into()),
88 }))
89 },
90 |page| page.next.as_deref(),
91 append,
92 )
93 .await;
94 assert!(result.is_err());
95 }
96 #[tokio::test]
97 async fn later_failure_never_returns_a_partial_list() {
98 let result: Result<Page, InvalidPage> = fetch_all(
99 |cursor| {
100 std::future::ready(match cursor {
101 Cursor::Start {} => Ok(Page {
102 rows: vec![1],
103 next: Some("next".into()),
104 }),
105 Cursor::After { .. } => Err(InvalidPage),
106 })
107 },
108 |page| page.next.as_deref(),
109 append,
110 )
111 .await;
112 assert!(result.is_err());
113 }
114}