cranpose_core/snapshot_v2/
readonly.rs

1//! Read-only snapshot implementation.
2
3use super::*;
4
5/// A read-only snapshot of state at a specific point in time.
6///
7/// This snapshot cannot be used to modify state. Any attempts to write
8/// to state objects while this snapshot is active will fail.
9///
10/// # Thread Safety
11/// Contains `Cell<T>` and `RefCell<T>` which are not `Send`/`Sync`. This is safe because
12/// snapshots are stored in thread-local storage and never shared across threads. The `Arc`
13/// is used for cheap cloning within a single thread, not for cross-thread sharing.
14#[allow(clippy::arc_with_non_send_sync)]
15pub struct ReadonlySnapshot {
16    state: SnapshotState,
17}
18
19impl ReadonlySnapshot {
20    /// Create a new read-only snapshot.
21    pub fn new(
22        id: SnapshotId,
23        invalid: SnapshotIdSet,
24        read_observer: Option<ReadObserver>,
25    ) -> Arc<Self> {
26        Arc::new(Self {
27            state: SnapshotState::new(id, invalid, read_observer, None, false),
28        })
29    }
30
31    pub fn snapshot_id(&self) -> SnapshotId {
32        self.state.id.get()
33    }
34
35    pub fn invalid(&self) -> SnapshotIdSet {
36        self.state.invalid.borrow().clone()
37    }
38
39    pub fn read_only(&self) -> bool {
40        true
41    }
42
43    pub fn root_readonly(&self) -> Arc<Self> {
44        // Readonly snapshots are always their own root
45        ReadonlySnapshot::new(
46            self.state.id.get(),
47            self.state.invalid.borrow().clone(),
48            self.state.read_observer.clone(),
49        )
50    }
51
52    pub fn enter<T>(&self, f: impl FnOnce() -> T) -> T {
53        let previous = current_snapshot();
54        set_current_snapshot(Some(AnySnapshot::Readonly(self.root_readonly())));
55        let result = f();
56        set_current_snapshot(previous);
57        result
58    }
59
60    pub fn take_nested_snapshot(&self, read_observer: Option<ReadObserver>) -> Arc<Self> {
61        let merged_observer = merge_read_observers(read_observer, self.state.read_observer.clone());
62        ReadonlySnapshot::new(
63            self.state.id.get(),
64            self.state.invalid.borrow().clone(),
65            merged_observer,
66        )
67    }
68
69    pub fn has_pending_changes(&self) -> bool {
70        false // Read-only snapshots never have changes
71    }
72
73    pub fn dispose(&self) {
74        self.state.dispose();
75    }
76
77    pub fn record_read(&self, state: &dyn StateObject) {
78        self.state.record_read(state);
79    }
80
81    pub fn record_write(&self, _state: Arc<dyn StateObject>) {
82        panic!("Cannot write to a read-only snapshot");
83    }
84
85    pub fn is_disposed(&self) -> bool {
86        self.state.disposed.get()
87    }
88
89    // Internal: set a callback to run when this snapshot is disposed.
90    pub(crate) fn set_on_dispose<F>(&self, f: F)
91    where
92        F: FnOnce() + 'static,
93    {
94        self.state.set_on_dispose(f);
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::state::StateObject;
102    use std::cell::Cell;
103
104    // Mock StateObject for testing
105    #[allow(dead_code)]
106    struct MockStateObject {
107        value: Cell<i32>,
108    }
109
110    impl StateObject for MockStateObject {
111        fn object_id(&self) -> crate::state::ObjectId {
112            crate::state::ObjectId(0)
113        }
114
115        fn first_record(&self) -> Arc<crate::state::StateRecord> {
116            unimplemented!("Not needed for tests")
117        }
118
119        fn readable_record(
120            &self,
121            _snapshot_id: crate::snapshot_id_set::SnapshotId,
122            _invalid: &SnapshotIdSet,
123        ) -> Arc<crate::state::StateRecord> {
124            unimplemented!("Not needed for tests")
125        }
126
127        fn prepend_state_record(&self, _record: Arc<crate::state::StateRecord>) {
128            unimplemented!("Not needed for tests")
129        }
130
131        fn promote_record(
132            &self,
133            _child_id: crate::snapshot_id_set::SnapshotId,
134        ) -> Result<(), &'static str> {
135            unimplemented!("Not needed for tests")
136        }
137
138        fn as_any(&self) -> &dyn std::any::Any {
139            self
140        }
141    }
142
143    #[test]
144    fn test_readonly_snapshot_creation() {
145        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
146        assert_eq!(snapshot.snapshot_id(), 1);
147        assert!(!snapshot.is_disposed());
148    }
149
150    #[test]
151    fn test_readonly_snapshot_is_valid() {
152        let invalid = SnapshotIdSet::new().set(5);
153        let snapshot = ReadonlySnapshot::new(10, invalid, None);
154
155        let any_snapshot = AnySnapshot::Readonly(snapshot.clone());
156        assert!(any_snapshot.is_valid(1));
157        assert!(any_snapshot.is_valid(10));
158        assert!(!any_snapshot.is_valid(5)); // Invalid
159        assert!(!any_snapshot.is_valid(11)); // Future
160    }
161
162    #[test]
163    fn test_readonly_snapshot_no_pending_changes() {
164        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
165        assert!(!snapshot.has_pending_changes());
166    }
167
168    #[test]
169    fn test_readonly_snapshot_enter() {
170        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
171
172        set_current_snapshot(None);
173        assert!(current_snapshot().is_none());
174
175        snapshot.enter(|| {
176            let current = current_snapshot();
177            assert!(current.is_some());
178            assert_eq!(current.unwrap().snapshot_id(), 1);
179        });
180
181        assert!(current_snapshot().is_none());
182    }
183
184    #[test]
185    fn test_readonly_snapshot_enter_restores_previous() {
186        let snapshot1 = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
187        let snapshot2 = ReadonlySnapshot::new(2, SnapshotIdSet::new(), None);
188
189        snapshot1.enter(|| {
190            snapshot2.enter(|| {
191                let current = current_snapshot();
192                assert_eq!(current.unwrap().snapshot_id(), 2);
193            });
194
195            let current = current_snapshot();
196            assert_eq!(current.unwrap().snapshot_id(), 1);
197        });
198    }
199
200    #[test]
201    fn test_readonly_snapshot_nested() {
202        let parent = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
203        let nested = parent.take_nested_snapshot(None);
204
205        assert_eq!(nested.snapshot_id(), 1); // Same ID
206    }
207
208    #[test]
209    fn test_readonly_snapshot_read_observer() {
210        use std::sync::{Arc as StdArc, Mutex};
211
212        let read_count = StdArc::new(Mutex::new(0));
213        let read_count_clone = read_count.clone();
214
215        let observer = Arc::new(move |_: &dyn StateObject| {
216            *read_count_clone.lock().unwrap() += 1;
217        });
218
219        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), Some(observer));
220        let mock_state = MockStateObject {
221            value: Cell::new(42),
222        };
223
224        snapshot.record_read(&mock_state);
225        snapshot.record_read(&mock_state);
226
227        assert_eq!(*read_count.lock().unwrap(), 2);
228    }
229
230    #[test]
231    fn test_readonly_snapshot_nested_with_observer() {
232        use std::sync::{Arc as StdArc, Mutex};
233
234        let parent_reads = StdArc::new(Mutex::new(0));
235        let parent_reads_clone = parent_reads.clone();
236        let parent_observer = Arc::new(move |_: &dyn StateObject| {
237            *parent_reads_clone.lock().unwrap() += 1;
238        });
239
240        let nested_reads = StdArc::new(Mutex::new(0));
241        let nested_reads_clone = nested_reads.clone();
242        let nested_observer = Arc::new(move |_: &dyn StateObject| {
243            *nested_reads_clone.lock().unwrap() += 1;
244        });
245
246        let parent = ReadonlySnapshot::new(1, SnapshotIdSet::new(), Some(parent_observer));
247        let nested = parent.take_nested_snapshot(Some(nested_observer));
248
249        let mock_state = MockStateObject {
250            value: Cell::new(42),
251        };
252
253        // Reading in nested snapshot should call both observers
254        nested.record_read(&mock_state);
255
256        assert_eq!(*parent_reads.lock().unwrap(), 1);
257        assert_eq!(*nested_reads.lock().unwrap(), 1);
258    }
259
260    #[test]
261    #[should_panic(expected = "Cannot write to a read-only snapshot")]
262    fn test_readonly_snapshot_write_panics() {
263        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
264        let mock_state = Arc::new(MockStateObject {
265            value: Cell::new(42),
266        });
267        snapshot.record_write(mock_state);
268    }
269
270    #[test]
271    fn test_readonly_snapshot_dispose() {
272        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
273        assert!(!snapshot.is_disposed());
274
275        snapshot.dispose();
276        assert!(snapshot.is_disposed());
277    }
278}