Skip to main content

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}