1use crate::signal::{create_effect, EffectHandle, Signal};
29use std::rc::Rc;
30
31type SubscriberList<S> = Rc<std::cell::RefCell<Vec<Rc<dyn Fn(&S)>>>>;
35
36pub struct Store<S> {
42 state: Signal<S>,
43 inner: Rc<StoreInner<S>>,
44}
45
46struct StoreInner<S> {
47 subscribers: SubscriberList<S>,
51 history: std::cell::RefCell<Option<HistoryBuf<S>>>,
53 persistence: std::cell::RefCell<Option<Box<dyn Persistence<S>>>>,
55}
56
57struct HistoryBuf<S> {
58 past: Vec<S>,
59 cursor: usize,
62 limit: usize,
63}
64
65impl<S: Clone + 'static> Store<S> {
66 pub fn new(initial: S) -> Self {
68 Store {
69 state: Signal::new(initial),
70 inner: Rc::new(StoreInner {
71 subscribers: SubscriberList::default(),
72 history: std::cell::RefCell::new(None),
73 persistence: std::cell::RefCell::new(None),
74 }),
75 }
76 }
77
78 pub fn with_time_travel(self, limit: usize) -> Self {
80 let initial = self.state.get();
83 *self.inner.history.borrow_mut() = Some(HistoryBuf {
84 past: vec![initial],
85 cursor: 0,
86 limit: limit.max(1),
87 });
88 self
89 }
90
91 pub fn with_persistence(self, backend: Box<dyn Persistence<S>>) -> Self {
93 if let Some(restored) = backend.load() {
94 self.state.set(restored);
95 }
96 *self.inner.persistence.borrow_mut() = Some(backend);
97 self
98 }
99
100 pub fn signal(&self) -> Signal<S> {
102 self.state.clone()
103 }
104
105 pub fn get(&self) -> S {
107 self.state.get()
108 }
109
110 pub fn set(&self, next: S) {
113 self.commit(next);
114 }
115
116 pub fn update(&self, f: impl FnOnce(&S) -> S) {
118 let next = f(&self.state.get());
119 self.commit(next);
120 }
121
122 fn commit(&self, next: S) {
123 if let Some(hist) = self.inner.history.borrow_mut().as_mut() {
126 hist.past.truncate(hist.cursor + 1);
127 hist.past.push(next.clone());
128 if hist.past.len() > hist.limit {
129 hist.past.remove(0);
130 }
131 hist.cursor = hist.past.len() - 1;
132 }
133 self.state.set(next);
134 self.notify();
135 }
136
137 fn notify(&self) {
138 let subs = self.inner.subscribers.borrow();
139 let s = self.state.get();
140 for sub in subs.iter() {
141 sub(&s);
142 }
143 if let Some(p) = self.inner.persistence.borrow().as_ref() {
144 p.save(&s);
145 }
146 }
147
148 pub fn subscribe(&self, cb: impl Fn(&S) + 'static) -> StoreSubscription<S> {
151 let cb = Rc::new(cb) as Rc<dyn Fn(&S)>;
152 self.inner.subscribers.borrow_mut().push(Rc::clone(&cb));
153 StoreSubscription {
154 subscribers: std::rc::Rc::clone(&self.inner.subscribers),
155 cb,
156 }
157 }
158
159 pub fn can_undo(&self) -> bool {
161 self.inner
162 .history
163 .borrow()
164 .as_ref()
165 .map(|h| h.cursor > 0)
166 .unwrap_or(false)
167 }
168
169 pub fn can_redo(&self) -> bool {
171 self.inner
172 .history
173 .borrow()
174 .as_ref()
175 .map(|h| h.cursor + 1 < h.past.len())
176 .unwrap_or(false)
177 }
178
179 pub fn undo(&self) {
182 let mut hist = self.inner.history.borrow_mut();
183 if let Some(h) = hist.as_mut() {
184 if h.cursor > 0 {
185 h.cursor -= 1;
186 let prev = h.past[h.cursor].clone();
187 drop(hist);
188 self.state.set(prev);
189 self.notify_without_history();
190 }
191 }
192 }
193
194 pub fn redo(&self) {
196 let mut hist = self.inner.history.borrow_mut();
197 if let Some(h) = hist.as_mut() {
198 if h.cursor + 1 < h.past.len() {
199 h.cursor += 1;
200 let next = h.past[h.cursor].clone();
201 drop(hist);
202 self.state.set(next);
203 self.notify_without_history();
204 }
205 }
206 }
207
208 fn notify_without_history(&self) {
211 let subs = self.inner.subscribers.borrow();
212 let s = self.state.get();
213 for sub in subs.iter() {
214 sub(&s);
215 }
216 if let Some(p) = self.inner.persistence.borrow().as_ref() {
217 p.save(&s);
218 }
219 }
220
221 pub fn persist_now(&self) {
225 if let Some(p) = self.inner.persistence.borrow().as_ref() {
226 p.save(&self.state.get());
227 }
228 }
229}
230
231pub struct StoreSubscription<S> {
233 subscribers: SubscriberList<S>,
234 cb: Rc<dyn Fn(&S)>,
235}
236
237impl<S> Drop for StoreSubscription<S> {
238 fn drop(&mut self) {
239 let mut subs = self.subscribers.borrow_mut();
240 subs.retain(|other| !Rc::ptr_eq(other, &self.cb));
241 }
242}
243
244pub trait Persistence<S>: 'static {
247 fn load(&self) -> Option<S>;
249 fn save(&self, state: &S);
251}
252
253#[cfg(target_arch = "wasm32")]
254pub struct WebStorage {
259 key: String,
260}
261
262#[cfg(target_arch = "wasm32")]
263impl WebStorage {
264 pub fn new(key: &str) -> Self {
266 WebStorage {
267 key: key.to_string(),
268 }
269 }
270}
271
272#[cfg(target_arch = "wasm32")]
273impl<S: serde::Serialize + serde::de::DeserializeOwned + 'static> Persistence<S> for WebStorage {
274 fn load(&self) -> Option<S> {
275 let window = web_sys::window()?;
276 let storage = window.local_storage().ok().flatten()?;
277 let raw = storage.get_item(&self.key).ok().flatten()?;
278 serde_json::from_str(&raw).ok()
279 }
280
281 fn save(&self, state: &S) {
282 if let Some(window) = web_sys::window() {
283 if let Some(storage) = window.local_storage().ok().flatten() {
284 if let Ok(json) = serde_json::to_string(state) {
285 let _ = storage.set_item(&self.key, &json);
286 }
287 }
288 }
289 }
290}
291
292pub fn instrument_store<S: Clone + 'static>(store: &Store<S>, label: &str) -> EffectHandle {
300 let sig = store.signal().labeled(label);
301 create_effect(move || {
302 let _ = sig.get();
303 })
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
311 struct AppState {
312 count: i32,
313 name: String,
314 }
315
316 #[test]
317 fn set_and_get_round_trip() {
318 let store = Store::new(AppState {
319 count: 0,
320 name: "x".into(),
321 });
322 store.set(AppState {
323 count: 1,
324 name: "y".into(),
325 });
326 assert_eq!(store.get().count, 1);
327 }
328
329 #[test]
330 fn update_applies_transformation() {
331 let store = Store::new(AppState {
332 count: 0,
333 name: "x".into(),
334 });
335 store.update(|s| AppState {
336 count: s.count + 5,
337 name: s.name.clone(),
338 });
339 assert_eq!(store.get().count, 5);
340 }
341
342 #[test]
343 fn subscribers_fire_on_commit() {
344 let store = Store::new(AppState {
345 count: 0,
346 name: "x".into(),
347 });
348 let seen = Rc::new(std::cell::RefCell::new(Vec::new()));
349 let seen2 = seen.clone();
350 let _sub = store.subscribe(move |s: &AppState| {
351 seen2.borrow_mut().push(s.count);
352 });
353 store.set(AppState {
354 count: 1,
355 name: "a".into(),
356 });
357 store.update(|s| AppState {
358 count: s.count + 1,
359 name: "b".into(),
360 });
361 assert_eq!(*seen.borrow(), vec![1, 2]);
362 }
363
364 #[test]
365 fn dropping_subscription_stops_callbacks() {
366 let store = Store::new(AppState {
367 count: 0,
368 name: "x".into(),
369 });
370 let seen = Rc::new(std::cell::RefCell::new(0usize));
371 let seen2 = seen.clone();
372 let sub = store.subscribe(move |_: &AppState| {
373 *seen2.borrow_mut() += 1;
374 });
375 store.set(AppState {
376 count: 1,
377 name: "a".into(),
378 });
379 drop(sub);
380 store.set(AppState {
381 count: 2,
382 name: "b".into(),
383 });
384 assert_eq!(*seen.borrow(), 1, "no callback after drop");
385 }
386
387 #[test]
388 fn time_travel_undo_redo() {
389 let store = Store::new(AppState {
390 count: 0,
391 name: "x".into(),
392 })
393 .with_time_travel(10);
394
395 store.set(AppState {
396 count: 1,
397 name: "a".into(),
398 });
399 store.set(AppState {
400 count: 2,
401 name: "b".into(),
402 });
403 assert!(store.can_undo());
404 assert!(!store.can_redo());
405
406 store.undo();
407 assert_eq!(store.get().count, 1);
408 assert!(store.can_redo());
409
410 store.undo();
411 assert_eq!(store.get().count, 0);
412
413 store.redo();
414 assert_eq!(store.get().count, 1);
415 assert_eq!(store.get().name, "a");
416 }
417
418 #[test]
419 fn new_commit_after_undo_truncates_redo_branch() {
420 let store = Store::new(AppState {
421 count: 0,
422 name: "x".into(),
423 })
424 .with_time_travel(10);
425 store.set(AppState {
426 count: 1,
427 name: "a".into(),
428 });
429 store.set(AppState {
430 count: 2,
431 name: "b".into(),
432 });
433 store.undo(); store.set(AppState {
435 count: 9,
436 name: "c".into(),
437 });
438 assert!(!store.can_redo(), "redo branch truncated by new commit");
439 assert_eq!(store.get().count, 9);
440 }
441
442 struct MemoryStorage {
445 cell: std::rc::Rc<std::cell::RefCell<Option<String>>>,
446 }
447
448 impl Persistence<AppState> for MemoryStorage {
449 fn load(&self) -> Option<AppState> {
450 self.cell
451 .borrow()
452 .as_ref()
453 .and_then(|s| serde_json::from_str(s).ok())
454 }
455 fn save(&self, state: &AppState) {
456 *self.cell.borrow_mut() = Some(serde_json::to_string(state).unwrap());
457 }
458 }
459
460 #[test]
461 fn persistence_hydrates_initial_and_writes_on_commit() {
462 let cell = std::rc::Rc::new(std::cell::RefCell::new(None));
463 *cell.borrow_mut() = Some(
464 serde_json::to_string(&AppState {
465 count: 42,
466 name: "seed".into(),
467 })
468 .unwrap(),
469 );
470
471 let store = Store::new(AppState {
472 count: 0,
473 name: "default".into(),
474 })
475 .with_persistence(Box::new(MemoryStorage { cell: cell.clone() }));
476
477 assert_eq!(store.get().count, 42);
478 assert_eq!(store.get().name, "seed");
479
480 store.set(AppState {
481 count: 7,
482 name: "updated".into(),
483 });
484 store.persist_now();
485 let raw = cell.borrow();
486 let restored: AppState = serde_json::from_str(raw.as_ref().unwrap()).unwrap();
487 assert_eq!(restored.count, 7);
488 assert_eq!(restored.name, "updated");
489 }
490
491 #[test]
492 fn instrument_store_records_writes_in_devtools() {
493 use crate::signal::{reset_signal_activity, signal_activity};
494 reset_signal_activity();
495 let store = Store::new(AppState {
496 count: 0,
497 name: "x".into(),
498 });
499 let _handle = instrument_store(&store, "counter_store");
500 store.set(AppState {
501 count: 1,
502 name: "a".into(),
503 });
504 store.set(AppState {
505 count: 2,
506 name: "b".into(),
507 });
508 assert_eq!(
509 signal_activity().get("counter_store"),
510 Some(&2),
511 "devtools observes two store writes"
512 );
513 }
514}