use core::iter;
use core::num::NonZeroU32;
use mnesis::{DomainEvent, Id, Version};
use crate::decoded::Decoded;
use crate::state::{Hydrated, PersistTrigger, SnapshotStore};
use crate::store::AllPosition;
use crate::stream_id::StreamKey;
pub trait Projector: Send + Sync + 'static {
type Event: DomainEvent;
type State: Send + Sync + 'static;
type Error: core::error::Error + Send + Sync + 'static;
fn initial(&self) -> Self::State;
fn apply(&self, state: Self::State, event: &Self::Event) -> Result<Self::State, Self::Error>;
fn apply_attributed(
&self,
state: Self::State,
key: Option<&StreamKey>,
event: &Self::Event,
) -> Result<Self::State, Self::Error> {
let _ = key;
self.apply(state, event)
}
}
mod sealed {
pub trait Sealed {}
}
pub trait Positioned: sealed::Sealed {
type Event;
type Pos: Copy + Send;
fn into_parts(self) -> (Self::Pos, Option<StreamKey>, Decoded<Self::Event>);
}
impl<E> sealed::Sealed for Decoded<E> {}
impl<E> Positioned for Decoded<E> {
type Event = E;
type Pos = Version;
fn into_parts(self) -> (Version, Option<StreamKey>, Self) {
(self.version, None, self)
}
}
impl<E, P: AllPosition> sealed::Sealed for (P, StreamKey, Decoded<E>) {}
impl<E, P: AllPosition> Positioned for (P, StreamKey, Decoded<E>) {
type Event = E;
type Pos = P;
fn into_parts(self) -> (P, Option<StreamKey>, Decoded<E>) {
(self.0, Some(self.1), self.2)
}
}
pub struct Projection<I, P: Projector, Trig, SS, Pos = Version> {
id: I,
projector: P,
trigger: Trig,
snapshot_store: SS,
schema_version: NonZeroU32,
checkpoint: Option<Pos>,
pending: Option<Pos>,
rebuilt_from: Option<NonZeroU32>,
}
impl<I, P, Trig, SS, Pos> Projection<I, P, Trig, SS, Pos>
where
I: Id,
P: Projector,
Trig: PersistTrigger<Pos>,
SS: SnapshotStore<P::State, Pos>,
Pos: Copy + Send,
{
pub async fn load(
id: I,
projector: P,
trigger: Trig,
snapshot_store: SS,
schema_version: NonZeroU32,
) -> Result<(Self, P::State), SS::Error> {
let (state, checkpoint, rebuilt_from) = match snapshot_store
.hydrate(&id, schema_version)
.await?
{
Hydrated::Found { position, state } => (state, Some(position), None),
Hydrated::Absent => (projector.initial(), None, None),
Hydrated::Stale { stored_schema } => (projector.initial(), None, Some(stored_schema)),
};
Ok((
Self {
id,
projector,
trigger,
snapshot_store,
schema_version,
checkpoint,
pending: None,
rebuilt_from,
},
state,
))
}
#[must_use]
pub const fn rebuilding_from(&self) -> Option<NonZeroU32> {
self.rebuilt_from
}
pub const fn id(&self) -> &I {
&self.id
}
pub const fn checkpoint(&self) -> Option<Pos> {
self.checkpoint
}
pub async fn advance<It>(
&mut self,
state: P::State,
item: It,
) -> Result<P::State, ProjectionError<P::Error, SS::Error>>
where
It: Positioned<Event = P::Event, Pos = Pos>,
{
let (position, key, decoded) = item.into_parts();
let folded = self
.projector
.apply_attributed(state, key.as_ref(), &decoded.event)
.map_err(ProjectionError::Apply)?;
if self
.trigger
.should_persist(self.checkpoint, position, iter::once(decoded.event.name()))
{
self.commit(position, &folded).await?;
} else {
self.pending = Some(position);
}
Ok(folded)
}
pub async fn flush(
&mut self,
state: &P::State,
) -> Result<(), ProjectionError<P::Error, SS::Error>> {
match self.pending {
Some(position) => self.commit(position, state).await,
None => Ok(()),
}
}
async fn commit(
&mut self,
position: Pos,
state: &P::State,
) -> Result<(), ProjectionError<P::Error, SS::Error>> {
self.snapshot_store
.commit(&self.id, self.schema_version, position, state)
.await
.map_err(ProjectionError::Commit)?;
self.checkpoint = Some(position);
self.pending = None;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProjectionError<PErr, SErr> {
#[error("projector failed to apply event")]
Apply(#[source] PErr),
#[error("snapshot commit failed")]
Commit(#[source] SErr),
}