///|
/// One decoded page and its optional cursor for the following page.
pub(all) struct Page[T] {
items : Array[T]
next : String?
}
///|
/// A stateful paginator with expected failures as values. Transport errors
/// remain effects; a failed fetch never advances the cursor.
pub struct Paginator[T, E] {
priv fetch : async () -> Result[Array[T]?, E]
priv mut done : Bool
priv mut pending : Array[T]?
}
///|
/// Creates a paginator whose fetch owns its cursor. None or an empty page ends
/// iteration. The fetch must advance its cursor only after a successful page.
pub fn[T, E] Paginator::new(
fetch : async () -> Result[Array[T]?, E],
) -> Paginator[T, E] {
{ fetch, done: false, pending: None, }
}
///|
/// Creates a paginator which advances its string cursor only on Ok.
pub fn[T, E] Paginator::with_cursor(
fetch : async (String?) -> Result[Page[T], E],
) -> Paginator[T, E] {
let cursor : Ref[String?] = Ref(None)
let finished = Ref(false)
Paginator::new(() => {
if finished.val {
return Ok(None)
}
match fetch(cursor.val) {
Err(error) => Err(error)
Ok(page) => {
cursor.val = page.next
finished.val = page.next is None
Ok(Some(page.items))
}
}
})
}
///|
/// Fetches the next non-empty page, preserving unused items from collect.
pub async fn[T, E] Paginator::next_page(
self : Paginator[T, E],
) -> Result[Array[T]?, E] {
if self.pending is Some(items) {
self.pending = None
return Ok(Some(items))
}
if self.done {
return Ok(None)
}
match (self.fetch)() {
Err(error) => Err(error)
Ok(Some(items)) if !items.is_empty() => Ok(Some(items))
Ok(_) => {
self.done = true
Ok(None)
}
}
}
///|
/// Visits each item in page order, stopping at the first expected failure.
pub async fn[T, E] Paginator::each(
self : Paginator[T, E],
f : async (T) -> Result[Unit, E],
) -> Result[Unit, E] {
for ;; {
match self.next_page() {
Err(error) => return Err(error)
Ok(None) => return Ok(())
Ok(Some(items)) =>
for item in items {
if f(item) is Err(error) {
return Err(error)
}
}
}
}
}
///|
/// Collects at most max items, keeping unused items from the last page.
/// A non-positive maximum performs no requests.
pub async fn[T, E] Paginator::collect(
self : Paginator[T, E],
max~ : Int32,
) -> Result[Array[T], E] {
let result = []
if max <= 0 {
return Ok(result)
}
while result.length() < max {
let items = match self.next_page() {
Err(error) => return Err(error)
Ok(None) => break
Ok(Some(items)) => items
}
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
}
}
}
Ok(result)
}