arc_core/snapshot.rs
1//! # Snapshot Module
2//!
3//! A snapshot captures an aggregate's fully-folded state at a known version so
4//! the next load can resume from there instead of replaying the entire stream.
5//! Without it, `CommandBus::dispatch` reads every event for an aggregate on
6//! each command — a long-lived aggregate with hundreds of events pays that cost
7//! on every operation. The serialized `state` is opaque to the store: only the
8//! owning aggregate knows how to read it back via `Aggregate::from_snapshot`.
9
10use serde::{Deserialize, Serialize};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13/// A point-in-time capture of an aggregate's state.
14///
15/// # Fields
16///
17/// - `aggregate_id`: instance the snapshot belongs to
18/// - `aggregate_type`: type name (e.g., "User"), mirrors [`Event::aggregate_type`](crate::event::Event)
19/// - `version`: sequence of the last event folded into `state`; a loader replays
20/// events from `version + 1` onward
21/// - `state`: aggregate state serialized by `Aggregate::to_snapshot`, opaque here
22/// - `created_at`: wall-clock capture time (milliseconds since UNIX epoch, same
23/// unit as `Event::timestamp`)
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct Snapshot {
26 pub aggregate_id: String,
27 pub aggregate_type: String,
28 pub version: i64,
29 pub state: serde_json::Value,
30 pub created_at: u64,
31}
32
33impl Snapshot {
34 /// Capture a snapshot, stamping `created_at` with the current wall clock.
35 pub fn new(
36 aggregate_id: impl Into<String>,
37 aggregate_type: impl Into<String>,
38 version: i64,
39 state: serde_json::Value,
40 ) -> Self {
41 Self {
42 aggregate_id: aggregate_id.into(),
43 aggregate_type: aggregate_type.into(),
44 version,
45 state,
46 created_at: SystemTime::now()
47 .duration_since(UNIX_EPOCH)
48 .expect("Time went backwards")
49 .as_millis() as u64,
50 }
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use serde_json::json;
58
59 #[test]
60 fn test_snapshot_new_stamps_created_at() {
61 let snap = Snapshot::new("user-1", "User", 7, json!({ "name": "Alice" }));
62 assert_eq!(snap.aggregate_id, "user-1");
63 assert_eq!(snap.aggregate_type, "User");
64 assert_eq!(snap.version, 7);
65 assert_eq!(snap.state["name"], "Alice");
66 assert!(snap.created_at > 0);
67 }
68
69 #[test]
70 fn test_snapshot_roundtrips_through_json() {
71 let snap = Snapshot::new("user-2", "User", 3, json!({ "value": 42 }));
72 let encoded = serde_json::to_string(&snap).unwrap();
73 let decoded: Snapshot = serde_json::from_str(&encoded).unwrap();
74 assert_eq!(snap, decoded);
75 }
76}