use super::diagnostics::snapshot_zone;
use super::effect::create_memo_effect;
use super::into_signal::{IntoSignal, MemoMarker};
use super::invalidation::suspend_widget_tracking;
use super::runtime::suspend_effect_tracking;
use super::signal::{RwSignal, Signal, create_signal};
pub struct Memo<T: Clone + PartialEq + Send + 'static> {
signal: RwSignal<T>,
}
impl<T: Clone + PartialEq + Send + 'static> Clone for Memo<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: Clone + PartialEq + Send + 'static> Copy for Memo<T> {}
pub fn create_memo<T, F>(f: F) -> Memo<T>
where
T: Clone + PartialEq + Send + 'static,
F: Fn() -> T + 'static,
{
let initial = snapshot_zone(|| suspend_widget_tracking(|| suspend_effect_tracking(&f)));
let signal = create_signal(initial);
create_memo_effect(move || {
signal.set(f());
});
Memo { signal }
}
impl<T: Clone + PartialEq + Send + 'static> Memo<T> {
pub fn get(&self) -> T {
self.signal.get()
}
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
self.signal.with(f)
}
pub fn watch(&self) -> tokio::sync::watch::Receiver<T>
where
T: Send + Sync,
{
self.signal.watch()
}
pub fn into_signal(self) -> Signal<T> {
self.signal.read_only()
}
}
impl<T: Clone + PartialEq + Send + 'static> IntoSignal<T, MemoMarker> for Memo<T> {
fn into_signal(self) -> Signal<T> {
self.signal.read_only()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memo_initial_value() {
let signal = create_signal(5);
let memo = create_memo(move || signal.get() * 2);
assert_eq!(memo.get(), 10);
}
#[test]
fn test_memo_is_copy() {
let signal = create_signal(1);
let memo = create_memo(move || signal.get());
let memo2 = memo; assert_eq!(memo.get(), 1);
assert_eq!(memo2.get(), 1);
}
#[test]
fn test_memo_with() {
let signal = create_signal(String::from("hello"));
let memo = create_memo(move || signal.get());
let len = memo.with(|s| s.len());
assert_eq!(len, 5);
}
#[test]
fn test_memo_into_signal() {
let signal = create_signal(7);
let memo = create_memo(move || signal.get() + 3);
let sig: Signal<i32> = memo.into_signal();
assert_eq!(sig.get(), 10);
}
#[test]
fn test_effect_depending_on_memo_reruns() {
use std::cell::Cell;
use std::rc::Rc;
let count = create_signal(1);
let doubled = create_memo(move || count.get() * 2);
let observed = Rc::new(Cell::new(0));
let observed_c = observed.clone();
crate::reactive::create_effect(move || {
observed_c.set(doubled.get());
});
assert_eq!(observed.get(), 2);
count.set(5);
assert_eq!(observed.get(), 10, "memo change must propagate to effects");
}
#[test]
fn memo_created_in_a_tracked_scope_does_not_leak_its_dependencies() {
use crate::jobs::{self, JobType};
use crate::reactive::invalidation::with_signal_tracking;
use crate::tree::WidgetId;
let hot = create_signal(0);
let wid = WidgetId::from_u64(4242);
let memo = with_signal_tracking(wid, JobType::Reconcile, || {
create_memo(move || hot.get() * 2)
});
assert_eq!(memo.get(), 0);
jobs::clear_pending_jobs();
hot.set(21);
assert!(
!jobs::has_pending_jobs(),
"writing a signal only the memo reads must not invalidate the \
widget whose scope happened to build the memo"
);
assert_eq!(memo.get(), 42, "the memo itself must still track it");
}
#[test]
fn memo_created_inside_an_effect_does_not_leak_its_dependencies() {
use std::cell::Cell;
use std::rc::Rc;
let hot = create_signal(0);
let trigger = create_signal(0);
let runs = Rc::new(Cell::new(0));
let runs_c = runs.clone();
crate::reactive::create_effect(move || {
trigger.get();
runs_c.set(runs_c.get() + 1);
let _memo = create_memo(move || hot.get() * 2);
});
assert_eq!(runs.get(), 1);
hot.set(1);
assert_eq!(
runs.get(),
1,
"the enclosing effect must not depend on what the memo reads"
);
trigger.set(1);
assert_eq!(runs.get(), 2, "its own dependencies must still fire");
}
#[test]
fn test_memo_chains() {
let base = create_signal(1);
let doubled = create_memo(move || base.get() * 2);
let quadrupled = create_memo(move || doubled.get() * 2);
assert_eq!(quadrupled.get(), 4);
base.set(3);
assert_eq!(doubled.get(), 6);
assert_eq!(quadrupled.get(), 12, "second-level memo must recompute");
}
type Seen = std::rc::Rc<std::cell::RefCell<Vec<(i32, i32)>>>;
fn record(len: RwSignal<i32>, index: Memo<i32>) -> Seen {
let seen = Seen::default();
let sink = seen.clone();
crate::reactive::create_effect(move || sink.borrow_mut().push((len.get(), index.get())));
seen
}
#[test]
fn an_effect_never_sees_a_memo_older_than_the_signal_it_reads() {
for other_readers in 1..=2 {
let len = create_signal(5);
let selected = create_signal(4);
let clamped = create_memo(move || selected.get().min(len.get() - 1));
let seen = record(len, clamped);
for _ in 0..other_readers {
crate::reactive::create_effect(move || {
len.get();
});
}
for shorter in [4, 3, 2, 1] {
len.set(shorter);
}
let seen = seen.borrow();
assert!(
seen.iter().all(|&(len, index)| index < len),
"with {other_readers} other reader(s), an index was not below its length: {seen:?}"
);
assert_eq!(
*seen,
[(5, 4), (4, 3), (3, 2), (2, 1), (1, 0)],
"with {other_readers} other reader(s), one run per write"
);
}
}
#[test]
fn a_memo_of_a_memo_recomputes_before_the_effect_reading_it() {
let len = create_signal(5);
let selected = create_signal(4);
let clamped = create_memo(move || selected.get().min(len.get() - 1));
let index = create_memo(move || clamped.get());
let seen = record(len, index);
crate::reactive::create_effect(move || {
len.get();
});
for shorter in [4, 3, 2, 1] {
len.set(shorter);
}
assert_eq!(*seen.borrow(), [(5, 4), (4, 3), (3, 2), (2, 1), (1, 0)]);
}
#[test]
fn a_resumed_memo_runs_before_the_resumed_effect_reading_it() {
use crate::reactive::owner::{pause_owner, resume_owner, with_owner};
let len = create_signal(5);
let selected = create_signal(4);
let (seen, scope) = with_owner(|| {
let (clamped, _) =
with_owner(|| create_memo(move || selected.get().min(len.get() - 1)));
record(len, clamped)
});
pause_owner(scope);
len.set(4);
resume_owner(scope);
assert_eq!(*seen.borrow(), [(5, 4), (4, 3)]);
}
#[test]
fn an_effect_below_a_diamond_of_memos_sees_only_settled_values() {
let len = create_signal(5);
let selected = create_signal(4);
let clamped = create_memo(move || selected.get().min(len.get() - 1));
let pair = create_memo(move || (len.get(), clamped.get()));
let seen = Seen::default();
let sink = seen.clone();
crate::reactive::create_effect(move || sink.borrow_mut().push(pair.get()));
for shorter in [4, 3, 2, 1] {
len.set(shorter);
}
assert_eq!(*seen.borrow(), [(5, 4), (4, 3), (3, 2), (2, 1), (1, 0)]);
}
}