#![allow(
clippy::shadow_reuse,
reason = "per-iteration Arc clones in try_fold closures intentionally re-bind"
)]
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::borrow::Borrow;
use core::future::Future;
use core::marker::PhantomData;
use core::num::NonZeroU32;
use mnesis::{Aggregate, AggregateRoot, DomainEvent, EventOf, Events, Version};
use futures::TryStreamExt;
use crate::codec::{Decode, Encode};
use crate::envelope::{EnvelopeError, PendingBatch, PersistedEnvelope, pending_envelope};
use crate::error::{AppendError, LoadWithError, StoreError};
use crate::metadata::MetadataProvider;
use crate::store::{AllPosition, RawEventStore, Store};
use crate::stream_id::StreamKey;
use crate::upcasting::EventMorsel;
use crate::value::{Payload, SchemaVersion};
pub trait Repository<A: Aggregate>: Send + Sync {
type Error: core::error::Error + Send + Sync + 'static;
type Position: AllPosition;
fn load(&self, id: A::Id)
-> impl Future<Output = Result<AggregateRoot<A>, Self::Error>> + Send;
fn save<const N: usize>(
&self,
aggregate: &mut AggregateRoot<A>,
events: &Events<EventOf<A>, N>,
) -> impl Future<Output = Result<Self::Position, Self::Error>> + Send;
}
pub(crate) trait ReplayFrom<A: Aggregate>: Send + Sync {
type Error: core::error::Error + Send + Sync + 'static;
fn replay_from(
&self,
root: AggregateRoot<A>,
from: Version,
) -> impl Future<Output = Result<AggregateRoot<A>, Self::Error>> + Send;
}
pub(super) fn version_to_nz32(version: Version) -> Option<NonZeroU32> {
let raw = version.as_u64();
let narrow = u32::try_from(raw).ok()?;
NonZeroU32::new(narrow)
}
pub(crate) const fn first_persisted_version(current: Option<Version>) -> Option<Version> {
match current {
None => Some(Version::INITIAL),
Some(v) => v.next(),
}
}
pub struct EventStore<S, C, A, M = ()> {
store: Store<S>,
codec: Arc<C>,
meta: Arc<M>,
_aggregate: PhantomData<fn() -> A>,
}
impl<S, C, A, M> EventStore<S, C, A, M> {
pub(crate) fn new(store: Store<S>, codec: C, meta: M) -> Self {
Self {
store,
codec: Arc::new(codec),
meta: Arc::new(meta),
_aggregate: PhantomData,
}
}
}
impl<S, C, A, M> ReplayFrom<A> for EventStore<S, C, A, M>
where
A: Aggregate,
S: RawEventStore + 'static,
for<'a> C: Encode<EventOf<A>> + Decode<EventOf<A>, Output<'a>: Borrow<EventOf<A>>> + 'static,
EventOf<A>: DomainEvent,
S::Stream: Send,
M: Send + Sync + 'static,
{
type Error =
StoreError<S::Error, <C as Encode<EventOf<A>>>::Error, <C as Decode<EventOf<A>>>::Error>;
#[cfg_attr(
feature = "tracing",
tracing::instrument(
name = "mnesis.aggregate.load",
level = "debug",
skip_all,
fields(
aggregate = core::any::type_name::<A>(),
stream = %root.id(),
from = %from,
version = tracing::field::Empty
)
)
)]
async fn replay_from(
&self,
root: AggregateRoot<A>,
from: Version,
) -> Result<AggregateRoot<A>, Self::Error> {
let store = self.store.clone();
let codec = Arc::<C>::clone(&self.codec);
let raw_stream = store
.raw()
.read_stream(&StreamKey::from_slice(root.id().as_ref()), from)
.await
.map_err(StoreError::Adapter)?;
let loaded = raw_stream
.map_err(StoreError::Adapter)
.try_fold(root, move |mut r, env| {
let codec = Arc::<C>::clone(&codec);
async move {
let version = env.version();
let out = <C as Decode<EventOf<A>>>::decode(&codec, &env)
.map_err(StoreError::Decode)?;
r.replay(version, out.borrow())?;
Ok(r)
}
})
.await?;
#[cfg(feature = "tracing")]
tracing::Span::current().record("version", tracing::field::debug(&loaded.version()));
Ok(loaded)
}
}
impl<S, C, A, M> Repository<A> for EventStore<S, C, A, M>
where
A: Aggregate,
S: RawEventStore + 'static,
for<'a> C: Encode<EventOf<A>> + Decode<EventOf<A>, Output<'a>: Borrow<EventOf<A>>> + 'static,
EventOf<A>: DomainEvent,
S::Stream: Send,
M: MetadataProvider<EventOf<A>>,
{
type Error =
StoreError<S::Error, <C as Encode<EventOf<A>>>::Error, <C as Decode<EventOf<A>>>::Error>;
type Position = S::AllPosition;
async fn load(&self, id: A::Id) -> Result<AggregateRoot<A>, Self::Error> {
let root = AggregateRoot::<A>::new(id);
self.replay_from(root, Version::INITIAL).await
}
async fn save<const N: usize>(
&self,
aggregate: &mut AggregateRoot<A>,
events: &Events<EventOf<A>, N>,
) -> Result<Self::Position, Self::Error> {
save_events::<A, S, C, _, M, N>(self, aggregate, events, |_| None).await
}
}
impl<S, C, A, M> EventStore<S, C, A, M> {
#[allow(
clippy::type_complexity,
reason = "the four-source LoadWithError return is intrinsic to the contract; an alias would \
hide which domains the upcasting read path can fail from"
)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(
name = "mnesis.aggregate.load",
level = "debug",
skip_all,
fields(
aggregate = core::any::type_name::<A>(),
stream = %id,
from = %Version::INITIAL,
version = tracing::field::Empty
)
)
)]
pub async fn load_with<F, E>(
&self,
id: A::Id,
upcast: F,
) -> Result<
AggregateRoot<A>,
LoadWithError<
S::Error,
<C as Encode<EventOf<A>>>::Error,
<C as Decode<EventOf<A>>>::Error,
E,
>,
>
where
A: Aggregate,
S: RawEventStore + 'static,
for<'a> C:
Encode<EventOf<A>> + Decode<EventOf<A>, Output<'a>: Borrow<EventOf<A>>> + 'static,
F: for<'a> Fn(EventMorsel<'a>) -> Result<EventMorsel<'a>, E> + Send + Sync + 'static,
E: core::error::Error + Send + Sync + 'static,
EventOf<A>: DomainEvent,
S::Stream: Send,
M: Send + Sync + 'static,
{
let store = self.store.clone();
let codec = Arc::<C>::clone(&self.codec);
let root = AggregateRoot::<A>::new(id);
let raw_stream = store
.raw()
.read_stream(&StreamKey::from_slice(root.id().as_ref()), Version::INITIAL)
.await
.map_err(|e| LoadWithError::Store(StoreError::Adapter(e)))?;
let upcast = Arc::new(upcast);
let loaded = raw_stream
.map_err(|e| LoadWithError::Store(StoreError::Adapter(e)))
.try_fold(root, move |mut r, env| {
let codec = Arc::<C>::clone(&codec);
let upcast = Arc::<F>::clone(&upcast);
async move {
let version = env.version();
let morsel = EventMorsel::borrowed(
env.event_type(),
env.schema_version_as_version(),
env.payload(),
);
let transformed = upcast(morsel).map_err(LoadWithError::Upcast)?;
let upcast_env = PersistedEnvelope::for_decode(
transformed.event_type(),
transformed.payload(),
)
.map_err(|e| LoadWithError::Store(StoreError::EnvelopeSynthesis(e)))?;
let out = <C as Decode<EventOf<A>>>::decode(&codec, &upcast_env)
.map_err(|e| LoadWithError::Store(StoreError::Decode(e)))?;
r.replay(version, out.borrow())
.map_err(|e| LoadWithError::Store(StoreError::Kernel(e)))?;
Ok(r)
}
})
.await?;
#[cfg(feature = "tracing")]
tracing::Span::current().record("version", tracing::field::debug(&loaded.version()));
Ok(loaded)
}
pub async fn save_with<F, const N: usize>(
&self,
aggregate: &mut AggregateRoot<A>,
events: &Events<EventOf<A>, N>,
current_version: F,
) -> Result<
S::AllPosition,
StoreError<S::Error, <C as Encode<EventOf<A>>>::Error, <C as Decode<EventOf<A>>>::Error>,
>
where
A: Aggregate,
S: RawEventStore + 'static,
C: Encode<EventOf<A>> + Decode<EventOf<A>> + 'static,
F: Fn(&str) -> Option<Version>,
EventOf<A>: DomainEvent,
M: MetadataProvider<EventOf<A>>,
{
save_events::<A, S, C, _, M, N>(self, aggregate, events, current_version).await
}
}
#[allow(
clippy::type_complexity,
reason = "the three-source StoreError return is intrinsic to the contract; an alias would hide \
which domains the save path can fail from"
)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(
name = "mnesis.aggregate.save",
level = "debug",
skip_all,
fields(
aggregate = core::any::type_name::<A>(),
stream = %aggregate.id(),
events = events.len(),
expected = ?aggregate.version(),
position = tracing::field::Empty
)
)
)]
async fn save_events<A, S, C, F, M, const N: usize>(
facade: &EventStore<S, C, A, M>,
aggregate: &mut AggregateRoot<A>,
events: &Events<EventOf<A>, N>,
current_version: F,
) -> Result<
S::AllPosition,
StoreError<S::Error, <C as Encode<EventOf<A>>>::Error, <C as Decode<EventOf<A>>>::Error>,
>
where
A: Aggregate,
S: RawEventStore,
C: Encode<EventOf<A>> + Decode<EventOf<A>>,
F: Fn(&str) -> Option<Version>,
M: MetadataProvider<EventOf<A>>,
EventOf<A>: DomainEvent,
{
let expected_version = aggregate.version();
let mut next_version =
first_persisted_version(expected_version).ok_or(StoreError::VersionOverflow)?;
let (head, tail, last_version) = {
let encode_at = |event: &EventOf<A>, version: Version| {
let payload_bytes = <C as Encode<EventOf<A>>>::encode(&facade.codec, event)
.map_err(StoreError::Encode)?;
let payload = Payload::from_bytes(payload_bytes)
.map_err(EnvelopeError::from)
.map_err(StoreError::from)?;
let schema_version = current_version(event.name()).unwrap_or(Version::INITIAL);
let schema_nz32 = version_to_nz32(schema_version).ok_or(StoreError::VersionOverflow)?;
let metadata = facade.meta.metadata(version, event, &payload);
let builder = pending_envelope(version)
.event(event)
.payload(payload.into_bytes())
.schema_version(SchemaVersion::new(schema_nz32));
match metadata {
Some(m) => builder.metadata(m.into_bytes()).build(),
None => builder.build(),
}
.map_err(StoreError::from)
};
let head = encode_at(events.first(), next_version)?;
let mut last_version = next_version;
let mut tail = Vec::with_capacity(events.rest().len());
for event in events.rest() {
next_version = next_version.next().ok_or(StoreError::VersionOverflow)?;
tail.push(encode_at(event, next_version)?);
last_version = next_version;
}
(head, tail, last_version)
};
let position = facade
.store
.raw()
.append(
&StreamKey::from_slice(aggregate.id().as_ref()),
expected_version,
PendingBatch::from_parts(&head, &tail),
)
.await
.map_err(|err| match err {
AppendError::Conflict {
stream_id,
expected,
actual,
} => StoreError::Conflict {
stream_id,
expected,
actual,
},
AppendError::Store(e) => StoreError::Adapter(e),
})?;
#[cfg(feature = "tracing")]
tracing::Span::current().record("position", tracing::field::debug(&position));
aggregate.commit_persisted(last_version, events);
Ok(position)
}
#[cfg(test)]
mod version_helper_tests {
use super::first_persisted_version;
use mnesis::Version;
#[test]
fn fresh_stream_starts_at_initial() {
assert_eq!(first_persisted_version(None), Some(Version::INITIAL));
}
#[test]
fn existing_stream_advances_by_one() {
let v = Version::INITIAL;
assert_eq!(first_persisted_version(Some(v)), v.next());
}
#[test]
fn overflow_at_max_returns_none() {
let max = Version::new(u64::MAX).expect("u64::MAX is non-zero");
assert_eq!(first_persisted_version(Some(max)), None);
}
}