tincan 0.3.0

A lightweight reactive state management library for Rust.
Documentation
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Forward declaration - Effect is defined in effect module
#[allow(unused_imports)]
use crate::Effect;

/// Lock-free runtime state for single-threaded reactivity.
///
/// This struct manages the reactive dependency graph, tracking which observers
/// depend on which signals, and triggering re-execution when dependencies change.
pub struct RuntimeInner {
    /// Counter for generating unique IDs (centralized allocation)
    id_counter: AtomicUsize,
    /// Current observer context (for tracking reads)
    pub(crate) current_observer: RefCell<Option<usize>>,
    /// Map from signal ID to set of observer IDs that depend on it
    pub(crate) dependencies: RefCell<HashMap<usize, HashSet<usize>>>,
    /// Map from observer ID to set of signal IDs it depends on
    pub(crate) observer_deps: RefCell<HashMap<usize, HashSet<usize>>>,
    /// Map from observer ID to the effect function
    pub(crate) observers: RefCell<HashMap<usize, Rc<dyn Fn()>>>,
    /// Map from memo ID to dirty state
    pub(crate) memo_dirty: RefCell<HashMap<usize, bool>>,
    /// Storage for derived signal effects (map, zip) to keep them alive
    pub(crate) derived_effects: RefCell<HashMap<usize, Vec<crate::Effect>>>,
}

impl RuntimeInner {
    pub fn new() -> Self {
        Self {
            id_counter: AtomicUsize::new(0),
            current_observer: RefCell::new(None),
            dependencies: RefCell::new(HashMap::new()),
            observer_deps: RefCell::new(HashMap::new()),
            observers: RefCell::new(HashMap::new()),
            memo_dirty: RefCell::new(HashMap::new()),
            derived_effects: RefCell::new(HashMap::new()),
        }
    }

    /// Allocate a unique ID from the centralized counter.
    pub fn allocate_id(&self) -> usize {
        self.id_counter.fetch_add(1, Ordering::SeqCst)
    }

    pub fn track_read(&self, signal_id: usize) {
        let current_observer = *self.current_observer.borrow();
        if let Some(current_observer) = current_observer {
            self.dependencies
                .borrow_mut()
                .entry(signal_id)
                .or_insert_with(HashSet::new)
                .insert(current_observer);
            self.observer_deps
                .borrow_mut()
                .entry(current_observer)
                .or_insert_with(HashSet::new)
                .insert(signal_id);
        }
    }

    pub fn notify_observers(&self, signal_id: usize) {
        let observers: Vec<usize> = self
            .dependencies
            .borrow()
            .get(&signal_id)
            .map(|obs| obs.iter().copied().collect())
            .unwrap_or_default();

        for observer_id in observers {
            self.mark_observer_dirty(observer_id);
        }
    }

    fn mark_observer_dirty(&self, observer_id: usize) {
        let is_memo = self.memo_dirty.borrow().contains_key(&observer_id);

        if is_memo {
            let already_dirty = self
                .memo_dirty
                .borrow()
                .get(&observer_id)
                .copied()
                .unwrap_or(false);

            if !already_dirty {
                self.memo_dirty.borrow_mut().insert(observer_id, true);

                let dependents: Vec<usize> = self
                    .dependencies
                    .borrow()
                    .get(&observer_id)
                    .map(|deps| deps.iter().copied().collect())
                    .unwrap_or_default();

                for dependent_id in dependents {
                    self.mark_observer_dirty(dependent_id);
                }
            }
        } else {
            let effect = self.observers.borrow().get(&observer_id).cloned();
            if let Some(effect) = effect {
                effect();
            }
        }
    }

    pub fn create_observer<F>(&self, observer_id: usize, f: F)
    where
        F: Fn() + 'static,
    {
        if let Some(old_deps) = self.observer_deps.borrow_mut().remove(&observer_id) {
            for signal_id in old_deps {
                if let Some(deps) = self.dependencies.borrow_mut().get_mut(&signal_id) {
                    deps.remove(&observer_id);
                }
            }
        }

        self.observers.borrow_mut().insert(observer_id, Rc::new(f));
    }

