Skip to main content

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.borrow().clone(),
49        )
50    }
51
52    pub fn enter<T>(&self, f: impl FnOnce() -> T) -> T {
53        enter_snapshot_scope(AnySnapshot::Readonly(self.root_readonly()), f)
54    }
55
56    pub fn take_nested_snapshot(&self, read_observer: Option<ReadObserver>) -> Arc<Self> {
57        let merged_observer =
58            merge_read_observers(read_observer, self.state.read_observer.borrow().clone());
59        ReadonlySnapshot::new(
60            self.state.id.get(),
61            self.state.invalid.borrow().clone(),
62            merged_observer,
63        )
64    }
65
66    pub fn has_pending_changes(&self) -> bool {
67        false // Read-only snapshots never have changes
68    }
69
70    pub fn dispose(&self) {
71        self.state.dispose();
72    }
73
74    pub fn record_read(&self, state: &dyn StateObject) {
75        self.state.record_read(state);
76    }
77
78    pub fn record_write(&self, _state: Arc<dyn StateObject>) {
79        panic!("Cannot write to a read-only snapshot");
80    }
81
82    pub fn is_disposed(&self) -> bool {
83        self.state.disposed.get()
84    }
85
86    // Internal: set a callback to run when this snapshot is disposed.
87    pub(crate) fn set_on_dispose<F>(&self, f: F)
88    where
89        F: FnOnce() + 'static,
90    {
91        self.state.set_on_dispose(f);
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::state::{StateObject, PREEXISTING_SNAPSHOT_ID};
99    use std::rc::Rc;
100
101    fn mock_state_record() -> Rc<crate::state::StateRecord> {
102        crate::state::StateRecord::new(PREEXISTING_SNAPSHOT_ID, (), None)
103    }
104
105    // Mock StateObject for testing
106    struct MockStateObject;
107
108    impl StateObject for MockStateObject {
109        fn object_id(&self) -> crate::state::ObjectId {
110            crate::state::ObjectId(0)
111        }
112
113        fn first_record(&self) -> Rc<crate::state::StateRecord> {
114            mock_state_record()
115        }
116
117        fn try_readable_record(
118            &self,
119            snapshot_id: crate::snapshot_id_set::SnapshotId,
120            invalid: &SnapshotIdSet,
121        ) -> Option<Rc<crate::state::StateRecord>> {
122            Some(self.readable_record(snapshot_id, invalid))
123        }
124
125        fn readable_record(
126            &self,
127            _snapshot_id: crate::snapshot_id_set::SnapshotId,
128            _invalid: &SnapshotIdSet,
129        ) -> Rc<crate::state::StateRecord> {
130            mock_state_record()
131        }
132
133        fn prepend_state_record(&self, _record: Rc<crate::state::StateRecord>) {}
134
135        fn promote_record(
136            &self,
137            _child_id: crate::snapshot_id_set::SnapshotId,
138        ) -> Result<(), &'static str> {
139            Ok(())
140        }
141
142        fn as_any(&self) -> &dyn std::any::Any {
143            self
144        }
145    }
146
147    #[test]
148    fn test_readonly_snapshot_creation() {
149        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
150        assert_eq!(snapshot.snapshot_id(), 1);
151        assert!(!snapshot.is_disposed());
152    }
153
154    #[test]
155    fn test_readonly_snapshot_is_valid() {
156        let invalid = SnapshotIdSet::new().set(5);
157        let snapshot = ReadonlySnapshot::new(10, invalid, None);
158
159        let any_snapshot = AnySnapshot::Readonly(snapshot.clone());
160        assert!(any_snapshot.is_valid(1));
161        assert!(any_snapshot.is_valid(10));
162        assert!(!any_snapshot.is_valid(5)); // Invalid
163        assert!(!any_snapshot.is_valid(11)); // Future
164    }
165
166    #[test]
167    fn test_readonly_snapshot_no_pending_changes() {
168        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
169        assert!(!snapshot.has_pending_changes());
170    }
171
172    #[test]
173    fn test_readonly_snapshot_enter() {
174        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
175
176        set_current_snapshot(None);
177        assert!(current_snapshot().is_none());
178
179        snapshot.enter(|| {
180            let current = current_snapshot();
181            assert!(current.is_some());
182            assert_eq!(current.unwrap().snapshot_id(), 1);
183        });
184
185        assert!(current_snapshot().is_none());
186    }
187
188    #[test]
189    fn test_readonly_snapshot_enter_restores_previous() {
190        let snapshot1 = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
191        let snapshot2 = ReadonlySnapshot::new(2, SnapshotIdSet::new(), None);
192
193        snapshot1.enter(|| {
194            snapshot2.enter(|| {
195                let current = current_snapshot();
196                assert_eq!(current.unwrap().snapshot_id(), 2);
197            });
198
199            let current = current_snapshot();
200            assert_eq!(current.unwrap().snapshot_id(), 1);
201        });
202    }
203
204    #[test]
205    fn test_readonly_snapshot_nested() {
206        let parent = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
207        let nested = parent.take_nested_snapshot(None);
208
209        assert_eq!(nested.snapshot_id(), 1); // Same ID
210    }
211
212    #[test]
213    fn test_readonly_snapshot_read_observer() {
214        use std::sync::{Arc as StdArc, Mutex};
215
216        let read_count = StdArc::new(Mutex::new(0));
217        let read_count_clone = read_count.clone();
218
219        let observer = Arc::new(move |_: &dyn StateObject| {
220            *read_count_clone.lock().unwrap() += 1;
221        });
222
223        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), Some(observer));
224        let mock_state = MockStateObject;
225
226        snapshot.record_read(&mock_state);
227        snapshot.record_read(&mock_state);
228
229        assert_eq!(*read_count.lock().unwrap(), 2);
230    }
231
232    #[test]
233    fn test_readonly_snapshot_nested_with_observer() {
234        use std::sync::{Arc as StdArc, Mutex};
235
236        let parent_reads = StdArc::new(Mutex::new(0));
237        let parent_reads_clone = parent_reads.clone();
238        let parent_observer = Arc::new(move |_: &dyn StateObject| {
239            *parent_reads_clone.lock().unwrap() += 1;
240        });
241
242        let nested_reads = StdArc::new(Mutex::new(0));
243        let nested_reads_clone = nested_reads.clone();
244        let nested_observer = Arc::new(move |_: &dyn StateObject| {
245            *nested_reads_clone.lock().unwrap() += 1;
246        });
247
248        let parent = ReadonlySnapshot::new(1, SnapshotIdSet::new(), Some(parent_observer));
249        let nested = parent.take_nested_snapshot(Some(nested_observer));
250
251        let mock_state = MockStateObject;
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        snapshot.record_write(mock_state);
266    }
267
268    #[test]
269    fn test_readonly_snapshot_dispose() {
270        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
271        assert!(!snapshot.is_disposed());
272
273        snapshot.dispose();
274        assert!(snapshot.is_disposed());
275    }
276}