reactive_cache/memo.rs
1use std::{
2 cell::RefCell,
3 rc::{Rc, Weak},
4};
5
6use crate::{IObservable, memo_stack, store_in_cache, touch};
7
8/// A memoized reactive computation that caches its result and tracks dependencies.
9///
10/// `Memo<T>` behaves similarly to a computed property: it stores the result of a closure
11/// and only recomputes when its dependencies change. Other signals or effects that access
12/// the memo will automatically be tracked.
13///
14/// In short:
15/// - Like a computed property: returns a cached value derived from other signals.
16/// - Adds tracking: recomputes only when dependencies are invalidated.
17///
18/// # Type Parameters
19///
20/// - `T`: The result type of the computation. Must implement `Clone`.
21///
22/// # Examples
23///
24/// ## Basic usage
25/// ```
26/// use std::rc::Rc;
27/// use reactive_cache::{Signal, Memo};
28///
29/// let counter = Signal::new(1);
30/// let double = {
31/// let counter = Rc::clone(&counter);
32/// Memo::new({
33/// let counter = Rc::new(counter);
34/// move || *counter.get() * 2
35/// })
36/// };
37///
38/// assert_eq!(double.get(), 2);
39/// counter.set(3);
40/// assert_eq!(double.get(), 6);
41/// ```
42///
43/// ## Using inside a struct
44/// ```
45/// use std::rc::Rc;
46/// use reactive_cache::{Signal, Memo};
47///
48/// struct ViewModel {
49/// counter: Rc<Signal<i32>>,
50/// double: Rc<Memo<i32>>,
51/// }
52///
53/// let counter = Signal::new(1);
54/// let double = Memo::new({
55/// let counter = counter.clone();
56/// move || *counter.get() * 2
57/// });
58///
59/// let vm = ViewModel { counter, double };
60/// assert_eq!(vm.double.get(), 2);
61/// vm.counter.set(4);
62/// assert_eq!(vm.double.get(), 8);
63/// ```
64pub struct Memo<T> {
65 f: Box<dyn Fn() -> T>,
66 dependents: RefCell<Vec<Weak<dyn IMemo>>>,
67 /// A self-referential weak pointer, set during construction with `Rc::new_cyclic`.
68 /// Used to upgrade to `Rc<Memo<T>>` and then coerce into `Rc<dyn IMemo>` when needed.
69 weak: Weak<Memo<T>>,
70}
71
72impl<T> Memo<T> {
73 /// Creates a new `Memo` wrapping the provided closure.
74 ///
75 /// # Requirements
76 /// - `T` must be `'static`, because the value is stored in global cache.
77 /// - The closure must be `'static` as well.
78 ///
79 /// # Examples
80 ///
81 /// Basic usage:
82 /// ```
83 /// use reactive_cache::Memo;
84 ///
85 /// let memo = Memo::new(|| 10);
86 /// assert_eq!(memo.get(), 10);
87 /// ```
88 ///
89 /// Using inside a struct:
90 /// ```
91 /// use std::rc::Rc;
92 ///
93 /// use reactive_cache::{Signal, Memo};
94 ///
95 /// struct ViewModel {
96 /// a: Rc<Signal<i32>>,
97 /// b: Rc<Signal<i32>>,
98 /// sum: Rc<Memo<i32>>,
99 /// }
100 ///
101 /// // Construct signals
102 /// let a = Signal::new(2);
103 /// let b = Signal::new(3);
104 ///
105 /// // Construct a memo depending on `a` and `b`
106 /// let sum = {
107 /// let a = a.clone();
108 /// let b = b.clone();
109 /// Memo::new(move || {
110 /// // `Signal::get()` will register dependencies automatically
111 /// *a.get() + *b.get()
112 /// })
113 /// };
114 ///
115 /// let vm = ViewModel { a, b, sum };
116 ///
117 /// // Initial computation
118 /// assert_eq!(vm.sum.get(), 5);
119 ///
120 /// // Update a signal → memo recomputes
121 /// vm.a.set(10);
122 /// assert_eq!(vm.sum.get(), 13);
123 /// ```
124 pub fn new(f: impl Fn() -> T + 'static) -> Rc<Self>
125 where
126 T: 'static,
127 {
128 Rc::new_cyclic(|weak| Memo {
129 f: Box::new(f),
130 dependents: vec![].into(),
131 weak: weak.clone(),
132 })
133 }
134
135 /// Returns the memoized value, recomputing it only if necessary.
136 ///
137 /// During the computation, dependencies are tracked for reactive updates.
138 ///
139 /// # Examples
140 ///
141 /// ```
142 /// use reactive_cache::Memo;
143 ///
144 /// let memo = Memo::new(|| 5);
145 /// assert_eq!(memo.get(), 5);
146 /// ```
147 pub fn get(&self) -> T
148 where
149 T: Clone + 'static,
150 {
151 self.dependency_collection();
152
153 memo_stack::push(self.weak.clone());
154
155 let rc = if let Some(this) = self.weak.upgrade() {
156 let key: Rc<dyn IMemo> = this.clone();
157 if let Some(rc) = touch(&key) {
158 rc
159 } else {
160 let result: T = (self.f)();
161 store_in_cache(&key, result)
162 }
163 } else {
164 unreachable!()
165 };
166
167 memo_stack::pop();
168
169 (*rc).clone()
170 }
171}
172
173impl<T> IObservable for Memo<T> {
174 fn dependents(&self) -> &RefCell<Vec<Weak<dyn IMemo>>> {
175 &self.dependents
176 }
177}
178
179/// Internal marker trait for all memoized computations.
180/// Used for type erasure when storing heterogeneous `Memo<T>` in caches.
181pub(crate) trait IMemo: IObservable {}
182
183impl<T> IMemo for Memo<T> {}