1use crate::event::{Body, Event};
5use crate::format::{CodecId, OwnedStoredRecord};
6use crate::log::Log;
7use crate::{Result, SalamanderError};
8
9pub trait Projection {
13 type Body: Body;
20
21 type State;
23
24 fn apply(&mut self, event: &Event<Self::Body>);
28
29 fn cursor(&self) -> u64;
31
32 fn state(&self) -> &Self::State;
34}
35
36pub trait NamespaceScoped: Projection {
41 fn new_for(namespace: &str) -> Self;
43}
44
45pub(crate) fn decode_event<B: Body>(offset: u64, bytes: &[u8]) -> Result<Event<B>> {
53 let mut event: Event<B> =
54 bincode::deserialize(bytes).map_err(|e| SalamanderError::Corrupt {
55 offset,
56 reason: e.to_string(),
57 })?;
58 event.offset = offset;
59 Ok(event)
60}
61
62pub(crate) fn decode_stored_event<B: Body>(record: &OwnedStoredRecord) -> Result<Event<B>> {
63 if record.envelope.event_type.as_str() == "salamander.raw-v2" {
64 return decode_event(record.position, &record.payload);
65 }
66 if record.envelope.codec != CodecId::RUST_BINCODE_V1 {
67 return Err(SalamanderError::Codec(format!(
68 "typed Rust replay does not support codec {}",
69 record.envelope.codec.0
70 )));
71 }
72 let body: B =
73 bincode::deserialize(&record.payload).map_err(|error| SalamanderError::Corrupt {
74 offset: record.position,
75 reason: format!("payload decode: {error}"),
76 })?;
77 let namespace = record
78 .envelope
79 .metadata
80 .get("salamander.stream_name")
81 .ok_or_else(|| SalamanderError::Corrupt {
82 offset: record.position,
83 reason: "v2 event is missing salamander.stream_name metadata".into(),
84 })
85 .and_then(|bytes| {
86 std::str::from_utf8(bytes)
87 .map(str::to_owned)
88 .map_err(|_| SalamanderError::Corrupt {
89 offset: record.position,
90 reason: "v2 stream name is not UTF-8".into(),
91 })
92 })?;
93 Ok(Event {
94 offset: record.position,
95 timestamp_ms: u64::try_from(record.envelope.timestamp_unix_nanos.max(0))
96 .unwrap_or(u64::MAX)
97 / 1_000_000,
98 namespace,
99 body,
100 })
101}
102
103pub fn replay_into<P: Projection>(p: &mut P, log: &Log, upto: u64) -> Result<()> {
108 for item in log.records_from(p.cursor()) {
109 let record = item?;
110 if record.position >= upto {
111 break;
112 }
113 let event = decode_stored_event::<P::Body>(&record)?;
114 p.apply(&event);
115 }
116 Ok(())
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::agent::{EventBody, KvProjection};
123 use tempfile::tempdir;
124
125 fn put(namespace: &str, key: &str, value: &[u8]) -> Event<EventBody> {
126 Event {
127 offset: 0, timestamp_ms: 0,
129 namespace: namespace.to_string(),
130 body: EventBody::Put {
131 key: key.to_string(),
132 value: value.to_vec(),
133 },
134 }
135 }
136
137 fn delete(namespace: &str, key: &str) -> Event<EventBody> {
138 Event {
139 offset: 0,
140 timestamp_ms: 0,
141 namespace: namespace.to_string(),
142 body: EventBody::Delete {
143 key: key.to_string(),
144 },
145 }
146 }
147
148 fn append_bincode(log: &mut Log, event: &Event<EventBody>) -> u64 {
149 let bytes = bincode::serialize(event).unwrap();
150 log.append(&bytes).unwrap()
151 }
152
153 #[test]
154 fn replay_into_folds_put_and_delete_events() {
155 let dir = tempdir().unwrap();
156 let mut log = Log::open(dir.path()).unwrap();
157 append_bincode(&mut log, &put("ns", "a", b"1"));
158 append_bincode(&mut log, &put("ns", "b", b"2"));
159 append_bincode(&mut log, &delete("ns", "a"));
160 log.commit().unwrap();
161
162 let mut proj = KvProjection::default();
163 replay_into(&mut proj, &log, log.head()).unwrap();
164
165 assert_eq!(proj.state().get("a"), None);
166 assert_eq!(proj.state().get("b"), Some(&b"2".to_vec()));
167 assert_eq!(proj.cursor(), log.head());
168 }
169
170 #[test]
171 fn two_replays_of_same_log_are_identical() {
172 let dir = tempdir().unwrap();
173 let mut log = Log::open(dir.path()).unwrap();
174 for i in 0..5u8 {
175 append_bincode(&mut log, &put("ns", &format!("k{i}"), &[i]));
176 }
177 log.commit().unwrap();
178
179 let mut proj_a = KvProjection::default();
180 let mut proj_b = KvProjection::default();
181 replay_into(&mut proj_a, &log, log.head()).unwrap();
182 replay_into(&mut proj_b, &log, log.head()).unwrap();
183
184 assert_eq!(proj_a.state(), proj_b.state());
185 assert_eq!(proj_a.cursor(), proj_b.cursor());
186 }
187
188 #[test]
189 fn rebuild_after_reopen_is_identical() {
190 let dir = tempdir().unwrap();
191 {
192 let mut log = Log::open(dir.path()).unwrap();
193 for i in 0..5u8 {
194 append_bincode(&mut log, &put("ns", &format!("k{i}"), &[i]));
195 }
196 log.commit().unwrap();
197 }
198
199 let log = Log::open(dir.path()).unwrap();
200 let mut proj = KvProjection::default();
201 replay_into(&mut proj, &log, log.head()).unwrap();
202
203 for i in 0..5u8 {
204 assert_eq!(proj.state().get(&format!("k{i}")), Some(&vec![i]));
205 }
206 }
207
208 #[test]
209 fn replay_into_stops_before_upto() {
210 let dir = tempdir().unwrap();
211 let mut log = Log::open(dir.path()).unwrap();
212 for i in 0..5u8 {
213 append_bincode(&mut log, &put("ns", &format!("k{i}"), &[i]));
214 }
215 log.commit().unwrap();
216
217 let mut proj = KvProjection::default();
218 replay_into(&mut proj, &log, 3).unwrap();
219
220 assert_eq!(proj.cursor(), 3);
221 assert_eq!(proj.state().len(), 3);
222 assert!(proj.state().contains_key("k2"));
223 assert!(!proj.state().contains_key("k3"));
224 }
225
226 #[test]
227 fn replay_into_resumes_from_existing_cursor() {
228 let dir = tempdir().unwrap();
229 let mut log = Log::open(dir.path()).unwrap();
230 for i in 0..5u8 {
231 append_bincode(&mut log, &put("ns", &format!("k{i}"), &[i]));
232 }
233 log.commit().unwrap();
234
235 let mut incremental = KvProjection::default();
236 replay_into(&mut incremental, &log, 2).unwrap();
237 replay_into(&mut incremental, &log, log.head()).unwrap();
238
239 let mut one_shot = KvProjection::default();
240 replay_into(&mut one_shot, &log, log.head()).unwrap();
241
242 assert_eq!(incremental.state(), one_shot.state());
243 assert_eq!(incremental.cursor(), one_shot.cursor());
244 }
245
246 #[test]
247 fn replay_into_stamps_log_offset_even_if_event_embeds_a_different_one() {
248 let dir = tempdir().unwrap();
249 let mut log = Log::open(dir.path()).unwrap();
250 for i in 0..3u8 {
251 let event = Event {
254 offset: 999,
255 timestamp_ms: 0,
256 namespace: "ns".to_string(),
257 body: EventBody::Put {
258 key: format!("k{i}"),
259 value: vec![i],
260 },
261 };
262 append_bincode(&mut log, &event);
263 }
264 log.commit().unwrap();
265
266 let mut proj = KvProjection::default();
267 replay_into(&mut proj, &log, log.head()).unwrap();
268
269 assert_eq!(proj.cursor(), 3);
273 assert_eq!(proj.state().len(), 3);
274 }
275
276 #[test]
277 fn replay_into_surfaces_corrupt_payload_as_error() {
278 let dir = tempdir().unwrap();
279 let mut log = Log::open(dir.path()).unwrap();
280 append_bincode(&mut log, &put("ns", "a", b"1"));
281 log.append(b"not a bincode-encoded Event").unwrap();
282 log.commit().unwrap();
283
284 let mut proj = KvProjection::default();
285 let err = replay_into(&mut proj, &log, log.head()).unwrap_err();
286
287 assert!(matches!(err, SalamanderError::Corrupt { offset: 1, .. }));
288 assert_eq!(proj.state().get("a"), Some(&b"1".to_vec()));
291 assert_eq!(proj.cursor(), 1);
292 }
293}