use {
crate::groups::{Command, Cursor, Index, Term},
core::ops::RangeInclusive,
};
pub trait Storage<C: Command>: Send + 'static {
fn append(&mut self, command: C, term: Term) -> Index;
fn available(&self) -> RangeInclusive<Index>;
fn get(&self, index: Index) -> Option<(C, Term)>;
fn get_range(&self, range: &RangeInclusive<Index>) -> Vec<(Term, Index, C)>;
fn truncate(&mut self, at: Index);
fn last(&self) -> Cursor;
fn term_at(&self, index: Index) -> Option<Term> {
if index.is_zero() {
return Some(Term::zero());
}
self.get(index).map(|(_, term)| term)
}
fn prune_prefix(&mut self, up_to: Index);
fn reset_to(&mut self, cursor: Cursor) {
let _ = cursor;
unimplemented!("reset_to is not supported by this storage implementation")
}
}
#[derive(Debug)]
pub struct InMemoryLogStore<C: Command> {
entries: Vec<(C, Term)>,
offset: u64,
last_pruned: Option<Cursor>,
}
impl<C: Command> Default for InMemoryLogStore<C> {
fn default() -> Self {
Self {
entries: Vec::new(),
offset: 0,
last_pruned: None,
}
}
}
impl<C: Command> Storage<C> for InMemoryLogStore<C> {
fn append(&mut self, command: C, term: Term) -> Index {
self.entries.push((command, term));
(self.offset + self.entries.len() as u64).into()
}
fn get(&self, index: Index) -> Option<(C, Term)> {
if index.is_zero() || index.0 <= self.offset {
return None;
}
let physical = (index.0 - self.offset - 1) as usize;
self.entries.get(physical).cloned()
}
fn available(&self) -> std::ops::RangeInclusive<Index> {
Index(self.offset)..=Index(self.offset + self.entries.len() as u64)
}
fn get_range(&self, range: &RangeInclusive<Index>) -> Vec<(Term, Index, C)> {
let first_available = self.offset + 1;
let start = range.start().0.max(first_available);
let last_logical = self.offset + self.entries.len() as u64;
let end = range.end().0.min(last_logical);
if start > end {
return Vec::new();
}
let phys_start = (start - self.offset - 1) as usize;
let phys_end = (end - self.offset - 1) as usize;
self
.entries
.iter()
.enumerate()
.skip(phys_start)
.take(phys_end - phys_start + 1)
.map(move |(i, (cmd, term))| {
let logical_index = self.offset + 1 + i as u64;
(*term, Index(logical_index), cmd.clone())
})
.collect()
}
fn truncate(&mut self, at: Index) {
if at.0 <= self.offset {
self.entries.clear();
} else {
let physical = (at.0 - self.offset - 1) as usize;
self.entries.truncate(physical);
}
}
fn last(&self) -> Cursor {
if self.entries.is_empty() {
self.last_pruned.unwrap_or_default()
} else {
let (_, term) = self.entries.last().unwrap();
let logical_index = self.offset + self.entries.len() as u64;
Cursor(*term, Index(logical_index))
}
}
fn term_at(&self, index: Index) -> Option<Term> {
if index.is_zero() {
return Some(Term::zero());
}
if index.0 == self.offset {
return self.last_pruned.map(|c| c.term());
}
self.get(index).map(|(_, term)| term)
}
fn prune_prefix(&mut self, up_to: Index) {
if up_to.0 <= self.offset {
return;
}
let to_drain = ((up_to.0 - self.offset) as usize).min(self.entries.len());
if to_drain > 0 {
let (_, term) = &self.entries[to_drain - 1];
let logical_index = self.offset + to_drain as u64;
self.last_pruned = Some(Cursor(*term, Index(logical_index)));
}
self.entries.drain(..to_drain);
self.offset += to_drain as u64;
}
fn reset_to(&mut self, cursor: Cursor) {
self.entries.clear();
self.offset = cursor.index().0;
self.last_pruned = Some(cursor);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn term(n: u64) -> Term {
Term(n)
}
fn populate(store: &mut InMemoryLogStore<u64>, n: u64) {
for i in 1..=n {
store.append(i * 10, term(1));
}
}
#[test]
fn prune_prefix_basic() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 5);
store.prune_prefix(Index(2));
let avail = store.available();
assert_eq!(*avail.start(), Index(2));
assert_eq!(*avail.end(), Index(5));
assert!(store.get(Index(1)).is_none());
assert!(store.get(Index(2)).is_none());
assert_eq!(store.get(Index(3)), Some((30, term(1))));
assert_eq!(store.get(Index(4)), Some((40, term(1))));
assert_eq!(store.get(Index(5)), Some((50, term(1))));
}
#[test]
fn prune_prefix_last_unchanged() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 5);
let last_before = store.last();
store.prune_prefix(Index(3));
let last_after = store.last();
assert_eq!(last_before, last_after);
assert_eq!(last_after, Cursor(term(1), Index(5)));
}
#[test]
fn prune_prefix_then_append() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 3);
store.prune_prefix(Index(2));
let idx = store.append(60, term(2));
assert_eq!(idx, Index(4));
assert_eq!(store.get(Index(4)), Some((60, term(2))));
assert_eq!(store.last(), Cursor(term(2), Index(4)));
}
#[test]
fn prune_prefix_get_range() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 6);
store.prune_prefix(Index(2));
let entries = store.get_range(&(Index(3)..=Index(5)));
assert_eq!(entries.len(), 3);
assert_eq!(entries[0], (term(1), Index(3), 30));
assert_eq!(entries[1], (term(1), Index(4), 40));
assert_eq!(entries[2], (term(1), Index(5), 50));
let entries = store.get_range(&(Index(1)..=Index(4)));
assert_eq!(entries.len(), 2); assert_eq!(entries[0], (term(1), Index(3), 30));
assert_eq!(entries[1], (term(1), Index(4), 40));
let entries = store.get_range(&(Index(1)..=Index(2)));
assert!(entries.is_empty());
}
#[test]
fn prune_prefix_then_truncate() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 6);
store.prune_prefix(Index(2)); store.truncate(Index(5));
assert_eq!(store.last(), Cursor(term(1), Index(4)));
assert!(store.get(Index(5)).is_none());
assert!(store.get(Index(6)).is_none());
assert_eq!(store.get(Index(4)), Some((40, term(1))));
}
#[test]
fn prune_prefix_idempotent() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 5);
store.prune_prefix(Index(3));
store.prune_prefix(Index(2)); store.prune_prefix(Index(3));
let avail = store.available();
assert_eq!(*avail.start(), Index(3));
assert_eq!(*avail.end(), Index(5));
assert_eq!(store.get(Index(4)), Some((40, term(1))));
}
#[test]
fn prune_prefix_all_entries() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 3);
store.prune_prefix(Index(3));
assert_eq!(store.last(), Cursor(term(1), Index(3)));
let avail = store.available();
assert_eq!(*avail.start(), Index(3));
assert_eq!(*avail.end(), Index(3));
let idx = store.append(100, term(2));
assert_eq!(idx, Index(4));
assert_eq!(store.get(Index(4)), Some((100, term(2))));
}
#[test]
fn prune_prefix_zero_is_noop() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 3);
store.prune_prefix(Index(0));
let avail = store.available();
assert_eq!(*avail.start(), Index(0));
assert_eq!(*avail.end(), Index(3));
assert_eq!(store.get(Index(1)), Some((10, term(1))));
}
#[test]
fn truncate_at_pruned_boundary_clears_remaining() {
let mut store = InMemoryLogStore::<u64>::default();
populate(&mut store, 5);
store.prune_prefix(Index(3)); store.truncate(Index(2));
assert_eq!(store.last(), Cursor(term(1), Index(3)));
}
}