use super::{
RawEnumerationPageBuilder, RawEnumerationPageIndex, RawEnumerationPageIndexError, RawEnumerationPageIndexInner,
RawEnumerationPageIndexState, owner_entry_is_valid,
};
use std::collections::{BTreeSet, HashSet};
pub(crate) struct RawEnumerationPageWriter {
inner: RawEnumerationPageIndexInner,
indexed: HashSet<String>,
pending: BTreeSet<String>,
observed_complete: HashSet<String>,
complete_source_changed: bool,
observations: usize,
revalidate_after_entries: usize,
committed_entries: usize,
}
impl RawEnumerationPageWriter {
pub(crate) fn new(index: RawEnumerationPageIndex) -> Result<Self, RawEnumerationPageIndexError> {
let RawEnumerationPageIndexState::Supported(inner) = index.state else {
return Err(RawEnumerationPageIndexError::Unsupported);
};
if inner.parent.is_empty() || inner.page_entry_limit == 0 {
return Err(RawEnumerationPageIndexError::CorruptIndex);
}
let entries = inner.validated_indexed_entries()?;
let entry_count = entries.len();
let indexed: HashSet<_> = entries.into_iter().collect();
if indexed.len() != entry_count
|| inner.pages.iter().any(|page| page.entries.len() > inner.page_entry_limit)
|| (inner.complete && inner.building.is_some())
|| inner.pages.last().is_some_and(|page| page.terminal != inner.complete)
{
return Err(RawEnumerationPageIndexError::CorruptIndex);
}
let committed_entries = entry_count.saturating_sub(inner.building.as_ref().map_or(0, |page| page.entries.len()));
Ok(Self {
inner,
indexed,
pending: BTreeSet::new(),
observed_complete: HashSet::new(),
complete_source_changed: false,
observations: 0,
revalidate_after_entries: entry_count,
committed_entries,
})
}
pub(crate) fn indexed_entry_count(&self) -> usize {
self.indexed.len()
}
pub(crate) fn record_entry(&mut self, entry: &str) -> Result<(), RawEnumerationPageIndexError> {
if !owner_entry_is_valid(entry) {
return Err(RawEnumerationPageIndexError::InvalidEntry);
}
self.observations = self.observations.saturating_add(1);
if self.inner.complete || self.inner.building.as_ref().is_some_and(|page| page.terminal) {
if self.indexed.contains(entry) {
self.observed_complete.insert(entry.to_owned());
} else {
self.complete_source_changed = true;
}
}
if self.inner.complete {
if self.observations >= self.revalidate_after_entries
&& (self.observed_complete.len() != self.indexed.len() || self.complete_source_changed)
{
return Err(RawEnumerationPageIndexError::IdentityMismatch);
}
return Ok(());
}
if !self.indexed.contains(entry) {
self.pending.insert(entry.to_owned());
}
if self.observations < self.revalidate_after_entries {
return Ok(());
}
if self
.inner
.building
.as_ref()
.is_some_and(|page| page.entries.len() >= self.inner.page_entry_limit)
{
return self.commit_page();
}
if let Some(entry) = self.pending.pop_first() {
let building = self.inner.building.get_or_insert_with(|| RawEnumerationPageBuilder {
page_index: u64::try_from(self.inner.pages.len()).unwrap_or(u64::MAX),
entries_start: u64::try_from(self.committed_entries).unwrap_or(u64::MAX),
entries: Vec::new(),
terminal: false,
});
self.indexed.insert(entry.clone());
building.entries.push(entry);
building.entries.sort();
building.terminal = false;
self.inner.generation = self.inner.generation.saturating_add(1);
}
if self
.inner
.building
.as_ref()
.is_some_and(|page| page.terminal || page.entries.len() >= self.inner.page_entry_limit)
{
self.commit_page()?;
}
Ok(())
}
fn commit_page(&mut self) -> Result<(), RawEnumerationPageIndexError> {
self.inner.commit_building_page(self.committed_entries)?;
self.committed_entries = self.indexed.len();
Ok(())
}
pub(crate) fn checkpoint(&self) -> Result<RawEnumerationPageIndex, RawEnumerationPageIndexError> {
let mut inner = self.inner.clone();
if inner.building.is_some() {
inner.commit_building_page(self.committed_entries)?;
}
Ok(RawEnumerationPageIndex {
state: RawEnumerationPageIndexState::Supported(inner),
})
}
}
#[cfg(test)]
mod tests;