use std::future::Future;
use std::time::Duration;
use chrono::NaiveDate;
use futures_core::Stream;
use crate::{
Error, Release, ReleaseDate, ReleaseDatesResults, ReleasesResults, Result, Series,
SeriesSearchResults, Source, SourcesResults, Tag, TagsResults, VintageDates,
};
pub(crate) mod sealed {
pub trait Sealed {}
}
pub trait Page: sealed::Sealed {
type Item: Send;
fn total(&self) -> u32;
fn items_len(&self) -> usize;
fn into_items(self) -> Vec<Self::Item>;
}
pub trait Paginate: Clone + Send + Sized + sealed::Sealed {
type Page: Page + Send;
const MAX_PAGE: u32;
fn requested_limit(&self) -> Option<u32>;
fn requested_offset(&self) -> Option<u32>;
#[must_use]
fn with_paging(self, limit: u32, offset: u32) -> Self;
fn send_page(self) -> impl Future<Output = Result<Self::Page>> + Send;
fn send_all(self) -> impl Future<Output = Result<Vec<<Self::Page as Page>::Item>>> + Send {
collect_all(self)
}
fn stream(self) -> impl Stream<Item = Result<<Self::Page as Page>::Item>> + Send {
stream_pages(self)
}
}
const MAX_RETRIES: u32 = 3;
async fn collect_all<P: Paginate>(request: P) -> Result<Vec<<P::Page as Page>::Item>> {
let ceiling = request.requested_limit();
let mut offset = request.requested_offset().unwrap_or(0);
let mut collected: Vec<<P::Page as Page>::Item> = Vec::new();
loop {
let page_size = match ceiling {
Some(cap) => {
let remaining = cap.saturating_sub(collected.len() as u32);
if remaining == 0 {
break;
}
remaining.min(P::MAX_PAGE)
}
None => P::MAX_PAGE,
};
let page = send_page_with_retry(request.clone().with_paging(page_size, offset)).await?;
let total = page.total();
let got = page.items_len() as u32;
collected.extend(page.into_items());
offset = offset.saturating_add(got);
if got == 0 || offset >= total {
break;
}
}
if let Some(cap) = ceiling {
collected.truncate(cap as usize);
}
Ok(collected)
}
fn stream_pages<P: Paginate>(
request: P,
) -> impl Stream<Item = Result<<P::Page as Page>::Item>> + Send {
async_stream::try_stream! {
let ceiling = request.requested_limit();
let mut offset = request.requested_offset().unwrap_or(0);
let mut yielded: u32 = 0;
loop {
let page_size = match ceiling {
Some(cap) => {
let remaining = cap.saturating_sub(yielded);
if remaining == 0 {
break;
}
remaining.min(P::MAX_PAGE)
}
None => P::MAX_PAGE,
};
let page = send_page_with_retry(request.clone().with_paging(page_size, offset)).await?;
let total = page.total();
let got = page.items_len() as u32;
if got == 0 {
break;
}
for item in page.into_items() {
yield item;
yielded += 1;
if ceiling.is_some_and(|cap| yielded >= cap) {
break;
}
}
offset = offset.saturating_add(got);
if offset >= total || ceiling.is_some_and(|cap| yielded >= cap) {
break;
}
}
}
}
async fn send_page_with_retry<P: Paginate>(request: P) -> Result<P::Page> {
let mut attempt = 0;
loop {
match request.clone().send_page().await {
Err(Error::RateLimited { retry_after }) if attempt < MAX_RETRIES => {
let delay = retry_after.unwrap_or_else(|| backoff(attempt));
tokio::time::sleep(delay).await;
attempt += 1;
}
result => return result,
}
}
}
fn backoff(attempt: u32) -> Duration {
Duration::from_secs(1u64 << attempt)
}
impl sealed::Sealed for SeriesSearchResults {}
impl Page for SeriesSearchResults {
type Item = Series;
fn total(&self) -> u32 {
self.count
}
fn items_len(&self) -> usize {
self.series.len()
}
fn into_items(self) -> Vec<Self::Item> {
self.series
}
}
impl sealed::Sealed for TagsResults {}
impl Page for TagsResults {
type Item = Tag;
fn total(&self) -> u32 {
self.count
}
fn items_len(&self) -> usize {
self.tags.len()
}
fn into_items(self) -> Vec<Self::Item> {
self.tags
}
}
impl sealed::Sealed for ReleasesResults {}
impl Page for ReleasesResults {
type Item = Release;
fn total(&self) -> u32 {
self.count
}
fn items_len(&self) -> usize {
self.releases.len()
}
fn into_items(self) -> Vec<Self::Item> {
self.releases
}
}
impl sealed::Sealed for ReleaseDatesResults {}
impl Page for ReleaseDatesResults {
type Item = ReleaseDate;
fn total(&self) -> u32 {
self.count
}
fn items_len(&self) -> usize {
self.release_dates.len()
}
fn into_items(self) -> Vec<Self::Item> {
self.release_dates
}
}
impl sealed::Sealed for SourcesResults {}
impl Page for SourcesResults {
type Item = Source;
fn total(&self) -> u32 {
self.count
}
fn items_len(&self) -> usize {
self.sources.len()
}
fn into_items(self) -> Vec<Self::Item> {
self.sources
}
}
impl sealed::Sealed for VintageDates {}
impl Page for VintageDates {
type Item = NaiveDate;
fn total(&self) -> u32 {
self.count
}
fn items_len(&self) -> usize {
self.vintage_dates.len()
}
fn into_items(self) -> Vec<Self::Item> {
self.vintage_dates
}
}