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