use crate::arena::{
EffectMetadata, SignalMetadata, current_effect, effect_arena_insert, set_effect_parent,
signal_arena_insert,
};
use crate::{Effect, Signal};
use parking_lot::{Mutex, RwLock};
use std::sync::Arc;
pub struct Computed<T> {
signal: Signal,
effect: Effect,
value: Arc<RwLock<Option<T>>>,
}
impl<T: Clone + PartialEq + Send + Sync + 'static> Computed<T> {
pub fn new<F>(f: F) -> Arc<Self>
where
F: FnMut() -> T + Send + 'static,
{
Self::new_internal(f, false, false)
}
pub fn new_skippable<F>(f: F) -> Arc<Self>
where
F: FnMut() -> T + Send + 'static,
{
Self::new_internal(f, false, true)
}
fn new_internal<F>(f: F, lazy: bool, skippable: bool) -> Arc<Self>
where
F: FnMut() -> T + Send + 'static,
{
let signal_metadata = SignalMetadata::new();
let signal_id = signal_arena_insert(signal_metadata);
let value: Arc<RwLock<Option<T>>> = Arc::new(RwLock::new(None));
let f: Arc<Mutex<Box<dyn FnMut() -> T + Send>>> =
Arc::new(Mutex::new(Box::new(f) as Box<dyn FnMut() -> T + Send>));
let value_for_effect = value.clone();
let f_for_effect = f.clone();
let parent = current_effect();
let callback: Box<dyn FnMut() + Send> = Box::new(move || {
let old_value = value_for_effect.read().clone();
let new_result = {
let mut f_guard = f_for_effect.lock();
(*f_guard)()
};
let changed = match &old_value {
Some(old) => old != &new_result,
None => true, };
*value_for_effect.write() = Some(new_result);
if changed {
signal_id.notify_subscribers();
}
});
let metadata =
EffectMetadata::new_with_callback_parent_and_skippable(callback, parent, skippable);
if lazy {
metadata.set_state(crate::arena::ReactiveState::Dirty);
}
let effect_id = effect_arena_insert(metadata);
if let Some(parent_id) = parent {
set_effect_parent(effect_id, parent_id);
parent_id.add_child(effect_id);
}
if !lazy {
let _guard = crate::arena::CurrentEffectGuard::new(Some(effect_id));
effect_id.run_callback();
}
Arc::new(Self {
signal: Signal::from_raw(signal_id),
effect: Effect::from_raw(effect_id),
value,
})
}
pub fn lazy<F>(f: F) -> Arc<Self>
where
F: FnMut() -> T + Send + 'static,
{
Self::new_internal(f, true, false)
}
pub fn lazy_skippable<F>(f: F) -> Arc<Self>
where
F: FnMut() -> T + Send + 'static,
{
Self::new_internal(f, true, true)
}
pub fn get(&self) -> T {
let effect_id = self.effect.id();
let signal_id = self.signal.id();
let should_recompute = effect_id.needs_work() && {
match current_effect() {
None => true, Some(current) => {
!current.has_source(signal_id)
}
}
};
if should_recompute {
effect_id.update_if_necessary(false);
}
if let Some(current_effect_id) = current_effect() {
current_effect_id.add_source(signal_id);
signal_id.add_subscriber(current_effect_id);
}
self.value
.read()
.clone()
.expect("computed value should always be set after creation")
}
pub fn invalidate(&self) {
crate::arena::mark_effect_pending(self.effect.id());
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::Ordering;
#[test]
fn computed_caches_value() {
let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let computed = Computed::new(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
42
});
let v1 = computed.get();
let v2 = computed.get();
let v3 = computed.get();
assert_eq!(v1, 42);
assert_eq!(v2, 42);
assert_eq!(v3, 42);
assert_eq!(call_count.load(Ordering::Relaxed), 1); }
#[test]
fn computed_recomputes_when_invalidated() {
let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let computed = Computed::new(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
42
});
let _v1 = computed.get();
assert_eq!(call_count.load(Ordering::Relaxed), 1);
computed.invalidate();
crate::effect::flush_effects();
let _v2 = computed.get();
assert_eq!(call_count.load(Ordering::Relaxed), 2);
let _v3 = computed.get();
assert_eq!(call_count.load(Ordering::Relaxed), 2); }
#[test]
fn computed_only_notifies_on_change() {
let counter = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
let counter_clone = counter.clone();
let computed = Computed::new(move || {
counter_clone.fetch_add(1, Ordering::Relaxed);
42
});
let effect_run_count = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
let effect_run_count_clone = effect_run_count.clone();
let computed_signal_id = computed.signal.id();
let _effect = crate::Effect::new(move || {
computed_signal_id.track_dependency();
effect_run_count_clone.fetch_add(1, Ordering::Relaxed);
});
assert_eq!(effect_run_count.load(Ordering::Relaxed), 1);
computed.invalidate();
crate::effect::flush_effects();
assert_eq!(counter.load(Ordering::Relaxed), 2);
assert_eq!(
effect_run_count.load(Ordering::Relaxed),
1,
"Effect should not re-run when computed value doesn't change"
);
}
#[test]
fn computed_notifies_on_actual_change() {
let counter = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
let counter_clone = counter.clone();
let computed = Computed::new(move || {
counter_clone.fetch_add(1, Ordering::Relaxed)
});
assert_eq!(computed.get(), 0);
let effect_run_count = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
let effect_run_count_clone = effect_run_count.clone();
let computed_signal_id = computed.signal.id();
let _effect = crate::Effect::new(move || {
computed_signal_id.track_dependency();
effect_run_count_clone.fetch_add(1, Ordering::Relaxed);
});
assert_eq!(effect_run_count.load(Ordering::Relaxed), 1);
computed.invalidate();
crate::effect::flush_effects();
assert_eq!(computed.get(), 1);
assert_eq!(
effect_run_count.load(Ordering::Relaxed),
2,
"Effect should re-run when computed value changes"
);
}
#[test]
fn test_computed_lazy_defers_computation() {
let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let computed = Computed::lazy(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
42
});
assert_eq!(
call_count.load(Ordering::Relaxed),
0,
"Lazy computed should not run during creation"
);
let v1 = computed.get();
assert_eq!(v1, 42);
assert_eq!(
call_count.load(Ordering::Relaxed),
1,
"Lazy computed should run on first get()"
);
let v2 = computed.get();
assert_eq!(v2, 42);
assert_eq!(
call_count.load(Ordering::Relaxed),
1,
"Lazy computed should cache value after first get()"
);
}
#[test]
fn test_computed_auto_recompute_on_stale_access() {
let counter = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
let counter_clone = counter.clone();
let computed = Computed::new(move || counter_clone.fetch_add(1, Ordering::Relaxed));
assert_eq!(computed.get(), 0);
assert_eq!(counter.load(Ordering::Relaxed), 1);
computed.invalidate();
let v = computed.get();
assert_eq!(v, 1, "Should have recomputed automatically");
assert_eq!(
counter.load(Ordering::Relaxed),
2,
"Should have run computation again"
);
let v2 = computed.get();
assert_eq!(v2, 1);
assert_eq!(
counter.load(Ordering::Relaxed),
2,
"Should not recompute if clean"
);
}
#[test]
fn test_computed_lazy_with_dependencies() {
use crate::Signal;
let signal = Signal::new();
let signal_id = signal.id();
let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let computed = Computed::lazy(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
signal_id.track_dependency();
42
});
assert_eq!(call_count.load(Ordering::Relaxed), 0);
assert_eq!(computed.get(), 42);
assert_eq!(call_count.load(Ordering::Relaxed), 1);
signal.emit();
crate::effect::flush_effects();
assert_eq!(computed.get(), 42);
assert_eq!(call_count.load(Ordering::Relaxed), 2);
}
#[test]
fn test_computed_recompute_inside_effect_first_access() {
use crate::Effect;
let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let computed = Computed::new(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
100
});
assert_eq!(computed.get(), 100);
assert_eq!(call_count.load(Ordering::Relaxed), 1);
computed.invalidate();
crate::effect::flush_effects();
let computed_clone = computed.clone();
let effect_run_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let effect_count_clone = effect_run_count.clone();
let _effect = Effect::new(move || {
effect_count_clone.fetch_add(1, Ordering::Relaxed);
let _val = computed_clone.get();
});
assert_eq!(effect_run_count.load(Ordering::Relaxed), 1);
assert_eq!(call_count.load(Ordering::Relaxed), 2);
}
#[test]
fn test_combined_lazy_and_auto_recompute() {
let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let computed = Computed::lazy(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
call_count_clone.load(Ordering::Relaxed) * 10
});
assert_eq!(call_count.load(Ordering::Relaxed), 0);
let v1 = computed.get();
assert_eq!(v1, 10); assert_eq!(call_count.load(Ordering::Relaxed), 1);
let v2 = computed.get();
assert_eq!(v2, 10);
assert_eq!(call_count.load(Ordering::Relaxed), 1);
computed.invalidate();
let v3 = computed.get();
assert_eq!(v3, 20); assert_eq!(call_count.load(Ordering::Relaxed), 2);
let v4 = computed.get();
assert_eq!(v4, 20);
assert_eq!(call_count.load(Ordering::Relaxed), 2);
}
#[test]
fn skippable_computed_flag_set_correctly() {
let computed = Computed::new_skippable(|| 42);
assert!(computed.effect.id().is_skippable());
let regular_computed = Computed::new(|| 42);
assert!(!regular_computed.effect.id().is_skippable());
let lazy_skippable = Computed::lazy_skippable(|| 42);
assert!(lazy_skippable.effect.id().is_skippable());
let lazy_regular = Computed::lazy(|| 42);
assert!(!lazy_regular.effect.id().is_skippable());
}
}