    /// Execute a function with an observer context.
    /// This is used to track which signals are read during computation.
    pub fn with_observer_scoped<F, R>(
        runtime: &Rc<RefCell<RuntimeInner>>,
        observer_id: usize,
        f: F,
    ) -> R
    where
        F: FnOnce() -> R,
    {
        // Use RAII guard for proper cleanup even on panic
        struct Guard<'a> {
            runtime: &'a Rc<RefCell<RuntimeInner>>,
            prev_observer: Option<usize>,
        }

        impl<'a> Drop for Guard<'a> {
            fn drop(&mut self) {
                *self.runtime.borrow().current_observer.borrow_mut() = self.prev_observer;
            }
        }

        // Set the current observer
        let prev_observer = runtime
            .borrow()
            .current_observer
            .borrow_mut()
            .replace(observer_id);

        let _guard = Guard {
            runtime,
            prev_observer,
        };

        f()
    }

    pub fn register_memo(&self, memo_id: usize) {
        self.memo_dirty.borrow_mut().insert(memo_id, true);
    }

    pub fn is_memo_dirty(&self, memo_id: usize) -> bool {
        self.memo_dirty
            .borrow()
            .get(&memo_id)
            .copied()
            .unwrap_or(true)
    }

    pub fn mark_memo_clean(&self, memo_id: usize) {
        self.memo_dirty.borrow_mut().insert(memo_id, false);
    }

    pub fn remove_observer(&self, observer_id: usize) {
        // Collect the item to drop first, then release borrow before dropping
        // This prevents reentrancy issues when drop handlers try to borrow_mut
        let to_drop = self.observers.borrow_mut().remove(&observer_id);

        let old_deps = self.observer_deps.borrow_mut().remove(&observer_id);
        if let Some(old_deps) = old_deps {
            for signal_id in old_deps {
                if let Some(deps) = self.dependencies.borrow_mut().get_mut(&signal_id) {
                    deps.remove(&observer_id);
                }
            }
        }

        // Drop after releasing all borrows
        drop(to_drop);
    }

    pub fn remove_signal(&self, signal_id: usize) {
        // Collect items to drop first, then release borrows before dropping
        // This prevents reentrancy issues when drop handlers try to borrow_mut
        let derived_to_drop = self.derived_effects.borrow_mut().remove(&signal_id);
        let observers = self.dependencies.borrow_mut().remove(&signal_id);

        if let Some(observers) = observers {
            for observer_id in &observers {
                if let Some(deps) = self.observer_deps.borrow_mut().get_mut(observer_id) {
                    deps.remove(&signal_id);
                }
            }
        }

        // Drop after releasing all borrows
        drop(derived_to_drop);
    }

    pub fn remove_memo(&self, memo_id: usize) {
        // Collect items to drop first, then release borrows before dropping
        // This prevents reentrancy issues when drop handlers try to borrow_mut
        self.memo_dirty.borrow_mut().remove(&memo_id);
        let observer_to_drop = self.observers.borrow_mut().remove(&memo_id);
        let old_deps = self.observer_deps.borrow_mut().remove(&memo_id);

        if let Some(old_deps) = old_deps {
            for signal_id in old_deps {
                if let Some(deps) = self.dependencies.borrow_mut().get_mut(&signal_id) {
                    deps.remove(&memo_id);
                }
            }
        }

        // Drop after releasing all borrows
        drop(observer_to_drop);
    }

    pub fn clear(&self) {
        *self.current_observer.borrow_mut() = None;
        self.dependencies.borrow_mut().clear();
        self.observer_deps.borrow_mut().clear();
        self.observers.borrow_mut().clear();
        self.memo_dirty.borrow_mut().clear();
        self.derived_effects.borrow_mut().clear();
    }

    /// Register an effect to keep it alive for a derived signal (map/zip).
    pub fn register_derived_effect(&self, signal_id: usize, effect: crate::Effect) {
        self.derived_effects
            .borrow_mut()
            .entry(signal_id)
            .or_insert_with(Vec::new)
            .push(effect);
    }
}