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/// # Memory Management Note
23///
24/// When referencing `Memo` instances that belong to other struct instances
25/// (for example, when one `ViewModel` holds references to memos in another `ViewModel`),
26/// you **must** store them as `Weak<Memo<T>>` obtained via `Rc::downgrade` instead of
27/// cloning a strong `Rc`. Failing to do so can create reference cycles between the structs
28/// and their dependent effects, preventing proper cleanup and causing memory leaks.
29///
30/// # Examples
31///
32/// ## Basic usage
33/// ```
34/// use std::rc::Rc;
35/// use reactive_cache::{Signal, Memo};
36///
37/// let counter = Signal::new(1);
38/// let double = {
39/// let counter = Rc::clone(&counter);
40/// Memo::new({
41/// let counter = Rc::new(counter);
42/// move || *counter.get() * 2
43/// })
44/// };
45///
46/// assert_eq!(double.get(), 2);
47/// counter.set(3);
48/// assert_eq!(double.get(), 6);
49/// ```
50///
51/// ## Using inside a struct
52/// ```
53/// use std::rc::Rc;
54/// use reactive_cache::{Signal, Memo};
55///
56/// struct ViewModel {
57/// counter: Rc<Signal<i32>>,
58/// double: Rc<Memo<i32>>,
59/// }
60///
61/// let counter = Signal::new(1);
62/// let double = Memo::new({
63/// let counter = counter.clone();
64/// move || *counter.get() * 2
65/// });
66///
67/// let vm = ViewModel { counter, double };
68/// assert_eq!(vm.double.get(), 2);
69/// vm.counter.set(4);
70/// assert_eq!(vm.double.get(), 8);
71/// ```
72pub struct Memo<T> {
73 f: Box<dyn Fn() -> T>,
74 dependents: RefCell<Vec<Weak<dyn IMemo>>>,
75 /// A self-referential weak pointer, set during construction with `Rc::new_cyclic`.
76 /// Used to upgrade to `Rc<Memo<T>>` and then coerce into `Rc<dyn IMemo>` when needed.
77 weak: Weak<Memo<T>>,
78}
79
80impl<T> Memo<T> {
81 /// Creates a new `Memo` wrapping the provided closure.
82 ///
83 /// # Requirements
84 /// - `T` must be `'static`, because the value is stored in global cache.
85 /// - The closure must be `'static` as well.
86 ///
87 /// # Examples
88 ///
89 /// Basic usage:
90 /// ```
91 /// use reactive_cache::Memo;
92 ///
93 /// let memo = Memo::new(|| 10);
94 /// assert_eq!(memo.get(), 10);
95 /// ```
96 ///
97 /// Using inside a struct:
98 /// ```
99 /// use std::rc::Rc;
100 ///
101 /// use reactive_cache::{Signal, Memo};
102 ///
103 /// struct ViewModel {
104 /// a: Rc<Signal<i32>>,
105 /// b: Rc<Signal<i32>>,
106 /// sum: Rc<Memo<i32>>,
107 /// }
108 ///
109 /// // Construct signals
110 /// let a = Signal::new(2);
111 /// let b = Signal::new(3);
112 ///
113 /// // Construct a memo depending on `a` and `b`
114 /// let sum = {
115 /// let a = a.clone();
116 /// let b = b.clone();
117 /// Memo::new(move || {
118 /// // `Signal::get()` will register dependencies automatically
119 /// *a.get() + *b.get()
120 /// })
121 /// };
122 ///
123 /// let vm = ViewModel { a, b, sum };
124 ///
125 /// // Initial computation
126 /// assert_eq!(vm.sum.get(), 5);
127 ///
128 /// // Update a signal → memo recomputes
129 /// vm.a.set(10);
130 /// assert_eq!(vm.sum.get(), 13);
131 /// ```
132 pub fn new(f: impl Fn() -> T + 'static) -> Rc<Self>
133 where
134 T: 'static,
135 {
136 Rc::new_cyclic(|weak| Memo {
137 f: Box::new(f),
138 dependents: vec![].into(),
139 weak: weak.clone(),
140 })
141 }
142
143 /// Returns the memoized value, recomputing it only if necessary.
144 ///
145 /// During the computation, dependencies are tracked for reactive updates.
146 ///
147 /// # Examples
148 ///
149 /// ```
150 /// use reactive_cache::Memo;
151 ///
152 /// let memo = Memo::new(|| 5);
153 /// assert_eq!(memo.get(), 5);
154 /// ```
155 pub fn get(&self) -> T
156 where
157 T: Clone + 'static,
158 {
159 self.dependency_collection();
160
161 memo_stack::push(self.weak.clone());
162
163 let rc = if let Some(this) = self.weak.upgrade() {
164 let key: Rc<dyn IMemo> = this.clone();
165 if let Some(rc) = touch(&key) {
166 rc
167 } else {
168 let result: T = (self.f)();
169 store_in_cache(&key, result)
170 }
171 } else {
172 unreachable!()
173 };
174
175 memo_stack::pop();
176
177 (*rc).clone()
178 }
179}
180
181impl<T> IObservable for Memo<T> {
182 fn dependents(&self) -> &RefCell<Vec<Weak<dyn IMemo>>> {
183 &self.dependents
184 }
185}
186
187/// Internal marker trait for all memoized computations.
188/// Used for type erasure when storing heterogeneous `Memo<T>` in caches.
189pub(crate) trait IMemo: IObservable {}
190
191impl<T> IMemo for Memo<T> {}