1use crate::reactive::error::WriteResult;
2use crate::reactive::watch::{Immediate, Watch, Watchable};
3use amethystate_core::{Signal, SignalSubscription};
4use std::sync::Arc;
5use uuid::Uuid;
6
7type Writer<T> = Arc<dyn Fn(T) -> WriteResult<()> + Send + Sync>;
8
9#[derive(Clone)]
12pub(crate) struct CellCommit {
13 pub(crate) now: Arc<dyn Fn() -> WriteResult<()> + Send + Sync>,
14 pub(crate) start: Arc<dyn Fn() -> crate::store::Commit + Send + Sync>,
15}
16
17pub struct ReactiveCell<T> {
23 cache: Signal<T>,
24 writer: Writer<T>,
25 origin: Uuid,
26 commit: Option<CellCommit>,
27 _keepalive: Option<Arc<dyn Send + Sync>>,
28}
29
30impl<T> Clone for ReactiveCell<T> {
31 fn clone(&self) -> Self {
32 Self {
33 cache: self.cache.clone(),
34 writer: Arc::clone(&self.writer),
35 origin: self.origin,
36 commit: self.commit.clone(),
37 _keepalive: self._keepalive.clone(),
38 }
39 }
40}
41
42impl<T> ReactiveCell<T>
43where
44 T: Clone + Send + Sync + 'static,
45{
46 pub(crate) fn from_parts(
49 cache: Signal<T>,
50 writer: Writer<T>,
51 origin: Uuid,
52 commit: Option<CellCommit>,
53 keepalive: Option<Arc<dyn Send + Sync>>,
54 ) -> Self {
55 Self {
56 cache,
57 writer,
58 origin,
59 commit,
60 _keepalive: keepalive,
61 }
62 }
63
64 pub fn new(initial: T) -> Self {
66 let cache = Signal::new(initial);
67 let sink = cache.clone();
68
69 Self {
70 cache,
71 writer: Arc::new(move |value| {
72 sink.set(value);
73 Ok(())
74 }),
75 origin: Uuid::new_v4(),
76 commit: None,
77 _keepalive: None,
78 }
79 }
80
81 pub fn get(&self) -> T {
82 self.cache.get()
83 }
84
85 pub fn durable(&self) -> crate::store::Durable<'_, Self> {
90 crate::store::Durable(self)
91 }
92
93 pub fn set(&self, value: T) -> WriteResult<()> {
97 (self.writer)(value)
98 }
99
100 pub fn update<F>(&self, f: F) -> WriteResult<T>
102 where
103 F: FnOnce(T) -> T,
104 {
105 let next = f(self.get());
106 self.set(next.clone())?;
107 Ok(next)
108 }
109
110 pub fn modify<F>(&self, f: F) -> WriteResult<()>
112 where
113 F: FnOnce(&mut T),
114 {
115 let mut value = self.get();
116 f(&mut value);
117 self.set(value)
118 }
119
120 #[track_caller]
121 pub fn subscribe<F>(&self, callback: F) -> SignalSubscription
122 where
123 F: Fn(&T) + Send + Sync + 'static,
124 {
125 self.cache.subscribe(callback)
126 }
127
128 #[track_caller]
131 pub fn subscribe_with_source<F>(&self, callback: F) -> SignalSubscription
132 where
133 F: Fn(&T, Option<Uuid>) + Send + Sync + 'static,
134 {
135 self.cache.subscribe_with_source(callback)
136 }
137
138 pub fn subscription_with(&self) -> Watch<Self, Immediate> {
141 Watch::new(self.clone())
142 }
143}
144
145impl<T> Watchable for ReactiveCell<T>
146where
147 T: Clone + Send + Sync + 'static,
148{
149 type Item = T;
150
151 fn watch_id(&self) -> Uuid {
152 self.origin
153 }
154
155 fn watch_raw<F>(&self, callback: F) -> SignalSubscription
156 where
157 F: Fn(&T, Option<Uuid>) + Send + Sync + 'static,
158 {
159 self.cache.subscribe_with_source(callback)
160 }
161}
162
163impl<T> amethystate_core::pipeline::Reactive<T> for ReactiveCell<T>
164where
165 T: Clone + Send + Sync + 'static,
166{
167 fn get(&self) -> T {
168 self.get()
169 }
170
171 fn subscribe_with_source<F>(&self, callback: F) -> SignalSubscription
172 where
173 F: for<'a> Fn(&'a T, Option<Uuid>) + Send + Sync + 'static,
174 {
175 self.cache.subscribe_with_source(callback)
176 }
177
178 fn keepalive(&self) -> Option<Arc<dyn Send + Sync>> {
179 self._keepalive.clone()
180 }
181}
182
183impl<T: std::fmt::Debug + Clone + Send + Sync + 'static> std::fmt::Debug for ReactiveCell<T> {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 f.debug_struct("ReactiveCell")
186 .field("value", &self.get())
187 .finish_non_exhaustive()
188 }
189}
190
191impl<T> crate::store::Durable<'_, ReactiveCell<T>>
192where
193 T: Clone + Send + Sync + 'static,
194{
195 fn commit(&self) -> WriteResult<()> {
196 match &self.0.commit {
197 Some(commit) => (commit.now)(),
198 None => Ok(()),
199 }
200 }
201
202 async fn commit_async(&self) -> WriteResult<()> {
203 let pending = self.0.commit.as_ref().map(|commit| (commit.start)());
204 match pending {
205 Some(commit) => Ok(commit.await?),
206 None => Ok(()),
207 }
208 }
209
210 pub fn set(&self, value: T) -> WriteResult<()> {
211 self.0.set(value)?;
212 self.commit()
213 }
214
215 pub async fn set_async(&self, value: T) -> WriteResult<()> {
216 self.0.set(value)?;
217 self.commit_async().await
218 }
219
220 pub fn update<F>(&self, f: F) -> WriteResult<T>
221 where
222 F: FnOnce(T) -> T,
223 {
224 let value = self.0.update(f)?;
225 self.commit()?;
226 Ok(value)
227 }
228
229 pub async fn update_async<F>(&self, f: F) -> WriteResult<T>
230 where
231 F: FnOnce(T) -> T,
232 {
233 let value = self.0.update(f)?;
234 self.commit_async().await?;
235 Ok(value)
236 }
237
238 pub fn modify<F>(&self, f: F) -> WriteResult<()>
239 where
240 F: FnOnce(&mut T),
241 {
242 self.0.modify(f)?;
243 self.commit()
244 }
245
246 pub async fn modify_async<F>(&self, f: F) -> WriteResult<()>
247 where
248 F: FnOnce(&mut T),
249 {
250 self.0.modify(f)?;
251 self.commit_async().await
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::DefaultStore;
259 use crate::store::{StateScope, Store};
260 use crate::test_utils::unique_store;
261 use std::sync::Mutex;
262 use std::sync::atomic::{AtomicUsize, Ordering};
263
264 struct UiScope;
265 impl StateScope for UiScope {
266 const PREFIX: &'static str = "ui";
267 }
268
269 fn stored_field(
270 store: &DefaultStore,
271 name: &'static str,
272 default: u64,
273 ) -> crate::WritableField<u64, DefaultStore> {
274 crate::store::field::<UiScope, u64, DefaultStore>(store, name, default, Uuid::new_v4())
275 .expect("field")
276 }
277
278 #[test]
279 fn volatile_cell_round_trips() {
280 let cell = ReactiveCell::new(7u64);
281 assert_eq!(cell.get(), 7);
282
283 cell.set(9).expect("volatile write cannot fail");
284 assert_eq!(cell.get(), 9);
285 }
286
287 #[test]
288 fn field_cell_writes_reach_the_store() {
289 let store = unique_store("write-through");
290 let field = stored_field(&store, "width", 110);
291 let cell = field.cell();
292
293 cell.set(200).expect("write should reach the store");
294
295 assert_eq!(store.get::<u64>("ui.width").unwrap(), Some(200));
296 assert_eq!(cell.get(), 200, "cache must reflect the committed value");
297 assert_eq!(field.get(), 200, "field and cell share one cache");
298 }
299
300 #[test]
301 fn cell_outlives_the_field_it_came_from() {
302 let store = unique_store("outlive");
308 let cell = {
309 let field = stored_field(&store, "height", 10);
310 field.cell()
311 };
312
313 store.set("ui.height", &42u64).expect("external write");
314
315 assert_eq!(
316 cell.get(),
317 42,
318 "cache must still be fed after the field is dropped"
319 );
320 }
321
322 #[test]
323 fn subscribers_see_writes_from_anywhere() {
324 let store = unique_store("subscribe");
325 let field = stored_field(&store, "depth", 1);
326 let cell = field.cell();
327
328 let seen = Arc::new(Mutex::new(Vec::new()));
329 let cap = seen.clone();
330 let _sub = cell.subscribe(move |v: &u64| cap.lock().unwrap().push(*v));
331
332 cell.set(2).unwrap();
333 field.set(3).unwrap();
334 store.set("ui.depth", &4u64).unwrap();
335
336 assert_eq!(*seen.lock().unwrap(), vec![2, 3, 4]);
337 }
338
339 #[test]
340 fn write_through_cell_fires_once_per_write() {
341 let store = unique_store("single-fire");
346 let cell = stored_field(&store, "fires", 0).cell();
347
348 let fires = Arc::new(AtomicUsize::new(0));
349 let cap = fires.clone();
350 let _sub = cell.subscribe(move |_: &u64| {
351 cap.fetch_add(1, Ordering::SeqCst);
352 });
353
354 cell.set(1).unwrap();
355
356 assert_eq!(fires.load(Ordering::SeqCst), 1, "one write, one fire");
357 }
358
359 #[test]
360 fn rejected_write_reports_and_leaves_the_cache_alone() {
361 let store = unique_store("rejected");
365 let field = stored_field(&store, "guarded", 5);
366 let _guard = field.intercept(|_change| None);
367 let cell = field.cell();
368
369 let result = cell.set(99);
370
371 assert!(result.is_err(), "rejected write must not report success");
372 assert_eq!(
373 cell.get(),
374 5,
375 "cache must not hold a value the store refused"
376 );
377 assert_eq!(store.get::<u64>("ui.guarded").unwrap(), Some(5));
378 }
379
380 #[test]
381 fn clones_share_one_value() {
382 let cell = ReactiveCell::new(1u64);
383 let twin = cell.clone();
384
385 twin.set(2).unwrap();
386
387 assert_eq!(cell.get(), 2, "clones are handles onto the same cell");
388 }
389}