use alloc::sync::Arc;
use core::future::Future;
use futures::StreamExt;
use mnesis::Version;
use crate::envelope::PersistedEnvelope;
use crate::store::RawEventStore;
use crate::stream_id::StreamKey;
use crate::wake::{WakeRegistration, WakeSource};
pub trait Catchup: Send {
type Position: Copy + Send + core::fmt::Debug;
type Item: Send;
type Scan: futures::Stream<Item = Result<Self::Item, Self::Error>> + Send;
type Error: core::error::Error + Send + Sync + 'static;
fn read_after(
&self,
from: Option<Self::Position>,
) -> impl Future<Output = Result<Self::Scan, Self::Error>> + Send;
fn position_of(item: &Self::Item) -> Self::Position;
fn arm(&self) -> impl Future<Output = ()> + Send + 'static;
}
fn tag_version<E>(item: Result<PersistedEnvelope, E>) -> Result<(Version, PersistedEnvelope), E> {
item.map(|env| (env.version(), env))
}
pub struct StreamCatchup<S: RawEventStore + WakeSource> {
store: Arc<S>,
id: StreamKey,
reg: <S as WakeSource>::Registration,
}
impl<S: RawEventStore + WakeSource> StreamCatchup<S> {
pub fn new(store: Arc<S>, id_bytes: &[u8]) -> Result<Self, <S as WakeSource>::Error> {
let reg = store.register(Some(id_bytes))?;
Ok(Self {
store,
id: StreamKey::from_slice(id_bytes),
reg,
})
}
}
type TagFn<S> = fn(
Result<PersistedEnvelope, <S as RawEventStore>::Error>,
) -> Result<(Version, PersistedEnvelope), <S as RawEventStore>::Error>;
impl<S: RawEventStore + WakeSource> Catchup for StreamCatchup<S> {
type Position = Version;
type Item = (Version, PersistedEnvelope);
type Scan = futures::future::Either<
futures::stream::Map<<S as RawEventStore>::Stream, TagFn<S>>,
futures::stream::Empty<Result<(Version, PersistedEnvelope), <S as RawEventStore>::Error>>,
>;
type Error = <S as RawEventStore>::Error;
fn read_after(
&self,
from: Option<Version>,
) -> impl Future<Output = Result<Self::Scan, Self::Error>> + Send {
let resume = from.map_or(Some(Version::INITIAL), Version::next);
let store = Arc::clone(&self.store);
let id = self.id.clone();
async move {
match resume {
Some(v) => {
let scan = store.read_stream(&id, v).await?;
let tag: TagFn<S> = tag_version;
Ok(futures::future::Either::Left(scan.map(tag)))
}
None => Ok(futures::future::Either::Right(futures::stream::empty())),
}
}
}
fn position_of(item: &Self::Item) -> Version {
item.0
}
fn arm(&self) -> impl Future<Output = ()> + Send + 'static {
self.reg.arm()
}
}
pub struct AllCatchup<S: RawEventStore + WakeSource> {
store: Arc<S>,
reg: <S as WakeSource>::Registration,
}
impl<S: RawEventStore + WakeSource> AllCatchup<S> {
pub fn new(store: Arc<S>) -> Result<Self, <S as WakeSource>::Error> {
let reg = store.register(None)?;
Ok(Self { store, reg })
}
}
impl<S: RawEventStore + WakeSource> Catchup for AllCatchup<S> {
type Position = <S as RawEventStore>::AllPosition;
type Item = (
<S as RawEventStore>::AllPosition,
StreamKey,
PersistedEnvelope,
);
type Scan = <S as RawEventStore>::AllStream;
type Error = <S as RawEventStore>::Error;
fn read_after(
&self,
from: Option<Self::Position>,
) -> impl Future<Output = Result<Self::Scan, Self::Error>> + Send {
self.store.read_all(from)
}
fn position_of(item: &Self::Item) -> Self::Position {
item.0
}
fn arm(&self) -> impl Future<Output = ()> + Send + 'static {
self.reg.arm()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "test code")]
mod tests {
use super::*;
use crate::envelope::PendingBatch;
use crate::envelope::pending_envelope;
use crate::test_support::TestStore;
use futures::StreamExt;
async fn seed(store: &TestStore, id: &StreamKey, lo: u64, hi: u64) {
for v in lo..=hi {
let env = pending_envelope(Version::new(v).unwrap())
.event_type("E")
.payload(b"e".to_vec())
.build()
.unwrap();
store
.append(id, Version::new(v - 1), PendingBatch::of(&env))
.await
.unwrap();
}
}
#[tokio::test]
async fn stream_catchup_reads_after_none() {
let store = Arc::new(TestStore::new());
let id = StreamKey::from_slice(b"s");
seed(&store, &id, 1, 3).await;
let catchup = StreamCatchup::new(Arc::clone(&store), b"s").unwrap();
let scan = catchup.read_after(None).await.unwrap();
let tagged: Vec<(u64, u64)> = scan
.map(|r| {
let (pos, env) = r.unwrap();
(pos.as_u64(), env.version().as_u64())
})
.collect()
.await;
assert_eq!(
tagged,
vec![(1, 1), (2, 2), (3, 3)],
"per-stream catchup must yield (Version tag, env version) 1,2,3 in order"
);
}
#[tokio::test]
async fn stream_catchup_read_after_is_exclusive() {
let store = Arc::new(TestStore::new());
let id = StreamKey::from_slice(b"s");
seed(&store, &id, 1, 3).await;
let catchup = StreamCatchup::new(Arc::clone(&store), b"s").unwrap();
let scan = catchup
.read_after(Some(Version::new(2).unwrap()))
.await
.unwrap();
let versions: Vec<u64> = scan.map(|r| r.unwrap().0.as_u64()).collect().await;
assert_eq!(
versions,
vec![3],
"read_after(Some(2)) yields strictly after 2"
);
}
#[tokio::test]
async fn reopen_does_not_redeliver_last_event() {
let store = Arc::new(TestStore::new());
let id = StreamKey::from_slice(b"s");
seed(&store, &id, 1, 3).await;
let catchup = StreamCatchup::new(Arc::clone(&store), b"s").unwrap();
let first: Vec<(Version, PersistedEnvelope)> = catchup
.read_after(None)
.await
.unwrap()
.map(Result::unwrap)
.collect()
.await;
let versions: Vec<u64> = first.iter().map(|(p, _)| p.as_u64()).collect();
assert_eq!(versions, vec![1, 2, 3], "initial drain yields 1,2,3");
let (last_pos, _) = first.last().unwrap();
let second: Vec<u64> = catchup
.read_after(Some(*last_pos))
.await
.unwrap()
.map(|r| r.unwrap().0.as_u64())
.collect()
.await;
assert!(
second.is_empty(),
"reopen must NOT re-deliver event 3 (got {second:?})"
);
}
#[tokio::test]
async fn stream_catchup_read_after_max_is_empty() {
let store = Arc::new(TestStore::new());
let id = StreamKey::from_slice(b"s");
seed(&store, &id, 1, 1).await;
let catchup = StreamCatchup::new(Arc::clone(&store), b"s").unwrap();
let scan = catchup
.read_after(Some(Version::new(u64::MAX).unwrap()))
.await
.unwrap();
let count = scan.count().await;
assert_eq!(count, 0, "nothing is strictly after Version::MAX");
}
#[tokio::test]
async fn all_catchup_reads_after_none_then_exclusive() {
let store = Arc::new(TestStore::new());
seed(&store, &StreamKey::from_slice(b"a"), 1, 1).await;
seed(&store, &StreamKey::from_slice(b"b"), 1, 1).await;
let env = pending_envelope(Version::new(2).unwrap())
.event_type("E")
.payload(b"e".to_vec())
.build()
.unwrap();
store
.append(
&StreamKey::from_slice(b"a"),
Version::new(1),
PendingBatch::of(&env),
)
.await
.unwrap();
let catchup = AllCatchup::new(Arc::clone(&store)).unwrap();
let all: Vec<(
crate::test_support::TestAllPos,
StreamKey,
PersistedEnvelope,
)> = catchup
.read_after(None)
.await
.unwrap()
.map(Result::unwrap)
.collect()
.await;
let positions: Vec<u64> = all.iter().map(|(p, _, _)| p.as_u64()).collect();
assert_eq!(
positions,
vec![1, 2, 3],
"$all catchup must yield every stream's events in position order"
);
let keys: Vec<&[u8]> = all.iter().map(|(_, k, _)| k.as_bytes()).collect();
assert_eq!(
keys,
vec![b"a".as_slice(), b"b".as_slice(), b"a".as_slice()],
"$all items must carry the stream key they were appended to"
);
let (first_pos, _, _) = all.first().unwrap();
let rest: Vec<u64> = AllCatchup::new(Arc::clone(&store))
.unwrap()
.read_after(Some(*first_pos))
.await
.unwrap()
.map(|r| r.unwrap().0.as_u64())
.collect()
.await;
assert_eq!(rest, vec![2, 3], "read_after(Some(first)) is exclusive");
}
}