ravenrs_reactive/
state.rs1use std::cell::{Ref, RefCell};
2use std::fmt;
3use std::rc::Rc;
4
5use crate::Binding;
6use crate::observer::Subscribers;
7
8struct StateInner<T> {
9 value: RefCell<T>,
10 subscribers: Subscribers,
11}
12
13pub struct State<T> {
16 inner: Rc<StateInner<T>>,
17}
18
19impl<T: 'static> State<T> {
20 pub fn new(value: T) -> Self {
22 Self {
23 inner: Rc::new(StateInner { value: RefCell::new(value), subscribers: Subscribers::default() }),
24 }
25 }
26
27 pub fn get(&self) -> T
29 where
30 T: Clone,
31 {
32 self.with(T::clone)
33 }
34
35 pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
37 self.inner.subscribers.track_read();
38 f(&self.inner.value.borrow())
39 }
40
41 pub fn peek(&self) -> Ref<'_, T> {
43 self.inner.value.borrow()
44 }
45
46 pub fn set(&self, value: T) {
48 *self.inner.value.borrow_mut() = value;
49 self.inner.subscribers.notify();
50 }
51
52 pub fn set_if_changed(&self, value: T)
54 where
55 T: PartialEq,
56 {
57 let changed = *self.inner.value.borrow() != value;
58 if changed {
59 self.set(value);
60 }
61 }
62
63 pub fn update(&self, f: impl FnOnce(&mut T)) {
65 f(&mut self.inner.value.borrow_mut());
66 self.inner.subscribers.notify();
67 }
68
69 pub fn binding(&self) -> Binding<T>
71 where
72 T: Clone,
73 {
74 let (read, write) = (self.clone(), self.clone());
75 Binding::new(move || read.get(), move |v| write.set(v))
76 }
77
78 #[cfg(test)]
79 pub(crate) fn subscriber_count(&self) -> usize {
80 self.inner.subscribers.len()
81 }
82}
83
84impl<T> Clone for State<T> {
85 fn clone(&self) -> Self {
86 Self { inner: self.inner.clone() }
87 }
88}
89
90impl<T: fmt::Debug> fmt::Debug for State<T> {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 match self.inner.value.try_borrow() {
93 Ok(v) => f.debug_tuple("State").field(&*v).finish(),
94 Err(_) => f.write_str("State(<borrowed>)"),
95 }
96 }
97}
98
99impl<T: Default + 'static> Default for State<T> {
100 fn default() -> Self {
101 Self::new(T::default())
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use std::cell::Cell;
108
109 use super::*;
110 use crate::Observer;
111
112 fn counting_observer() -> (Observer, Rc<Cell<u32>>) {
113 let hits = Rc::new(Cell::new(0));
114 let h = hits.clone();
115 (Observer::new(move || h.set(h.get() + 1)), hits)
116 }
117
118 #[test]
119 fn notifies_only_observers_that_read() {
120 let a = State::new(1);
121 let b = State::new(10);
122 let (obs, hits) = counting_observer();
123 obs.track(|| a.get());
124 b.set(11);
125 assert_eq!(hits.get(), 0, "b was never read");
126 a.set(2);
127 assert_eq!(hits.get(), 1);
128 assert!(obs.is_dirty());
129 }
130
131 #[test]
132 fn callback_fires_once_until_retracked() {
133 let a = State::new(0);
134 let (obs, hits) = counting_observer();
135 obs.track(|| a.get());
136 a.set(1);
137 a.set(2);
138 assert_eq!(hits.get(), 1);
139 obs.track(|| a.get());
140 a.update(|v| *v += 1);
141 assert_eq!(hits.get(), 2);
142 }
143
144 #[test]
145 fn dependencies_are_replaced_on_each_run() {
146 let a = State::new(0);
147 let b = State::new(0);
148 let (obs, hits) = counting_observer();
149 obs.track(|| a.get());
150 obs.track(|| b.get());
151 a.set(5);
152 assert_eq!(hits.get(), 0, "a is no longer a dependency");
153 b.set(5);
154 assert_eq!(hits.get(), 1);
155 }
156
157 #[test]
158 fn set_if_changed_skips_equal_values() {
159 let a = State::new(3);
160 let (obs, hits) = counting_observer();
161 obs.track(|| a.get());
162 a.set_if_changed(3);
163 assert_eq!(hits.get(), 0);
164 a.set_if_changed(4);
165 assert_eq!(hits.get(), 1);
166 }
167
168 #[test]
169 fn dropped_observers_are_pruned() {
170 let a = State::new(0);
171 {
172 let (obs, _) = counting_observer();
173 obs.track(|| a.get());
174 assert_eq!(a.subscriber_count(), 1);
175 }
176 a.set(1);
177 assert_eq!(a.subscriber_count(), 0);
178 }
179
180 #[test]
181 fn reads_are_deduplicated_and_untracked_reads_ignored() {
182 let a = State::new(0);
183 let (obs, hits) = counting_observer();
184 obs.track(|| {
185 a.get();
186 a.get();
187 Observer::untracked(|| a.get());
188 });
189 assert_eq!(a.subscriber_count(), 1);
190 a.set(1);
191 assert_eq!(hits.get(), 1);
192 }
193}