use crate::{EventId, MemoryError, Result, StoredEvent, StreamId};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub trait Projection<E>: Default {
fn apply(&mut self, event: &StoredEvent<E>) -> Result<()>;
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReplayCursor {
pub global_position: Option<u64>,
pub stream_versions: BTreeMap<StreamId, u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectionSnapshot<P> {
pub cursor: ReplayCursor,
pub projection: P,
}
pub fn replay<E, P>(events: &[StoredEvent<E>]) -> Result<P>
where
P: Projection<E>,
{
replay_tracked(events).map(|(projection, _)| projection)
}
pub fn replay_tracked<E, P>(events: &[StoredEvent<E>]) -> Result<(P, ReplayCursor)>
where
P: Projection<E>,
{
apply_sequence(P::default(), ReplayCursor::default(), events, true)
}
pub fn resume<E, P>(
snapshot: ProjectionSnapshot<P>,
tail: &[StoredEvent<E>],
) -> Result<(P, ReplayCursor)>
where
P: Projection<E>,
{
apply_sequence(snapshot.projection, snapshot.cursor, tail, false)
}
fn apply_sequence<E, P>(
mut projection: P,
mut cursor: ReplayCursor,
events: &[StoredEvent<E>],
require_zero_start: bool,
) -> Result<(P, ReplayCursor)>
where
P: Projection<E>,
{
let mut event_ids = BTreeSet::<EventId>::new();
for event in events {
if !event_ids.insert(event.metadata.id.clone()) {
return Err(MemoryError::DuplicateEvent {
id: event.metadata.id.to_string(),
});
}
let expected_position = cursor.global_position.map_or(Ok(0), |position| {
position.checked_add(1).ok_or(MemoryError::CapacityOverflow)
})?;
if require_zero_start && cursor.global_position.is_none() && expected_position != 0 {
return Err(MemoryError::InvalidReplay {
reason: "complete replay must start at zero".to_owned(),
});
}
if event.metadata.global_position != expected_position {
return Err(MemoryError::InvalidReplay {
reason: format!(
"global position {}, expected {expected_position}",
event.metadata.global_position
),
});
}
let expected_version = cursor
.stream_versions
.get(&event.metadata.stream_id)
.map_or(Ok(0), |version| {
version.checked_add(1).ok_or(MemoryError::CapacityOverflow)
})?;
if event.metadata.stream_version != expected_version {
return Err(MemoryError::InvalidReplay {
reason: format!(
"stream {} version {}, expected {expected_version}",
event.metadata.stream_id, event.metadata.stream_version
),
});
}
projection.apply(event)?;
cursor
.stream_versions
.insert(event.metadata.stream_id.clone(), expected_version);
cursor.global_position = Some(expected_position);
}
Ok((projection, cursor))
}