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.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    use std::rc::Rc;
104
105    // Mock StateObject for testing
106    #[allow(dead_code)]
107    struct MockStateObject {
108        value: Cell<i32>,
109    }
110
111    impl StateObject for MockStateObject {
112        fn object_id(&self) -> crate::state::ObjectId {
113            crate::state::ObjectId(0)
114        }
115
116        fn first_record(&self) -> Rc<crate::state::StateRecord> {
117            unimplemented!("Not needed for tests")
118        }
119
120        fn readable_record(
121            &self,
122            _snapshot_id: crate::snapshot_id_set::SnapshotId,
123            _invalid: &SnapshotIdSet,
124        ) -> Rc<crate::state::StateRecord> {
125            unimplemented!("Not needed for tests")
126        }
127
128        fn prepend_state_record(&self, _record: Rc<crate::state::StateRecord>) {
129            unimplemented!("Not needed for tests")
130        }
131
132        fn promote_record(
133            &self,
134            _child_id: crate::snapshot_id_set::SnapshotId,
135        ) -> Result<(), &'static str> {
136            unimplemented!("Not needed for tests")
137        }
138
139        fn as_any(&self) -> &dyn std::any::Any {
140            self
141        }
142    }
143
144    #[test]
145    fn test_readonly_snapshot_creation() {
146        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
147        assert_eq!(snapshot.snapshot_id(), 1);
148        assert!(!snapshot.is_disposed());
149    }
150
151    #[test]
152    fn test_readonly_snapshot_is_valid() {
153        let invalid = SnapshotIdSet::new().set(5);
154        let snapshot = ReadonlySnapshot::new(10, invalid, None);
155
156        let any_snapshot = AnySnapshot::Readonly(snapshot.clone());
157        assert!(any_snapshot.is_valid(1));
158        assert!(any_snapshot.is_valid(10));
159        assert!(!any_snapshot.is_valid(5)); // Invalid
160        assert!(!any_snapshot.is_valid(11)); // Future
161    }
162
163    #[test]
164    fn test_readonly_snapshot_no_pending_changes() {
165        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
166        assert!(!snapshot.has_pending_changes());
167    }
168
169    #[test]
170    fn test_readonly_snapshot_enter() {
171        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
172
173        set_current_snapshot(None);
174        assert!(current_snapshot().is_none());
175
176        snapshot.enter(|| {
177            let current = current_snapshot();
178            assert!(current.is_some());
179            assert_eq!(current.unwrap().snapshot_id(), 1);
180        });
181
182        assert!(current_snapshot().is_none());
183    }
184
185    #[test]
186    fn test_readonly_snapshot_enter_restores_previous() {
187        let snapshot1 = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
188        let snapshot2 = ReadonlySnapshot::new(2, SnapshotIdSet::new(), None);
189
190        snapshot1.enter(|| {
191            snapshot2.enter(|| {
192                let current = current_snapshot();
193                assert_eq!(current.unwrap().snapshot_id(), 2);
194            });
195
196            let current = current_snapshot();
197            assert_eq!(current.unwrap().snapshot_id(), 1);
198        });
199    }
200
201    #[test]
202    fn test_readonly_snapshot_nested() {
203        let parent = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
204        let nested = parent.take_nested_snapshot(None);
205
206        assert_eq!(nested.snapshot_id(), 1); // Same ID
207    }
208
209    #[test]
210    fn test_readonly_snapshot_read_observer() {
211        use std::sync::{Arc as StdArc, Mutex};
212
213        let read_count = StdArc::new(Mutex::new(0));
214        let read_count_clone = read_count.clone();
215
216        let observer = Arc::new(move |_: &dyn StateObject| {
217            *read_count_clone.lock().unwrap() += 1;
218        });
219
220        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), Some(observer));
221        let mock_state = MockStateObject {
222            value: Cell::new(42),
223        };
224
225        snapshot.record_read(&mock_state);
226        snapshot.record_read(&mock_state);
227
228        assert_eq!(*read_count.lock().unwrap(), 2);
229    }
230
231    #[test]
232    fn test_readonly_snapshot_nested_with_observer() {
233        use std::sync::{Arc as StdArc, Mutex};
234
235        let parent_reads = StdArc::new(Mutex::new(0));
236        let parent_reads_clone = parent_reads.clone();
237        let parent_observer = Arc::new(move |_: &dyn StateObject| {
238            *parent_reads_clone.lock().unwrap() += 1;
239        });
240
241        let nested_reads = StdArc::new(Mutex::new(0));
242        let nested_reads_clone = nested_reads.clone();
243        let nested_observer = Arc::new(move |_: &dyn StateObject| {
244            *nested_reads_clone.lock().unwrap() += 1;
245        });
246
247        let parent = ReadonlySnapshot::new(1, SnapshotIdSet::new(), Some(parent_observer));
248        let nested = parent.take_nested_snapshot(Some(nested_observer));
249
250        let mock_state = MockStateObject {
251            value: Cell::new(42),
252        };
253
254        // Reading in nested snapshot should call both observers
255        nested.record_read(&mock_state);
256
257        assert_eq!(*parent_reads.lock().unwrap(), 1);
258        assert_eq!(*nested_reads.lock().unwrap(), 1);
259    }
260
261    #[test]
262    #[should_panic(expected = "Cannot write to a read-only snapshot")]
263    fn test_readonly_snapshot_write_panics() {
264        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
265        let mock_state = Arc::new(MockStateObject {
266            value: Cell::new(42),
267        });
268        snapshot.record_write(mock_state);
269    }
270
271    #[test]
272    fn test_readonly_snapshot_dispose() {
273        let snapshot = ReadonlySnapshot::new(1, SnapshotIdSet::new(), None);
274        assert!(!snapshot.is_disposed());
275
276        snapshot.dispose();
277        assert!(snapshot.is_disposed());
278    }
279}