Skip to main content

weavatrix_memory/projection/
replay.rs

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