use std::future::Future;
use onetaskgraph_plugin_api::{Cursor, Page, SourceError, SourceName};
use super::resume::{Owed, Resume, StreamKind, StreamState};
pub(crate) struct Row<T> {
pub item: T,
pub resume: Resume,
}
pub(crate) struct Fetched<T> {
pub rows: Vec<Row<T>>,
pub after: Option<Cursor>,
}
pub(crate) struct Stream<T> {
pub source: SourceName,
pub kind: StreamKind,
pub fetched: Fetched<T>,
}
pub(crate) async fn walk<T, K, F, Fut>(
start: &Resume,
budget: u32,
page_size: u32,
mut keep: K,
mut fetch: F,
) -> Result<Fetched<T>, SourceError>
where
K: FnMut(&T) -> bool,
F: FnMut(Option<Cursor>, u32) -> Fut,
Fut: Future<Output = Result<Page<T>, SourceError>>,
{
let mut rows: Vec<Row<T>> = Vec::new();
let mut cursor = start.cursor.clone();
let mut skip = start.skip;
let asked_for = page_size.max(1);
loop {
let asked = cursor.clone();
let page = fetch(asked.clone(), asked_for).await?;
fits(page.items.len(), asked_for)?;
if page.next.is_some() && page.next == asked {
return Err(SourceError::Malformed {
message: "the source returned the cursor it was given, so a walk of it \
would never end"
.to_owned(),
});
}
let mut ordinal = 0u32;
for item in page.items {
if !keep(&item) {
continue;
}
let resume = Resume {
cursor: asked.clone(),
skip: ordinal,
};
ordinal = ordinal.saturating_add(1);
if skip > 0 {
skip -= 1;
continue;
}
rows.push(Row { item, resume });
}
cursor = page.next;
if cursor.is_none() || rows.len() as u64 >= u64::from(budget) {
break;
}
}
Ok(Fetched {
rows,
after: cursor,
})
}
pub(crate) fn fits(returned: usize, asked_for: u32) -> Result<(), SourceError> {
if returned as u64 <= u64::from(asked_for) {
return Ok(());
}
Err(SourceError::Malformed {
message: format!(
"the source returned {returned} rows for a page of at most {asked_for}; a \
source may return fewer than it was asked for and never more"
),
})
}
pub(crate) fn merge<T>(
streams: Vec<Stream<T>>,
budget: u32,
resume_first: Option<&Owed>,
) -> (Vec<(SourceName, T)>, Vec<StreamState>, Option<Owed>) {
let count = streams.len();
let mut sources: Vec<SourceName> = Vec::with_capacity(count);
let mut kinds: Vec<StreamKind> = Vec::with_capacity(count);
let mut afters: Vec<Option<Cursor>> = Vec::with_capacity(count);
let mut rows: Vec<Vec<Option<Row<T>>>> = Vec::with_capacity(count);
for stream in streams {
sources.push(stream.source);
kinds.push(stream.kind);
afters.push(stream.fetched.after);
rows.push(stream.fetched.rows.into_iter().map(Some).collect());
}
let start = resume_first
.and_then(|owed| {
(0..count).find(|&position| {
sources[position] == owed.source && kinds[position] == owed.stream
})
})
.unwrap_or(0);
let mut taken = vec![0usize; count];
let mut items: Vec<(SourceName, T)> = Vec::new();
let mut round = 0usize;
let mut owed: Option<usize> = None;
'page: loop {
let mut progressed = false;
for step in 0..count {
let position = (start + step) % count;
if rows[position].len() <= round {
continue;
}
progressed = true;
if items.len() as u64 >= u64::from(budget) {
owed = Some(position);
break 'page;
}
let row = rows[position][round]
.take()
.expect("a round-robin takes each row once");
items.push((sources[position].clone(), row.item));
taken[position] += 1;
}
if !progressed {
break;
}
round += 1;
}
let mut states = Vec::new();
for position in 0..count {
let resume = match rows[position]
.get(taken[position])
.and_then(|row| row.as_ref())
{
Some(row) => Some(row.resume.clone()),
None => afters[position].as_ref().map(|cursor| Resume {
cursor: Some(cursor.clone()),
skip: 0,
}),
};
if let Some(resume) = resume {
states.push(StreamState {
source: sources[position].clone(),
stream: kinds[position],
resume,
});
}
}
let next = owed
.map(|position| Owed {
source: sources[position].clone(),
stream: kinds[position],
})
.filter(|owed| {
states
.iter()
.any(|state| state.source == owed.source && state.stream == owed.stream)
});
(items, states, next)
}