///|
/// One decoded page and its optional cursor for the following page, for
/// `Paginator::with_cursor`.
pub(all) struct Page[T] {
items : Array[T]
next : String?
}
///|
/// A stateful async paginator.
///
/// `E` is the error the fetch raises, so an SDK exposes its own error type
/// through the paginator instead of `SdkError`.
pub struct Paginator[T, E] {
// A struct type parameter cannot carry an `Error` bound, so the fetch is
// stored with its failure as a value and re-raised by the methods.
priv fetch : async () -> Result[Array[T]?, E] noraise
priv mut done : Bool
priv mut pending : Array[T]?
}
///|
/// Creates a paginator over a fetch that owns its own cursor.
///
/// `fetch` returns the next page, or `None` once the sequence has ended; an
/// empty page ends it as well. The fetch keeps whatever cursor the API uses
/// (a snowflake, a timestamp, a URL) and must advance it only after a page
/// has arrived, so that a fetch that raised is asked for the same page again.
/// For an API whose cursor is one string, `with_cursor` keeps that state here.
pub fn[T, E : Error] Paginator::new(
fetch : async () -> Array[T]? raise E,
) -> Paginator[T, E] {
{
fetch: () => Ok(fetch()) catch { error => Err(error) },
done: false,
pending: None,
}
}
///|
/// Creates a paginator over a string cursor. The first fetch receives `None`;
/// each page names the cursor of the following one, and a page without a
/// cursor is the last. A fetch that raised leaves the cursor unchanged.
pub fn[T, E : Error] Paginator::with_cursor(
fetch : async (String?) -> Page[T] raise E,
) -> Paginator[T, E] {
let cursor : Ref[String?] = Ref(None)
let started = Ref(false)
let finished = Ref(false)
Paginator::new(() => {
if finished.val {
return None
}
let page = fetch(if started.val { cursor.val } else { None })
started.val = true
cursor.val = page.next
finished.val = page.next is None
Some(page.items)
})
}
///|
/// Fetches the next non-empty page, or `None` once the sequence has ended.
/// Items that a previous `collect` did not use come back first.
pub async fn[T, E : Error] Paginator::next_page(
self : Paginator[T, E],
) -> Array[T]? raise E {
if self.pending is Some(items) {
self.pending = None
return Some(items)
}
if self.done {
return None
}
let page = match (self.fetch)() {
Ok(page) => page
Err(error) => raise error
}
match page {
Some(items) if !items.is_empty() => Some(items)
_ => {
self.done = true
None
}
}
}
///|
/// Visits every remaining item in page order.
pub async fn[T, E : Error] Paginator::each(
self : Paginator[T, E],
f : async (T) -> Unit raise E,
) -> Unit raise E {
for ;; {
guard self.next_page() is Some(items) else { return }
for item in items {
f(item)
}
}
}
///|
/// Collects at most `max` remaining items. The unused rest of the last page
/// is kept for the next call, so nothing fetched is lost. A non-positive
/// maximum performs no requests and returns an empty array.
pub async fn[T, E : Error] Paginator::collect(
self : Paginator[T, E],
max~ : Int,
) -> Array[T] raise E {
let result = []
if max <= 0 {
return result
}
for ; result.length() < max; {
guard self.next_page() is Some(items) else { break }
for index, item in items {
result.push(item)
if result.length() == max {
if index + 1 < items.length() {
self.pending = Some(items[index + 1:].to_owned())
}
break
}
}
}
result
}