use alloc::sync::Arc;
use mnesis::{ErrorId, Version};
use crate::envelope::{PendingBatch, PersistedEnvelope};
use crate::error::{AppendError, AppendValidationError};
use crate::stream::EventStream;
use crate::stream_id::StreamKey;
#[derive(Debug)]
pub struct Store<S> {
inner: Arc<S>,
}
impl<S> Store<S> {
pub fn new(raw: S) -> Self {
Self {
inner: Arc::new(raw),
}
}
#[must_use]
pub fn raw(&self) -> &S {
&self.inner
}
#[cfg(feature = "subscription")]
#[must_use]
pub(crate) const fn arc(&self) -> &Arc<S> {
&self.inner
}
}
impl<S> Clone for Store<S> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
pub trait RawEventStore: Send + Sync {
type Error: core::error::Error + Send + Sync + 'static;
type Stream: EventStream<Error = Self::Error> + 'static;
type AllPosition: AllPosition;
type AllStream: futures::Stream<
Item = Result<(Self::AllPosition, StreamKey, PersistedEnvelope), Self::Error>,
> + Send
+ 'static;
fn append(
&self,
id: &StreamKey,
expected_version: Option<Version>,
envelopes: PendingBatch<'_>,
) -> impl core::future::Future<Output = Result<Self::AllPosition, AppendError<Self::Error>>> + Send;
fn read_stream(
&self,
id: &StreamKey,
from: Version,
) -> impl core::future::Future<Output = Result<Self::Stream, Self::Error>> + Send;
fn read_all(
&self,
from: Option<Self::AllPosition>,
) -> impl core::future::Future<Output = Result<Self::AllStream, Self::Error>> + Send;
#[must_use]
fn into_store(self) -> Store<Self>
where
Self: Sized,
{
Store::new(self)
}
}
impl<S: RawEventStore> RawEventStore for Store<S> {
type Error = S::Error;
type Stream = S::Stream;
type AllPosition = S::AllPosition;
type AllStream = S::AllStream;
async fn append(
&self,
id: &StreamKey,
expected_version: Option<Version>,
envelopes: PendingBatch<'_>,
) -> Result<Self::AllPosition, AppendError<Self::Error>> {
self.raw().append(id, expected_version, envelopes).await
}
async fn read_stream(
&self,
id: &StreamKey,
from: Version,
) -> Result<Self::Stream, Self::Error> {
self.raw().read_stream(id, from).await
}
async fn read_all(
&self,
from: Option<Self::AllPosition>,
) -> Result<Self::AllStream, Self::Error> {
self.raw().read_all(from).await
}
}
pub trait AllPosition: Copy + Ord + Send + Sync + core::fmt::Debug + 'static {}
pub fn validate_append_versions(
current: u64,
expected: Option<Version>,
envelopes: PendingBatch<'_>,
id: &StreamKey,
) -> Result<(), AppendValidationError> {
let actual: Option<Version> = if current == 0 {
None
} else {
Version::new(current)
};
if expected != actual {
return Err(AppendValidationError::Conflict {
stream_id: ErrorId::from_display(id),
expected,
actual,
});
}
let mut next = current;
for env in envelopes {
next = next
.checked_add(1)
.ok_or(AppendValidationError::VersionOverflow)?;
if env.version().as_u64() != next {
return Err(AppendValidationError::Conflict {
stream_id: ErrorId::from_display(id),
expected: Version::new(next),
actual: Some(env.version()),
});
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "test code")]
#[allow(clippy::panic, reason = "test code")]
mod validate_append_tests {
use super::*;
use crate::envelope::{PendingEnvelope, pending_envelope};
fn sk() -> StreamKey {
StreamKey::from_slice(b"s")
}
fn batch(envs: &[PendingEnvelope]) -> PendingBatch<'_> {
PendingBatch::new(envs).expect("test batches are non-empty by construction")
}
fn env(version: u64) -> PendingEnvelope {
pending_envelope(Version::new(version).unwrap())
.event_type("E")
.payload(b"p".as_slice())
.build()
.unwrap()
}
#[test]
fn fresh_stream_ok() {
assert!(validate_append_versions(0, None, batch(&[env(1), env(2), env(3)]), &sk()).is_ok());
}
#[test]
fn existing_stream_ok() {
assert!(
validate_append_versions(5, Version::new(5), batch(&[env(6), env(7)]), &sk()).is_ok()
);
}
#[test]
fn stale_expected_conflict() {
let err =
validate_append_versions(5, Version::new(4), batch(&[env(6)]), &sk()).unwrap_err();
assert!(matches!(err, AppendValidationError::Conflict { .. }));
}
#[test]
fn fresh_stream_with_expected_some_conflict() {
let err =
validate_append_versions(0, Version::new(1), batch(&[env(1)]), &sk()).unwrap_err();
assert!(matches!(err, AppendValidationError::Conflict { .. }));
}
#[test]
fn gapped_batch_conflict() {
let err = validate_append_versions(0, None, batch(&[env(1), env(3)]), &sk()).unwrap_err();
match err {
AppendValidationError::Conflict {
expected, actual, ..
} => {
assert_eq!(expected, Version::new(2));
assert_eq!(actual, Some(env(3).version()));
}
AppendValidationError::VersionOverflow => {
panic!("expected Conflict, got VersionOverflow")
}
}
}
#[test]
fn out_of_order_batch_conflict() {
let err = validate_append_versions(0, None, batch(&[env(2), env(1)]), &sk()).unwrap_err();
match err {
AppendValidationError::Conflict {
expected, actual, ..
} => {
assert_eq!(expected, Version::new(1));
assert_eq!(actual, Some(env(2).version()));
}
AppendValidationError::VersionOverflow => {
panic!("expected Conflict, got VersionOverflow")
}
}
}
#[test]
fn wrong_start_version_conflict() {
let err =
validate_append_versions(5, Version::new(5), batch(&[env(7)]), &sk()).unwrap_err();
assert!(matches!(err, AppendValidationError::Conflict { .. }));
}
#[test]
fn version_overflow_is_version_overflow_not_conflict() {
let err =
validate_append_versions(u64::MAX, Version::new(u64::MAX), batch(&[env(1)]), &sk())
.unwrap_err();
assert!(matches!(err, AppendValidationError::VersionOverflow));
}
}