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> {}