tincan 0.3.0

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

/// A reactive signal that holds a value and notifies subscribers when changed.
///
/// Signals are the core primitive for building reactive applications. They automatically
/// track dependencies and notify observers when values change.
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// use tincan::Scope;
///
/// let cx = Scope::new();
/// let count = cx.signal(0);
/// assert_eq!(count.get(), 0);
///
/// count.set(42);
/// assert_eq!(count.get(), 42);
/// ```
///
/// Derived signals with map:
///
/// ```
/// use tincan::Scope;
///
/// let cx = Scope::new();
/// let celsius = cx.signal(0);
/// let fahrenheit = celsius.map(|c| c * 9 / 5 + 32);
///
/// assert_eq!(fahrenheit.get(), 32);
///
/// celsius.set(100);
/// assert_eq!(fahrenheit.get(), 212);
/// ```
#[derive(Clone)]
pub struct Signal<T> {
    value: Rc<RefCell<T>>,
    id: usize,
    runtime: Weak<RefCell<RuntimeInner>>,
}

impl<T: Clone + 'static> Signal<T> {
    /// Create a new signal within a scope (internal use).
    pub(crate) fn new_in_scope(initial: T, id: usize, runtime: Rc<RefCell<RuntimeInner>>) -> Self {
        Self {
            value: Rc::new(RefCell::new(initial)),
            id,
            runtime: Rc::downgrade(&runtime),
        }
    }

    /// Get the current value of the signal.
    ///
    /// This tracks the read in the current reactive context, allowing
    /// effects and memos to automatically subscribe to changes.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(42);
    /// assert_eq!(count.get(), 42);
    /// ```
    pub fn get(&self) -> T {
        if let Some(runtime) = self.runtime.upgrade() {
            runtime.borrow().track_read(self.id);
        }
        self.value.borrow().clone()
    }

    /// Set a new value for the signal.
    ///
    /// This will notify all subscribers (effects, memos, watchers).
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(0);
    /// count.set(42);
    /// assert_eq!(count.get(), 42);
    /// ```
    pub fn set(&self, new_value: T) {
        *self.value.borrow_mut() = new_value;
        if let Some(runtime) = self.runtime.upgrade() {
            runtime.borrow().notify_observers(self.id);
        }
    }

    /// Update the value using a function.
    ///
    /// This is useful for making modifications based on the current value.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(10);
    /// count.update(|n| *n += 5);
    /// assert_eq!(count.get(), 15);
    /// ```
    pub fn update(&self, f: impl FnOnce(&mut T)) {
        {
            let mut value = self.value.borrow_mut();
            f(&mut *value);
        }
        if let Some(runtime) = self.runtime.upgrade() {
            runtime.borrow().notify_observers(self.id);
        }
    }

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

    /// Get the signal's unique ID.
    ///
    /// This is mainly used internally by the reactivity system.
    pub fn id(&self) -> usize {
        self.id
    }

    /// Watch this signal for changes.
    ///
    /// Returns a guard that will unsubscribe when dropped.
    /// The callback is called immediately with the current value.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    /// use std::cell::Cell;
    /// use std::rc::Rc;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(0);
    /// let calls = Rc::new(Cell::new(0));
    ///
    /// let _guard = count.watch({
    ///     let calls = Rc::clone(&calls);
    ///     move |_| {
    ///         calls.set(calls.get() + 1);
    ///     }
    /// });
    ///
    /// // Called immediately
    /// assert_eq!(calls.get(), 1);
    /// ```
    pub fn watch<F>(&self, callback: F) -> WatchGuard
    where
        F: Fn(T) + 'static,
    {
        if let Some(runtime) = self.runtime.upgrade() {
            let observer_id = runtime.borrow().allocate_id();
            let value = Rc::clone(&self.value);
            let callback = Rc::new(callback);
            let callback_clone = Rc::clone(&callback);

            runtime.borrow().create_observer(observer_id, move || {
                let val = value.borrow().clone();
                callback_clone(val);
            });

            // Subscribe to this signal
            RuntimeInner::with_observer_scoped(&runtime, observer_id, || {
                runtime.borrow().track_read(self.id);
            });

            // Call immediately with current value
            let val = self.value.borrow().clone();
            callback(val);

            WatchGuard {
                observer_id,
                runtime: Rc::downgrade(&runtime),
            }
        } else {
            // Runtime dropped - return no-op guard
            WatchGuard {
                observer_id: 0,
                runtime: Weak::new(),
            }
        }
    }

    /// Create a derived signal by applying a function to this signal's value.
    ///
    /// The derived signal automatically updates when the source changes.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(5);
    /// let doubled = count.map(|n| n * 2);
    ///
    /// assert_eq!(doubled.get(), 10);
    ///
    /// count.set(10);
    /// assert_eq!(doubled.get(), 20);
    /// ```
    pub fn map<U, F>(&self, f: F) -> Signal<U>
    where
        U: Clone + 'static,
        F: Fn(&T) -> U + 'static,
    {
        if let Some(runtime) = self.runtime.upgrade() {
            let source = self.clone();
            let derived_id = runtime.borrow().allocate_id();
            let derived = Signal::new_in_scope(f(&self.get()), derived_id, Rc::clone(&runtime));
            let derived_clone = derived.clone();
            let f = Rc::new(f);

            let effect_id = runtime.borrow().allocate_id();
            let _effect = Effect::new_in_scope_with_lifecycle(
                move || {
                    let val = source.get();
                    derived_clone.set(f(&val));
                },
                effect_id,
                Rc::clone(&runtime),
                true, // Run immediately to track dependencies
            );

            // Store effect in runtime to keep it alive
            runtime
                .borrow()
                .register_derived_effect(derived_id, _effect);

            derived
        } else {
            // Runtime dropped - return signal with current computed value
            Signal {
                value: Rc::new(RefCell::new(f(&self.value.borrow()))),
                id: 0,
                runtime: Weak::new(),
            }
        }
    }

    /// Combine two signals into one.
    ///
    /// The combined signal updates when either source changes.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let width = cx.signal(10);
    /// let height = cx.signal(5);
    /// let area = width.clone().zip(height.clone()).map(|(w, h)| w * h);
    ///
    /// assert_eq!(area.get(), 50);
    ///
    /// width.set(20);
    /// assert_eq!(area.get(), 100);
    /// ```
    pub fn zip<U>(self, other: Signal<U>) -> Signal<(T, U)>
    where
        U: Clone + 'static,
    {
        if let Some(runtime) = self.runtime.upgrade() {
            let combined_id = runtime.borrow().allocate_id();
            let combined =
                Signal::new_in_scope((self.get(), other.get()), combined_id, Rc::clone(&runtime));

            let self_clone = self.clone();
            let other_value = Rc::clone(&other.value);
            let combined_clone1 = combined.clone();
            let effect1_id = runtime.borrow().allocate_id();
            let effect1 = Effect::new_in_scope_with_lifecycle(
                move || {
                    let self_val = self_clone.get();
                    let other_val = other_value.borrow().clone();
                    combined_clone1.set((self_val, other_val));
                },
                effect1_id,
                Rc::clone(&runtime),
                true, // Run immediately to track dependencies
            );

            let self_value = Rc::clone(&self.value);
            let other_clone = other.clone();
            let combined_clone2 = combined.clone();
            let effect2_id = runtime.borrow().allocate_id();
            let effect2 = Effect::new_in_scope_with_lifecycle(
                move || {
                    let self_val = self_value.borrow().clone();
                    let other_val = other_clone.get();
                    combined_clone2.set((self_val, other_val));
                },
                effect2_id,
                Rc::clone(&runtime),
                true, // Run immediately to track dependencies
            );

            // Store effects in runtime
            runtime
                .borrow()
                .register_derived_effect(combined_id, effect1);
            runtime
                .borrow()
                .register_derived_effect(combined_id, effect2);

            combined
        } else {
            // Runtime dropped - return signal with current value
            Signal {
                value: Rc::new(RefCell::new((
                    self.value.borrow().clone(),
                    other.value.borrow().clone(),
                ))),
                id: 0,
                runtime: Weak::new(),
            }
        }
    }
}

impl<T> Drop for Signal<T> {
    fn drop(&mut self) {
        // Only remove the signal 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.value) == 1 {
            if let Some(runtime) = self.runtime.upgrade() {
                runtime.borrow().remove_signal(self.id);
            }
        }
    }
}

/// RAII guard for signal watchers.
///
/// When dropped, automatically unsubscribes the watcher.
/// You typically don't create this directly - it's returned by [`Signal::watch`].
pub struct WatchGuard {
    observer_id: usize,
    runtime: Weak<RefCell<RuntimeInner>>,
}

impl Drop for WatchGuard {
    fn drop(&mut self) {
        if let Some(runtime) = self.runtime.upgrade() {
            runtime.borrow().remove_observer(self.observer_id);
        }
    }
}