tincan 0.3.0

A lightweight reactive state management library for Rust.
Documentation
use crate::runtime::RuntimeInner;
use std::cell::RefCell;
use std::rc::{Rc, Weak};

/// A memoized computed value that automatically tracks dependencies.
///
/// Memos cache their computed value and only recompute when dependencies change,
/// making them efficient for expensive computations.
///
/// # Examples
///
/// ```
/// use tincan::Scope;
///
/// let cx = Scope::new();
/// let count = cx.signal(5);
/// let doubled = cx.memo({
///     let count = count.clone();
///     move || count.get() * 2
/// });
///
/// assert_eq!(doubled.get(), 10);
/// assert_eq!(doubled.get(), 10); // Uses cached value
///
/// count.set(10);
/// assert_eq!(doubled.get(), 20); // Recomputes
/// ```
#[derive(Clone)]
pub struct Memo<T> {
    cached_value: Rc<RefCell<Option<T>>>,
    compute: Rc<dyn Fn() -> T>,
    id: usize,
    runtime: Weak<RefCell<RuntimeInner>>,
}

impl<T: Clone + 'static> Memo<T> {
    /// Create a new memo within a scope (internal use).
    pub(crate) fn new_in_scope<F>(compute: F, id: usize, runtime: Rc<RefCell<RuntimeInner>>) -> Self
    where
        F: Fn() -> T + 'static,
    {
        runtime.borrow().register_memo(id);

        Self {
            cached_value: Rc::new(RefCell::new(None)),
            compute: Rc::new(compute),
            id,
            runtime: Rc::downgrade(&runtime),
        }
    }

    /// Get the current value, recomputing if necessary.
    ///
    /// This tracks the read in the reactive context and recomputes
    /// if any dependencies have changed since the last call.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(5);
    /// let doubled = cx.memo({
    ///     let count = count.clone();
    ///     move || count.get() * 2
    /// });
    ///
    /// assert_eq!(doubled.get(), 10);
    /// ```
    pub fn get(&self) -> T {
        if let Some(runtime) = self.runtime.upgrade() {
            runtime.borrow().track_read(self.id);

            if runtime.borrow().is_memo_dirty(self.id) {
                let value =
                    RuntimeInner::with_observer_scoped(&runtime, self.id, || (self.compute)());
                *self.cached_value.borrow_mut() = Some(value.clone());
                runtime.borrow().mark_memo_clean(self.id);
                value
            } else {
                self.cached_value.borrow().as_ref().unwrap().clone()
            }
        } else {
            // Runtime dropped - recompute without tracking
            (self.compute)()
        }
    }

    /// Read the memoized value with a function without cloning.
    ///
    /// The read is still tracked for reactivity.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(5);
    /// let text = cx.memo({
    ///     let count = count.clone();
    ///     move || format!("Count: {}", count.get())
    /// });
    ///
    /// let len = text.with(|s| s.len());
    /// assert!(len > 0);
    /// ```
    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
        if let Some(runtime) = self.runtime.upgrade() {
            runtime.borrow().track_read(self.id);

            if runtime.borrow().is_memo_dirty(self.id) {
                let value =
                    RuntimeInner::with_observer_scoped(&runtime, self.id, || (self.compute)());
                *self.cached_value.borrow_mut() = Some(value.clone());
                runtime.borrow().mark_memo_clean(self.id);
                let cached = self.cached_value.borrow();
                f(cached.as_ref().unwrap())
            } else {
                let cached = self.cached_value.borrow();
                f(cached.as_ref().unwrap())
            }
        } else {
            // Runtime dropped - recompute without tracking
            let value = (self.compute)();
            f(&value)
        }
    }
}

impl<T> Drop for Memo<T> {
    fn drop(&mut self) {
        // Only remove the memo from runtime when this is the last clone
        // Check if there are other clones by looking at the Rc strong count
        // Count is 1 means this is the last reference (the one we're dropping)
        if Rc::strong_count(&self.cached_value) == 1 {
            if let Some(runtime) = self.runtime.upgrade() {
                runtime.borrow().remove_memo(self.id);
            }
        }
    }
}