Skip to main content

weavatrix_memory/projection/
replay.rs

1use crate::{EventId, MemoryError, Result, StoredEvent, StreamId};
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, HashSet};
4
5pub trait Projection<E>: Default {
6    /// Reserves projection-specific capacity before a known replay batch.
7    ///
8    /// The default is a no-op. Implementations must not mutate observable
9    /// projection state or weaken validation.
10    fn prepare_replay(&mut self, _events: &[StoredEvent<E>]) {}
11
12    /// Applies one validated stored event.
13    ///
14    /// # Errors
15    ///
16    /// Returns domain-specific invariant violations.
17    fn apply(&mut self, event: &StoredEvent<E>) -> Result<()>;
18
19    /// Applies one owned event. The default preserves the borrowed contract;
20    /// projections may override it to move large payloads without cloning.
21    ///
22    /// # Errors
23    ///
24    /// Returns the same invariant violations as [`Self::apply`].
25    fn apply_owned(&mut self, event: StoredEvent<E>) -> Result<()> {
26        self.apply(&event)
27    }
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31pub struct ReplayCursor {
32    pub global_position: Option<u64>,
33    pub stream_versions: BTreeMap<StreamId, u64>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct ProjectionSnapshot<P> {
38    pub cursor: ReplayCursor,
39    pub projection: P,
40}
41
42/// Replays a complete, zero-based event sequence after validating its cursors.
43///
44/// # Errors
45///
46/// Rejects global-position or per-stream-version gaps and projection errors.
47pub fn replay<E, P>(events: &[StoredEvent<E>]) -> Result<P>
48where
49    P: Projection<E>,
50{
51    replay_tracked(events).map(|(projection, _)| projection)
52}
53
54/// Replays and consumes a complete sequence, allowing projections to move
55/// payloads instead of cloning them.
56///
57/// # Errors
58///
59/// Rejects the same cursor, duplicate-ID, and projection failures as
60/// [`replay`].
61pub fn replay_owned<E, P>(events: Vec<StoredEvent<E>>) -> Result<P>
62where
63    P: Projection<E>,
64{
65    apply_owned_sequence(P::default(), ReplayCursor::default(), events, true)
66        .map(|(projection, _)| projection)
67}
68
69/// Replays a complete sequence and returns the cursor needed for a snapshot.
70///
71/// # Errors
72///
73/// Rejects invalid event ordering, duplicate IDs, and projection errors.
74pub fn replay_tracked<E, P>(events: &[StoredEvent<E>]) -> Result<(P, ReplayCursor)>
75where
76    P: Projection<E>,
77{
78    apply_sequence(P::default(), ReplayCursor::default(), events, true)
79}
80
81/// Resumes a materialized projection from its exact event cursor.
82///
83/// # Errors
84///
85/// Rejects gaps between the snapshot cursor and the supplied tail, invalid
86/// stream versions, duplicate IDs within the tail, and projection errors.
87pub fn resume<E, P>(
88    snapshot: ProjectionSnapshot<P>,
89    tail: &[StoredEvent<E>],
90) -> Result<(P, ReplayCursor)>
91where
92    P: Projection<E>,
93{
94    apply_sequence(snapshot.projection, snapshot.cursor, tail, false)
95}
96
97fn apply_sequence<E, P>(
98    mut projection: P,
99    mut cursor: ReplayCursor,
100    events: &[StoredEvent<E>],
101    require_zero_start: bool,
102) -> Result<(P, ReplayCursor)>
103where
104    P: Projection<E>,
105{
106    projection.prepare_replay(events);
107    let mut event_ids = HashSet::<EventId>::with_capacity(events.len());
108    for event in events {
109        let (expected_position, expected_version) =
110            validate_next(&cursor, &mut event_ids, event, require_zero_start)?;
111        projection.apply(event)?;
112        cursor
113            .stream_versions
114            .insert(event.metadata.stream_id.clone(), expected_version);
115        cursor.global_position = Some(expected_position);
116    }
117    Ok((projection, cursor))
118}
119
120fn apply_owned_sequence<E, P>(
121    mut projection: P,
122    mut cursor: ReplayCursor,
123    events: Vec<StoredEvent<E>>,
124    require_zero_start: bool,
125) -> Result<(P, ReplayCursor)>
126where
127    P: Projection<E>,
128{
129    projection.prepare_replay(&events);
130    let mut event_ids = HashSet::<EventId>::with_capacity(events.len());
131    for event in events {
132        let (expected_position, expected_version) =
133            validate_next(&cursor, &mut event_ids, &event, require_zero_start)?;
134        let stream = event.metadata.stream_id.clone();
135        projection.apply_owned(event)?;
136        cursor.stream_versions.insert(stream, expected_version);
137        cursor.global_position = Some(expected_position);
138    }
139    Ok((projection, cursor))
140}
141
142fn validate_next<E>(
143    cursor: &ReplayCursor,
144    event_ids: &mut HashSet<EventId>,
145    event: &StoredEvent<E>,
146    require_zero_start: bool,
147) -> Result<(u64, u64)> {
148    if !event_ids.insert(event.metadata.id.clone()) {
149        return Err(MemoryError::DuplicateEvent {
150            id: event.metadata.id.to_string(),
151        });
152    }
153    let expected_position = cursor.global_position.map_or(Ok(0), |position| {
154        position.checked_add(1).ok_or(MemoryError::CapacityOverflow)
155    })?;
156    if require_zero_start && cursor.global_position.is_none() && expected_position != 0 {
157        return Err(MemoryError::InvalidReplay {
158            reason: "complete replay must start at zero".to_owned(),
159        });
160    }
161    if event.metadata.global_position != expected_position {
162        return Err(MemoryError::InvalidReplay {
163            reason: format!(
164                "global position {}, expected {expected_position}",
165                event.metadata.global_position
166            ),
167        });
168    }
169    let expected_version = cursor
170        .stream_versions
171        .get(&event.metadata.stream_id)
172        .map_or(Ok(0), |version| {
173            version.checked_add(1).ok_or(MemoryError::CapacityOverflow)
174        })?;
175    if event.metadata.stream_version != expected_version {
176        return Err(MemoryError::InvalidReplay {
177            reason: format!(
178                "stream {} version {}, expected {expected_version}",
179                event.metadata.stream_id, event.metadata.stream_version
180            ),
181        });
182    }
183    Ok((expected_position, expected_version))
184}