use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
pub type SignalId = usize;
pub type EffectId = usize;
thread_local! {
static REACTIVE_CONTEXT: RefCell<ReactiveContext> = RefCell::new(ReactiveContext::new());
static EFFECT_REGISTRY: RefCell<HashMap<EffectId, EffectHandle>> = RefCell::new(HashMap::new());
}
struct ReactiveContext {
current_effect: Option<EffectId>,
cleanups: HashMap<EffectId, Vec<Box<dyn Fn()>>>,
}
impl ReactiveContext {
fn new() -> Self {
Self {
current_effect: None,
cleanups: HashMap::new(),
}
}
}
struct EffectHandle {
f: Rc<dyn Fn()>,
}
pub struct Signal<T> {
id: SignalId,
value: Rc<RefCell<T>>,
subscribers: Rc<RefCell<HashSet<EffectId>>>,
}
impl<T> Clone for Signal<T> {
fn clone(&self) -> Self {
Self {
id: self.id,
value: Rc::clone(&self.value),
subscribers: Rc::clone(&self.subscribers),
}
}
}
impl<T: Clone> Signal<T> {
pub fn new(value: T) -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Self {
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
value: Rc::new(RefCell::new(value)),
subscribers: Rc::new(RefCell::new(HashSet::new())),
}
}
pub fn get(&self) -> T {
REACTIVE_CONTEXT.with(|ctx| {
let ctx = ctx.borrow();
if let Some(effect_id) = ctx.current_effect {
self.subscribers.borrow_mut().insert(effect_id);
}
});
self.value.borrow().clone()
}
pub fn get_untracked(&self) -> T {
self.value.borrow().clone()
}
pub fn set(&self, value: T) {
*self.value.borrow_mut() = value;
self.notify();
}
pub fn update(&self, f: impl FnOnce(&mut T)) {
f(&mut self.value.borrow_mut());
self.notify();
}
fn notify(&self) {
let subscribers = self.subscribers.borrow().clone();
for effect_id in subscribers {
EFFECT_REGISTRY.with(|registry| {
if let Some(effect) = registry.borrow().get(&effect_id) {
(effect.f)();
}
});
}
}
pub fn id(&self) -> SignalId {
self.id
}
}
impl<T: Clone + std::fmt::Debug> std::fmt::Debug for Signal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Signal")
.field("id", &self.id)
.field("value", &self.get_untracked())
.finish()
}
}
pub struct Computed<T> {
signal: Signal<T>,
_effect_id: EffectId,
}
impl<T> Clone for Computed<T> {
fn clone(&self) -> Self {
Self {
signal: self.signal.clone(),
_effect_id: self._effect_id,
}
}
}
impl<T: Clone + 'static> Computed<T> {
pub fn new<F>(compute: F) -> Self
where
F: Fn() -> T + 'static,
{
let compute = Rc::new(compute);
let initial_value = compute();
let signal = Signal::new(initial_value);
let signal_clone = signal.clone();
let compute_clone = compute.clone();
let effect_id = Effect::new(move || {
let new_value = compute_clone();
*signal_clone.value.borrow_mut() = new_value;
});
Self {
signal,
_effect_id: effect_id,
}
}
pub fn get(&self) -> T {
self.signal.get()
}
pub fn get_untracked(&self) -> T {
self.signal.get_untracked()
}
}
impl<T: Clone + std::fmt::Debug + 'static> std::fmt::Debug for Computed<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Computed")
.field("value", &self.get_untracked())
.finish()
}
}
pub struct Effect {
#[allow(dead_code)]
id: EffectId,
}
impl Effect {
pub fn new<F>(f: F) -> EffectId
where
F: Fn() + 'static,
{
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let f = Rc::new(f);
EFFECT_REGISTRY.with(|registry| {
registry.borrow_mut().insert(id, EffectHandle { f: f.clone() });
});
let f_dyn: Rc<dyn Fn()> = f.clone();
Self::run_effect(id, &f_dyn);
id
}
fn run_effect(id: EffectId, f: &Rc<dyn Fn()>) {
REACTIVE_CONTEXT.with(|ctx| {
let prev_effect = ctx.borrow().current_effect;
ctx.borrow_mut().current_effect = Some(id);
f();
ctx.borrow_mut().current_effect = prev_effect;
});
}
pub fn dispose(id: EffectId) {
REACTIVE_CONTEXT.with(|ctx| {
if let Some(cleanups) = ctx.borrow_mut().cleanups.remove(&id) {
for cleanup in cleanups {
cleanup();
}
}
});
EFFECT_REGISTRY.with(|registry| {
registry.borrow_mut().remove(&id);
});
}
}
pub fn on_cleanup<F>(cleanup: F)
where
F: Fn() + 'static,
{
REACTIVE_CONTEXT.with(|ctx| {
let mut ctx = ctx.borrow_mut();
if let Some(effect_id) = ctx.current_effect {
ctx.cleanups
.entry(effect_id)
.or_insert_with(Vec::new)
.push(Box::new(cleanup));
}
});
}
pub fn create_effect<F>(f: F) -> EffectId
where
F: Fn() + 'static,
{
Effect::new(f)
}
pub fn create_computed<T, F>(compute: F) -> Computed<T>
where
T: Clone + 'static,
F: Fn() -> T + 'static,
{
Computed::new(compute)
}
pub fn create_signal<T: Clone>(value: T) -> Signal<T> {
Signal::new(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signal_basic() {
let count = Signal::new(0);
assert_eq!(count.get(), 0);
count.set(5);
assert_eq!(count.get(), 5);
count.update(|c| *c += 10);
assert_eq!(count.get(), 15);
}
#[test]
fn test_signal_effect() {
let count = Signal::new(0);
let result = Rc::new(RefCell::new(0));
let result_clone = result.clone();
let count_clone = count.clone();
Effect::new(move || {
*result_clone.borrow_mut() = count_clone.get() * 2;
});
assert_eq!(*result.borrow(), 0);
count.set(5);
assert_eq!(*result.borrow(), 10);
count.set(10);
assert_eq!(*result.borrow(), 20);
}
#[test]
fn test_computed() {
let count = Signal::new(5);
let count_clone = count.clone();
let doubled = Computed::new(move || count_clone.get() * 2);
assert_eq!(doubled.get(), 10);
count.set(10);
assert_eq!(doubled.get(), 20);
count.set(7);
assert_eq!(doubled.get(), 14);
}
#[test]
fn test_multiple_dependencies() {
let a = Signal::new(2);
let b = Signal::new(3);
let a_clone = a.clone();
let b_clone = b.clone();
let sum = Computed::new(move || a_clone.get() + b_clone.get());
assert_eq!(sum.get(), 5);
a.set(10);
assert_eq!(sum.get(), 13);
b.set(7);
assert_eq!(sum.get(), 17);
}
#[test]
fn test_effect_runs_immediately() {
let ran = Rc::new(RefCell::new(false));
let ran_clone = ran.clone();
Effect::new(move || {
*ran_clone.borrow_mut() = true;
});
assert!(*ran.borrow());
}
#[test]
fn test_untracked_read() {
let count = Signal::new(0);
let effect_count = Rc::new(RefCell::new(0));
let effect_count_clone = effect_count.clone();
let count_clone = count.clone();
Effect::new(move || {
let _ = count_clone.get_untracked();
*effect_count_clone.borrow_mut() += 1;
});
assert_eq!(*effect_count.borrow(), 1);
count.set(5);
assert_eq!(*effect_count.borrow(), 1); }
}