mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: new reactive system implementation (#637)
This commit is contained in:
parent
38daaf3b72
commit
817152ff39
20 changed files with 1242 additions and 364 deletions
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
|
@ -27,4 +28,4 @@ features = [
|
|||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement"
|
||||
]
|
||||
]
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
extern crate test;
|
||||
|
||||
//mod reactive;
|
||||
mod ssr;
|
||||
mod todomvc;
|
||||
mod reactive;
|
||||
//mod ssr;
|
||||
//mod todomvc;
|
||||
|
|
|
@ -1,35 +1,114 @@
|
|||
use std::{cell::Cell, rc::Rc};
|
||||
use test::Bencher;
|
||||
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
fn leptos_deep_creation(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_deep_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_down(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
fn leptos_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(|cx| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
|
@ -48,17 +127,20 @@ fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
|||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_scope, create_signal};
|
||||
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope({
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
|
@ -76,16 +158,183 @@ fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
|||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
fn l021_deep_creation(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_narrowing_down(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
#[bench]
|
||||
fn l021_narrowing_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
writes[1].update(|n| *n += 1);
|
||||
writes[10].update(|n| *n += 1);
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for disposer in disposers {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_narrowing_down(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
|
@ -97,13 +346,80 @@ fn sycamore_create_1000_signals(b: &mut Bencher) {
|
|||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
fn sycamore_fanning_out(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let sig = create_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 1000);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_creation(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(*memos[999].get(), 1001);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
#[bench]
|
||||
fn sycamore_narrowing_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
|
@ -129,7 +445,7 @@ fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
|||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
fn sycamore_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
|
|
|
@ -10,6 +10,11 @@ fn Component(
|
|||
#[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize,
|
||||
#[prop(into)] into: String,
|
||||
) -> impl IntoView {
|
||||
_ = optional;
|
||||
_ = optional_no_strip;
|
||||
_ = strip_option;
|
||||
_ = default;
|
||||
_ = into;
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -24,6 +24,7 @@ bytecheck = { version = "0.7", features = [
|
|||
"uuid",
|
||||
"simdutf8",
|
||||
], optional = true }
|
||||
rustc-hash = "1"
|
||||
serde-wasm-bindgen = "0.5"
|
||||
serde_json = "1"
|
||||
base64 = "0.21"
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
#![forbid(unsafe_code)]
|
||||
|
||||
use crate::{runtime::with_runtime, Scope};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
|
||||
/// Provides a context value of type `T` to the current reactive [Scope](crate::Scope)
|
||||
/// and all of its descendants. This can be consumed using [use_context](crate::use_context).
|
||||
|
@ -57,8 +55,7 @@ where
|
|||
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut contexts = runtime.scope_contexts.borrow_mut();
|
||||
let context =
|
||||
contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
|
||||
let context = contexts.entry(cx.id).unwrap().or_default();
|
||||
context.insert(id, Box::new(value) as Box<dyn Any>);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
macros::debug_warn,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
Runtime, Scope, ScopeProperty,
|
||||
};
|
||||
use crate::{Scope, ScopeProperty};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{cell::RefCell, fmt::Debug};
|
||||
use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
|
||||
/// Effects run a certain chunk of code whenever the signals they depend on change.
|
||||
/// `create_effect` immediately runs the given function once, tracks its dependence
|
||||
|
@ -69,6 +65,7 @@ where
|
|||
cfg_if! {
|
||||
if #[cfg(not(feature = "ssr"))] {
|
||||
let e = cx.runtime.create_effect(f);
|
||||
//eprintln!("created effect {e:?}");
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
|
||||
} else {
|
||||
// clear warnings
|
||||
|
@ -123,6 +120,7 @@ pub fn create_isomorphic_effect<T>(
|
|||
T: 'static,
|
||||
{
|
||||
let e = cx.runtime.create_effect(f);
|
||||
//eprintln!("created effect {e:?}");
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
|
||||
}
|
||||
|
||||
|
@ -145,27 +143,22 @@ where
|
|||
create_effect(cx, f);
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to an [Effect](crate::Effect).
|
||||
pub(crate) struct EffectId;
|
||||
}
|
||||
|
||||
pub(crate) struct Effect<T, F>
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> T,
|
||||
{
|
||||
pub(crate) f: F,
|
||||
pub(crate) value: RefCell<Option<T>>,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
pub(crate) trait AnyEffect {
|
||||
fn run(&self, id: EffectId, runtime: RuntimeId);
|
||||
pub(crate) trait AnyComputation {
|
||||
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool;
|
||||
}
|
||||
|
||||
impl<T, F> AnyEffect for Effect<T, F>
|
||||
impl<T, F> AnyComputation for Effect<T, F>
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> T,
|
||||
|
@ -177,73 +170,34 @@ where
|
|||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn run(&self, id: EffectId, runtime: RuntimeId) {
|
||||
_ = with_runtime(runtime, |runtime| {
|
||||
// clear previous dependencies
|
||||
id.cleanup(runtime);
|
||||
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
|
||||
// we defensively take and release the BorrowMut twice here
|
||||
// in case a change during the effect running schedules a rerun
|
||||
// ideally this should never happen, but this guards against panic
|
||||
let curr_value = {
|
||||
// downcast value
|
||||
let mut value = value.borrow_mut();
|
||||
let value = value
|
||||
.downcast_mut::<Option<T>>()
|
||||
.expect("to downcast effect value");
|
||||
value.take()
|
||||
};
|
||||
|
||||
// set this as the current observer
|
||||
let prev_observer = runtime.observer.take();
|
||||
runtime.observer.set(Some(id));
|
||||
// run the effect
|
||||
let new_value = (self.f)(curr_value);
|
||||
|
||||
// run the effect
|
||||
let value = self.value.take();
|
||||
let new_value = (self.f)(value);
|
||||
*self.value.borrow_mut() = Some(new_value);
|
||||
// set new value
|
||||
let mut value = value.borrow_mut();
|
||||
let value = value
|
||||
.downcast_mut::<Option<T>>()
|
||||
.expect("to downcast effect value");
|
||||
*value = Some(new_value);
|
||||
|
||||
// restore the previous observer
|
||||
runtime.observer.set(prev_observer);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectId {
|
||||
pub(crate) fn run(&self, runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
effects.get(*self).cloned()
|
||||
};
|
||||
if let Some(effect) = effect {
|
||||
effect.run(*self, runtime_id);
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Effect] Trying to run an Effect that has been disposed. \
|
||||
This is probably either a logic error in a component \
|
||||
that creates and disposes of scopes, or a Resource \
|
||||
resolving after its scope has been dropped without \
|
||||
having been cleaned up."
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Effect::cleanup()",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self,
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub(crate) fn cleanup(&self, runtime: &Runtime) {
|
||||
let sources = runtime.effect_sources.borrow();
|
||||
if let Some(sources) = sources.get(*self) {
|
||||
let subs = runtime.signal_subscribers.borrow();
|
||||
for source in sources.borrow().iter() {
|
||||
if let Some(source) = subs.get(*source) {
|
||||
source.borrow_mut().remove(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ mod context;
|
|||
mod effect;
|
||||
mod hydration;
|
||||
mod memo;
|
||||
mod node;
|
||||
mod resource;
|
||||
mod runtime;
|
||||
mod scope;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_effect, on_cleanup, ReadSignal, Scope, SignalGet,
|
||||
SignalGetUntracked, SignalStream, SignalWith, SignalWithUntracked,
|
||||
create_effect, node::NodeId, on_cleanup, with_runtime, AnyComputation,
|
||||
RuntimeId, Scope, SignalGet, SignalGetUntracked, SignalStream, SignalWith,
|
||||
SignalWithUntracked,
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
|
||||
|
||||
/// Creates an efficient derived reactive value based on other reactive values.
|
||||
///
|
||||
|
@ -64,7 +65,8 @@ use std::fmt::Debug;
|
|||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = ?cx.id,
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
|
@ -145,23 +147,29 @@ where
|
|||
/// # }).dispose();
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Memo<T>(
|
||||
pub(crate) ReadSignal<Option<T>>,
|
||||
#[cfg(debug_assertions)] pub(crate) &'static std::panic::Location<'static>,
|
||||
)
|
||||
pub struct Memo<T>
|
||||
where
|
||||
T: 'static;
|
||||
T: 'static,
|
||||
{
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Memo<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self(
|
||||
self.0,
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
self.1,
|
||||
)
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,16 +183,23 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
|||
name = "Memo::get_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T {
|
||||
// Unwrapping is fine because `T` will already be `Some(T)` by
|
||||
// the time this method can be called
|
||||
self.0.get_untracked().unwrap()
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
match self.id.try_with_no_subscription(runtime, T::clone) {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at,
|
||||
),
|
||||
}
|
||||
})
|
||||
.expect("runtime to be alive")
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
|
@ -194,14 +209,18 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
|||
name = "Memo::try_get_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn try_get_untracked(&self) -> Option<T> {
|
||||
self.0.try_get_untracked().flatten()
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
self.id.try_with_no_subscription(runtime, T::clone).ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,8 +232,8 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
|||
name = "Memo::with_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
|
@ -222,7 +241,16 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
|||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
// Unwrapping here is fine for the same reasons as <Memo as
|
||||
// UntrackedSignal>::get_untracked
|
||||
self.0.with_untracked(|v| f(v.as_ref().unwrap()))
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
match self.id.try_with_no_subscription(runtime, |v: &T| f(v)) {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at,
|
||||
),
|
||||
}
|
||||
})
|
||||
.expect("runtime to be alive")
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
|
@ -232,14 +260,18 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
|||
name = "Memo::try_with_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0.try_with_untracked(|t| f(t.as_ref().unwrap()))
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
self.id.try_with_no_subscription(runtime, |v: &T| f(v)).ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,13 +299,14 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
|||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get(&self) -> T {
|
||||
self.0.get().unwrap()
|
||||
self.with(T::clone)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
|
@ -283,14 +316,14 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
|||
name = "Memo::try_get()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn try_get(&self) -> Option<T> {
|
||||
self.0.try_get().flatten()
|
||||
self.try_with(T::clone)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,14 +335,20 @@ impl<T> SignalWith<T> for Memo<T> {
|
|||
name = "Memo::with()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
self.0.with(|t| f(t.as_ref().unwrap()))
|
||||
match self.try_with(f) {
|
||||
Some(t) => t,
|
||||
None => panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
|
@ -319,18 +358,40 @@ impl<T> SignalWith<T> for Memo<T> {
|
|||
name = "Memo::try_with()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0.try_with(|t| f(t.as_ref().unwrap())).ok()
|
||||
// memo is stored as Option<T>, but will always have T available
|
||||
// after latest_value() called, so we can unwrap safely
|
||||
let f = move |maybe_value: &Option<T>| f(maybe_value.as_ref().unwrap());
|
||||
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
self.id.subscribe(runtime);
|
||||
self.id.try_with_no_subscription(runtime, f).ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalStream<T> for Memo<T> {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Memo::to_stream()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn to_stream(
|
||||
&self,
|
||||
cx: Scope,
|
||||
|
@ -351,14 +412,92 @@ impl<T: Clone> SignalStream<T> for Memo<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Memo<T>
|
||||
impl_get_fn_traits![Memo];
|
||||
|
||||
pub(crate) struct MemoState<T, F>
|
||||
where
|
||||
T: 'static,
|
||||
T: PartialEq + 'static,
|
||||
F: Fn(Option<&T>) -> T,
|
||||
{
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) fn subscribe(&self) {
|
||||
self.0.subscribe()
|
||||
pub f: F,
|
||||
pub t: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T, F> AnyComputation for MemoState<T, F>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
F: Fn(Option<&T>) -> T,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Memo::run()",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
|
||||
let (new_value, is_different) = {
|
||||
let value = value.borrow();
|
||||
let curr_value = value
|
||||
.downcast_ref::<Option<T>>()
|
||||
.expect("to downcast memo value");
|
||||
|
||||
// run the effect
|
||||
let new_value = (self.f)(curr_value.as_ref());
|
||||
let is_different = curr_value.as_ref() != Some(&new_value);
|
||||
(new_value, is_different)
|
||||
};
|
||||
if is_different {
|
||||
let mut value = value.borrow_mut();
|
||||
let curr_value = value
|
||||
.downcast_mut::<Option<T>>()
|
||||
.expect("to downcast memo value");
|
||||
*curr_value = Some(new_value);
|
||||
}
|
||||
|
||||
is_different
|
||||
}
|
||||
}
|
||||
|
||||
impl_get_fn_traits![Memo];
|
||||
#[track_caller]
|
||||
fn format_memo_warning(
|
||||
msg: &str,
|
||||
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<'static>,
|
||||
) -> String {
|
||||
let location = std::panic::Location::caller();
|
||||
|
||||
let defined_at_msg = {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!("signal created here: {defined_at}\n")
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
String::default()
|
||||
}
|
||||
};
|
||||
|
||||
format!("{msg}\n{defined_at_msg}warning happened here: {location}",)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<'static>,
|
||||
) -> ! {
|
||||
panic!(
|
||||
"{}",
|
||||
format_memo_warning(
|
||||
"Attempted to get a memo after it was disposed.",
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
28
leptos_reactive/src/node.rs
Normal file
28
leptos_reactive/src/node.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use crate::AnyComputation;
|
||||
use std::{any::Any, cell::RefCell, rc::Rc};
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a signal.
|
||||
pub struct NodeId;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ReactiveNode {
|
||||
pub value: Rc<RefCell<dyn Any>>,
|
||||
pub state: ReactiveNodeState,
|
||||
pub node_type: ReactiveNodeType,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ReactiveNodeType {
|
||||
Signal,
|
||||
Memo { f: Rc<dyn AnyComputation> },
|
||||
Effect { f: Rc<dyn AnyComputation> },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub(crate) enum ReactiveNodeState {
|
||||
Clean,
|
||||
Check,
|
||||
Dirty,
|
||||
}
|
|
@ -115,6 +115,7 @@ where
|
|||
|
||||
let (loading, set_loading) = create_signal(cx, false);
|
||||
|
||||
//crate::macros::debug_warn!("creating fetcher");
|
||||
let fetcher = Rc::new(move |s| {
|
||||
Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>
|
||||
});
|
||||
|
@ -139,6 +140,7 @@ where
|
|||
})
|
||||
.expect("tried to create a Resource in a Runtime that has been disposed.");
|
||||
|
||||
//crate::macros::debug_warn!("creating effect");
|
||||
create_isomorphic_effect(cx, {
|
||||
let r = Rc::clone(&r);
|
||||
move |_| {
|
||||
|
@ -222,7 +224,6 @@ where
|
|||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_local_resource_with_initial_value<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> S + 'static,
|
||||
|
@ -318,7 +319,7 @@ where
|
|||
r.set_loading.update(|n| *n = false);
|
||||
|
||||
// for reactivity
|
||||
r.source.subscribe();
|
||||
r.source.track();
|
||||
} else if context.pending_resources.remove(&id) {
|
||||
// We're still waiting for the resource, add a "resolver" closure so
|
||||
// that it will be set as soon as the server sends the serialized
|
||||
|
@ -356,7 +357,7 @@ where
|
|||
);
|
||||
|
||||
// for reactivity
|
||||
r.source.subscribe()
|
||||
r.source.track()
|
||||
} else {
|
||||
// Server didn't mark the resource as pending, so load it on the
|
||||
// client
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo,
|
||||
ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer,
|
||||
ScopeId, ScopeProperty, SerializableResource, SignalId, SignalUpdate,
|
||||
UnserializableResource, WriteSignal,
|
||||
hydration::SharedContext,
|
||||
node::{NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType},
|
||||
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
|
||||
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
|
||||
ScopeProperty, SerializableResource, StoredValueId, UnserializableResource,
|
||||
WriteSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
|
@ -33,6 +35,233 @@ cfg_if! {
|
|||
}
|
||||
}
|
||||
|
||||
// The data structure that owns all the signals, memos, effects,
|
||||
// and other data included in the reactive system.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Runtime {
|
||||
pub shared_context: RefCell<SharedContext>,
|
||||
pub observer: Cell<Option<NodeId>>,
|
||||
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
|
||||
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
|
||||
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_contexts:
|
||||
RefCell<SparseSecondaryMap<ScopeId, FxHashMap<TypeId, Box<dyn Any>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_cleanups:
|
||||
RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
|
||||
pub stored_values: RefCell<SlotMap<StoredValueId, Rc<RefCell<dyn Any>>>>,
|
||||
pub nodes: RefCell<SlotMap<NodeId, ReactiveNode>>,
|
||||
pub node_subscribers:
|
||||
RefCell<SecondaryMap<NodeId, RefCell<FxHashSet<NodeId>>>>,
|
||||
pub node_sources: RefCell<SecondaryMap<NodeId, RefCell<FxHashSet<NodeId>>>>,
|
||||
pub pending_effects: RefCell<Vec<NodeId>>,
|
||||
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
|
||||
}
|
||||
|
||||
// This core Runtime impl block handles all the work of marking and updating
|
||||
// the reactive graph.
|
||||
//
|
||||
// In terms of concept and algorithm, this reactive-system implementation
|
||||
// is significantly inspired by Reactively (https://github.com/modderme123/reactively)
|
||||
impl Runtime {
|
||||
pub(crate) fn update_if_necessary(&self, node_id: NodeId) {
|
||||
//crate::macros::debug_warn!("update_if_necessary {node_id:?}");
|
||||
if self.current_state(node_id) == ReactiveNodeState::Check {
|
||||
let sources = {
|
||||
let sources = self.node_sources.borrow();
|
||||
sources.get(node_id).map(|n| n.borrow().clone())
|
||||
};
|
||||
for source in sources.into_iter().flatten() {
|
||||
self.update_if_necessary(source);
|
||||
if self.current_state(node_id) == ReactiveNodeState::Dirty {
|
||||
// as soon as a single parent has marked us dirty, we can
|
||||
// stop checking them to avoid over-re-running
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're dirty at this point, update
|
||||
if self.current_state(node_id) == ReactiveNodeState::Dirty {
|
||||
self.update(node_id);
|
||||
}
|
||||
|
||||
// now we're clean
|
||||
self.mark_clean(node_id);
|
||||
}
|
||||
|
||||
pub(crate) fn update(&self, node_id: NodeId) {
|
||||
//crate::macros::debug_warn!("updating {node_id:?}");
|
||||
let node = {
|
||||
let nodes = self.nodes.borrow();
|
||||
nodes.get(node_id).cloned()
|
||||
};
|
||||
let subs = {
|
||||
let subs = self.node_subscribers.borrow();
|
||||
subs.get(node_id).cloned()
|
||||
};
|
||||
if let Some(node) = node {
|
||||
// memos and effects rerun
|
||||
// signals simply have their value
|
||||
let changed = match node.node_type {
|
||||
ReactiveNodeType::Signal => true,
|
||||
ReactiveNodeType::Memo { f }
|
||||
| ReactiveNodeType::Effect { f } => {
|
||||
// set this node as the observer
|
||||
self.with_observer(node_id, move || {
|
||||
// clean up sources of this memo/effect
|
||||
self.cleanup(node_id);
|
||||
|
||||
f.run(Rc::clone(&node.value))
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// mark children dirty
|
||||
if changed {
|
||||
if let Some(subs) = subs {
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
for sub_id in subs.borrow().iter() {
|
||||
if let Some(sub) = nodes.get_mut(*sub_id) {
|
||||
//crate::macros::debug_warn!(
|
||||
// "update is marking {sub_id:?} dirty"
|
||||
//);
|
||||
sub.state = ReactiveNodeState::Dirty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mark clean
|
||||
self.mark_clean(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup(&self, node_id: NodeId) {
|
||||
let sources = self.node_sources.borrow();
|
||||
if let Some(sources) = sources.get(node_id) {
|
||||
let subs = self.node_subscribers.borrow();
|
||||
for source in sources.borrow().iter() {
|
||||
if let Some(source) = subs.get(*source) {
|
||||
source.borrow_mut().remove(&node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_state(&self, node: NodeId) -> ReactiveNodeState {
|
||||
match self.nodes.borrow().get(node) {
|
||||
None => ReactiveNodeState::Clean,
|
||||
Some(node) => node.state,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_observer<T>(&self, observer: NodeId, f: impl FnOnce() -> T) -> T {
|
||||
let prev_observer = self.observer.take();
|
||||
self.observer.set(Some(observer));
|
||||
let v = f();
|
||||
self.observer.set(prev_observer);
|
||||
v
|
||||
}
|
||||
|
||||
fn mark_clean(&self, node: NodeId) {
|
||||
//crate::macros::debug_warn!("marking {node:?} clean");
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
if let Some(node) = nodes.get_mut(node) {
|
||||
node.state = ReactiveNodeState::Clean;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mark_dirty(&self, node: NodeId) {
|
||||
//crate::macros::debug_warn!("marking {node:?} dirty");
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
let mut pending_effects = self.pending_effects.borrow_mut();
|
||||
let subscribers = self.node_subscribers.borrow();
|
||||
let current_observer = self.observer.get();
|
||||
|
||||
// mark self dirty
|
||||
if let Some(mut current_node) = nodes.get_mut(node) {
|
||||
Runtime::mark(
|
||||
node,
|
||||
&mut current_node,
|
||||
ReactiveNodeState::Dirty,
|
||||
&mut *pending_effects,
|
||||
current_observer,
|
||||
);
|
||||
|
||||
// mark all children check
|
||||
// this can probably be done in a better way
|
||||
let mut descendants = Default::default();
|
||||
Runtime::gather_descendants(&subscribers, node, &mut descendants);
|
||||
for descendant in descendants {
|
||||
if let Some(mut node) = nodes.get_mut(descendant) {
|
||||
Runtime::mark(
|
||||
descendant,
|
||||
&mut node,
|
||||
ReactiveNodeState::Check,
|
||||
&mut pending_effects,
|
||||
current_observer,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mark(
|
||||
//nodes: &mut SlotMap<NodeId, ReactiveNode>,
|
||||
node_id: NodeId,
|
||||
node: &mut ReactiveNode,
|
||||
level: ReactiveNodeState,
|
||||
pending_effects: &mut Vec<NodeId>,
|
||||
current_observer: Option<NodeId>,
|
||||
) {
|
||||
//crate::macros::debug_warn!("marking {node_id:?} {level:?}");
|
||||
if level > node.state {
|
||||
node.state = level;
|
||||
}
|
||||
if matches!(node.node_type, ReactiveNodeType::Effect { .. })
|
||||
&& current_observer != Some(node_id)
|
||||
{
|
||||
//crate::macros::debug_warn!("pushing effect {node_id:?}");
|
||||
pending_effects.push(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn gather_descendants(
|
||||
subscribers: &SecondaryMap<NodeId, RefCell<FxHashSet<NodeId>>>,
|
||||
node: NodeId,
|
||||
descendants: &mut FxHashSet<NodeId>,
|
||||
) {
|
||||
if let Some(children) = subscribers.get(node) {
|
||||
for child in children.borrow().iter() {
|
||||
descendants.insert(*child);
|
||||
Runtime::gather_descendants(subscribers, *child, descendants);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_effects(runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
let effects = runtime.pending_effects.take();
|
||||
for effect_id in effects {
|
||||
runtime.update_if_necessary(effect_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Runtime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Runtime")
|
||||
.field("shared_context", &self.shared_context)
|
||||
.field("observer", &self.observer)
|
||||
.field("scopes", &self.scopes)
|
||||
.field("scope_parents", &self.scope_parents)
|
||||
.field("scope_children", &self.scope_children)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
/// Get the selected runtime from the thread-local set of runtimes. On the server,
|
||||
/// this will return the correct runtime. In the browser, there should only be one runtime.
|
||||
pub(crate) fn with_runtime<T>(
|
||||
|
@ -130,11 +359,15 @@ impl RuntimeId {
|
|||
pub(crate) fn create_concrete_signal(
|
||||
self,
|
||||
value: Rc<RefCell<dyn Any>>,
|
||||
) -> SignalId {
|
||||
with_runtime(self, |runtime| runtime.signals.borrow_mut().insert(value))
|
||||
.expect(
|
||||
"tried to create a signal in a runtime that has been disposed",
|
||||
)
|
||||
) -> NodeId {
|
||||
with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value,
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Signal,
|
||||
})
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
@ -178,7 +411,7 @@ impl RuntimeId {
|
|||
T: Any + 'static,
|
||||
{
|
||||
with_runtime(self, move |runtime| {
|
||||
let mut signals = runtime.signals.borrow_mut();
|
||||
let mut signals = runtime.nodes.borrow_mut();
|
||||
let properties = runtime.scopes.borrow();
|
||||
let mut properties = properties
|
||||
.get(cx.id)
|
||||
|
@ -191,7 +424,13 @@ impl RuntimeId {
|
|||
signals.reserve(size);
|
||||
properties.reserve(size);
|
||||
values
|
||||
.map(|value| signals.insert(Rc::new(RefCell::new(value))))
|
||||
.map(|value| {
|
||||
signals.insert(ReactiveNode {
|
||||
value: Rc::new(RefCell::new(value)),
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Signal,
|
||||
})
|
||||
})
|
||||
.map(|id| {
|
||||
properties.push(ScopeProperty::Signal(id));
|
||||
(
|
||||
|
@ -225,6 +464,10 @@ impl RuntimeId {
|
|||
let id = self.create_concrete_signal(
|
||||
Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>
|
||||
);
|
||||
//crate::macros::debug_warn!(
|
||||
// "created RwSignal {id:?} at {:?}",
|
||||
// std::panic::Location::caller()
|
||||
//);
|
||||
RwSignal {
|
||||
runtime: self,
|
||||
id,
|
||||
|
@ -237,10 +480,27 @@ impl RuntimeId {
|
|||
#[track_caller]
|
||||
pub(crate) fn create_concrete_effect(
|
||||
self,
|
||||
effect: Rc<dyn AnyEffect>,
|
||||
) -> EffectId {
|
||||
value: Rc<RefCell<dyn Any>>,
|
||||
effect: Rc<dyn AnyComputation>,
|
||||
) -> NodeId {
|
||||
with_runtime(self, |runtime| {
|
||||
runtime.effects.borrow_mut().insert(effect)
|
||||
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value: Rc::clone(&value),
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Effect {
|
||||
f: Rc::clone(&effect),
|
||||
},
|
||||
});
|
||||
|
||||
// run the effect for the first time
|
||||
let prev_observer = runtime.observer.take();
|
||||
runtime.observer.set(Some(id));
|
||||
|
||||
effect.run(value);
|
||||
|
||||
runtime.observer.set(prev_observer);
|
||||
|
||||
id
|
||||
})
|
||||
.expect("tried to create an effect in a runtime that has been disposed")
|
||||
}
|
||||
|
@ -249,7 +509,7 @@ impl RuntimeId {
|
|||
pub(crate) fn create_effect<T>(
|
||||
self,
|
||||
f: impl Fn(Option<T>) -> T + 'static,
|
||||
) -> EffectId
|
||||
) -> NodeId
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
|
@ -258,14 +518,13 @@ impl RuntimeId {
|
|||
|
||||
let effect = Effect {
|
||||
f,
|
||||
value: RefCell::new(None),
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
};
|
||||
|
||||
let id = self.create_concrete_effect(Rc::new(effect));
|
||||
id.run(self);
|
||||
id
|
||||
let value = Rc::new(RefCell::new(None::<T>));
|
||||
self.create_concrete_effect(value, Rc::new(effect))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
@ -279,63 +538,30 @@ impl RuntimeId {
|
|||
#[cfg(debug_assertions)]
|
||||
let defined_at = std::panic::Location::caller();
|
||||
|
||||
let (read, write) = self.create_signal(None);
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value: Rc::new(RefCell::new(None::<T>)),
|
||||
// memos are lazy, so are dirty when created
|
||||
// will be run the first time we ask for it
|
||||
state: ReactiveNodeState::Dirty,
|
||||
node_type: ReactiveNodeType::Memo {
|
||||
f: Rc::new(MemoState {
|
||||
f,
|
||||
t: PhantomData,
|
||||
defined_at,
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
.expect("tried to create a memo in a runtime that has been disposed");
|
||||
|
||||
self.create_effect(move |_| {
|
||||
let (new, changed) = read.with_no_subscription(|p| {
|
||||
let new = f(p.as_ref());
|
||||
let changed = Some(&new) != p.as_ref();
|
||||
(new, changed)
|
||||
});
|
||||
|
||||
if changed {
|
||||
write.update(|n| *n = Some(new));
|
||||
}
|
||||
});
|
||||
|
||||
Memo(
|
||||
read,
|
||||
Memo {
|
||||
runtime: self,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Runtime {
|
||||
pub shared_context: RefCell<SharedContext>,
|
||||
pub observer: Cell<Option<EffectId>>,
|
||||
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
|
||||
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
|
||||
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_contexts:
|
||||
RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_cleanups:
|
||||
RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
|
||||
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
|
||||
pub signal_subscribers:
|
||||
RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
|
||||
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
|
||||
pub effect_sources:
|
||||
RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
|
||||
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
|
||||
}
|
||||
|
||||
impl Debug for Runtime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Runtime")
|
||||
.field("shared_context", &self.shared_context)
|
||||
.field("observer", &self.observer)
|
||||
.field("scopes", &self.scopes)
|
||||
.field("scope_parents", &self.scope_parents)
|
||||
.field("scope_children", &self.scope_children)
|
||||
.field("signals", &self.signals)
|
||||
.field("signal_subscribers", &self.signal_subscribers)
|
||||
.field("effects", &self.effects.borrow().len())
|
||||
.field("effect_sources", &self.effect_sources)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
console_warn,
|
||||
node::NodeId,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
suspense::StreamChunk,
|
||||
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
|
||||
PinnedFuture, ResourceId, SuspenseContext,
|
||||
};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use std::{collections::HashMap, fmt};
|
||||
|
@ -228,17 +229,16 @@ impl Scope {
|
|||
match property {
|
||||
ScopeProperty::Signal(id) => {
|
||||
// remove the signal
|
||||
runtime.signals.borrow_mut().remove(id);
|
||||
runtime.nodes.borrow_mut().remove(id);
|
||||
let subs = runtime
|
||||
.signal_subscribers
|
||||
.node_subscribers
|
||||
.borrow_mut()
|
||||
.remove(id);
|
||||
|
||||
// each of the subs needs to remove the signal from its dependencies
|
||||
// so that it doesn't try to read the (now disposed) signal
|
||||
if let Some(subs) = subs {
|
||||
let source_map =
|
||||
runtime.effect_sources.borrow();
|
||||
let source_map = runtime.node_sources.borrow();
|
||||
for effect in subs.borrow().iter() {
|
||||
if let Some(effect_sources) =
|
||||
source_map.get(*effect)
|
||||
|
@ -249,8 +249,8 @@ impl Scope {
|
|||
}
|
||||
}
|
||||
ScopeProperty::Effect(id) => {
|
||||
runtime.effects.borrow_mut().remove(id);
|
||||
runtime.effect_sources.borrow_mut().remove(id);
|
||||
runtime.nodes.borrow_mut().remove(id);
|
||||
runtime.node_sources.borrow_mut().remove(id);
|
||||
}
|
||||
ScopeProperty::Resource(id) => {
|
||||
runtime.resources.borrow_mut().remove(id);
|
||||
|
@ -313,8 +313,8 @@ slotmap::new_key_type! {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ScopeProperty {
|
||||
Signal(SignalId),
|
||||
Effect(EffectId),
|
||||
Signal(NodeId),
|
||||
Effect(NodeId),
|
||||
Resource(ResourceId),
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
use crate::{
|
||||
console_warn, create_effect,
|
||||
macros::debug_warn,
|
||||
node::NodeId,
|
||||
on_cleanup,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
Runtime, Scope, ScopeProperty,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::Stream;
|
||||
use std::{fmt::Debug, marker::PhantomData, pin::Pin};
|
||||
use std::{fmt::Debug, marker::PhantomData, pin::Pin, rc::Rc};
|
||||
use thiserror::Error;
|
||||
|
||||
macro_rules! impl_get_fn_traits {
|
||||
|
@ -124,6 +125,11 @@ pub trait SignalWith<T> {
|
|||
/// the running effect to this signal. Returns [`Some`] if the signal is
|
||||
/// valid and the function ran, otherwise returns [`None`].
|
||||
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O>;
|
||||
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
fn track(&self) {
|
||||
_ = self.try_with(|_| {});
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait allows setting the value of a signal.
|
||||
|
@ -323,6 +329,7 @@ pub fn create_signal<T>(
|
|||
value: T,
|
||||
) -> (ReadSignal<T>, WriteSignal<T>) {
|
||||
let s = cx.runtime.create_signal(value);
|
||||
//crate::macros::debug_warn!("created signal {:?} at {}", s.0.id, std::panic::Location::caller());
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.0.id)));
|
||||
s
|
||||
}
|
||||
|
@ -469,7 +476,7 @@ where
|
|||
T: 'static,
|
||||
{
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
|
@ -727,11 +734,6 @@ where
|
|||
self.id.with_no_subscription(self.runtime, f)
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) fn subscribe(&self) {
|
||||
_ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
|
||||
}
|
||||
|
||||
/// Applies the function to the current Signal, if it exists, and subscribes
|
||||
/// the running effect.
|
||||
pub(crate) fn try_with<U>(
|
||||
|
@ -812,7 +814,7 @@ where
|
|||
T: 'static,
|
||||
{
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
|
@ -1069,6 +1071,7 @@ impl<T> Copy for WriteSignal<T> {}
|
|||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
|
||||
let s = cx.runtime.create_rw_signal(value);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.id)));
|
||||
|
@ -1124,7 +1127,7 @@ where
|
|||
T: 'static,
|
||||
{
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
|
@ -1722,12 +1725,6 @@ impl<T> RwSignal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
// Internals
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a signal.
|
||||
pub struct SignalId;
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum SignalError {
|
||||
#[error("tried to access a signal in a runtime that had been disposed")]
|
||||
|
@ -1738,20 +1735,20 @@ pub(crate) enum SignalError {
|
|||
Type(&'static str),
|
||||
}
|
||||
|
||||
impl SignalId {
|
||||
impl NodeId {
|
||||
pub(crate) fn subscribe(&self, runtime: &Runtime) {
|
||||
// add subscriber
|
||||
if let Some(observer) = runtime.observer.get() {
|
||||
// add this observer to the signal's dependencies (to allow notification)
|
||||
let mut subs = runtime.signal_subscribers.borrow_mut();
|
||||
// add this observer to this node's dependencies (to allow notification)
|
||||
let mut subs = runtime.node_subscribers.borrow_mut();
|
||||
if let Some(subs) = subs.entry(*self) {
|
||||
subs.or_default().borrow_mut().insert(observer);
|
||||
}
|
||||
|
||||
// add this signal to the effect's sources (to allow cleanup)
|
||||
let mut effect_sources = runtime.effect_sources.borrow_mut();
|
||||
if let Some(effect_sources) = effect_sources.entry(observer) {
|
||||
let sources = effect_sources.or_default();
|
||||
// add this node to the observer's sources (to allow cleanup)
|
||||
let mut sources = runtime.node_sources.borrow_mut();
|
||||
if let Some(sources) = sources.entry(observer) {
|
||||
let sources = sources.or_default();
|
||||
sources.borrow_mut().insert(*self);
|
||||
}
|
||||
}
|
||||
|
@ -1765,29 +1762,18 @@ impl SignalId {
|
|||
where
|
||||
T: 'static,
|
||||
{
|
||||
// get the value
|
||||
runtime.update_if_necessary(*self);
|
||||
let value = {
|
||||
let signals = runtime.signals.borrow();
|
||||
match signals.get(*self).cloned().ok_or(SignalError::Disposed) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => {
|
||||
debug_warn!("[Signal::try_with] {e}");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}?;
|
||||
let value = value.try_borrow().unwrap_or_else(|e| {
|
||||
debug_warn!(
|
||||
"Signal::try_with_no_subscription failed on Signal<{}>. It \
|
||||
seems you're trying to read the value of a signal within an \
|
||||
effect caused by updating the signal.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
panic!("{e}");
|
||||
});
|
||||
let nodes = runtime.nodes.borrow();
|
||||
let node = nodes.get(*self).ok_or(SignalError::Disposed)?;
|
||||
Rc::clone(&node.value)
|
||||
};
|
||||
|
||||
let value = value.borrow();
|
||||
let value = value
|
||||
.downcast_ref::<T>()
|
||||
.ok_or_else(|| SignalError::Type(std::any::type_name::<T>()))?;
|
||||
.ok_or_else(|| SignalError::Type(std::any::type_name::<T>()))
|
||||
.expect("to downcast signal type");
|
||||
Ok(f(value))
|
||||
}
|
||||
|
||||
|
@ -1815,7 +1801,7 @@ impl SignalId {
|
|||
with_runtime(runtime, |runtime| {
|
||||
self.try_with_no_subscription(runtime, f).unwrap()
|
||||
})
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
.expect("runtime to be alive")
|
||||
}
|
||||
|
||||
fn update_value<T, U>(
|
||||
|
@ -1828,8 +1814,8 @@ impl SignalId {
|
|||
{
|
||||
with_runtime(runtime, |runtime| {
|
||||
let value = {
|
||||
let signals = runtime.signals.borrow();
|
||||
signals.get(*self).cloned()
|
||||
let signals = runtime.nodes.borrow();
|
||||
signals.get(*self).map(|node| Rc::clone(&node.value))
|
||||
};
|
||||
if let Some(value) = value {
|
||||
let mut value = value.borrow_mut();
|
||||
|
@ -1867,27 +1853,40 @@ impl SignalId {
|
|||
T: 'static,
|
||||
{
|
||||
with_runtime(runtime_id, |runtime| {
|
||||
// update the value
|
||||
let updated = self.update_value(runtime_id, f);
|
||||
let value = {
|
||||
let signals = runtime.nodes.borrow();
|
||||
signals.get(*self).map(|node| Rc::clone(&node.value))
|
||||
};
|
||||
let updated = if let Some(value) = value {
|
||||
let mut value = value.borrow_mut();
|
||||
if let Some(value) = value.downcast_mut::<T>() {
|
||||
Some(f(value))
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Signal::update] failed when downcasting to \
|
||||
Signal<{}>",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a Signal<{}> \
|
||||
that has already been disposed of. This is probably \
|
||||
either a logic error in a component that creates and \
|
||||
disposes of scopes, or a Resource resolving after its \
|
||||
scope has been dropped without having been cleaned up.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
None
|
||||
};
|
||||
|
||||
// mark descendants dirty
|
||||
runtime.mark_dirty(*self);
|
||||
|
||||
// notify subscribers
|
||||
if updated.is_some() {
|
||||
let subs = {
|
||||
let subs = runtime.signal_subscribers.borrow();
|
||||
let subs = subs.get(*self);
|
||||
subs.map(|subs| subs.borrow().clone())
|
||||
};
|
||||
if let Some(subs) = subs {
|
||||
for sub in subs {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
effects.get(sub).cloned()
|
||||
};
|
||||
if let Some(effect) = effect {
|
||||
effect.run(sub, runtime_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Runtime::run_effects(runtime_id);
|
||||
};
|
||||
updated
|
||||
})
|
||||
|
|
|
@ -22,47 +22,51 @@ use crate::{
|
|||
/// # use leptos_reactive::*;
|
||||
/// # let (cx, disposer) = raw_scope_and_disposer(create_runtime());
|
||||
///
|
||||
/// // this could be serialized to and from localstorage with miniserde
|
||||
/// pub struct State {
|
||||
/// token: String,
|
||||
/// dark_mode: bool,
|
||||
/// // some global state with independent fields
|
||||
/// #[derive(Default, Clone, Debug)]
|
||||
/// struct GlobalState {
|
||||
/// count: u32,
|
||||
/// name: String,
|
||||
/// }
|
||||
///
|
||||
/// let state = create_rw_signal(
|
||||
/// cx,
|
||||
/// State {
|
||||
/// token: "".into(),
|
||||
/// // this would cause flickering on reload,
|
||||
/// // use a cookie for the initial value in real projects
|
||||
/// dark_mode: false,
|
||||
/// },
|
||||
/// );
|
||||
/// let (token, set_token) = create_slice(
|
||||
/// let state = create_rw_signal(cx, GlobalState::default());
|
||||
///
|
||||
/// // `create_slice` lets us create a "lens" into the data
|
||||
/// let (count, set_count) = create_slice(
|
||||
/// cx,
|
||||
/// // we take a slice *from* `state`
|
||||
/// state,
|
||||
/// |state| state.token.clone(),
|
||||
/// |state, value| state.token = value,
|
||||
/// // our getter returns a "slice" of the data
|
||||
/// |state| state.count,
|
||||
/// // our setter describes how to mutate that slice, given a new value
|
||||
/// |state, n| state.count = n,
|
||||
/// );
|
||||
/// let (dark_mode, set_dark_mode) = create_slice(
|
||||
///
|
||||
/// // this slice is completely independent of the `count` slice
|
||||
/// // neither of them will cause the other to rerun
|
||||
/// let (name, set_name) = create_slice(
|
||||
/// cx,
|
||||
/// // we take a slice *from* `state`
|
||||
/// state,
|
||||
/// |state| state.dark_mode,
|
||||
/// |state, value| state.dark_mode = value,
|
||||
/// // our getter returns a "slice" of the data
|
||||
/// |state| state.name.clone(),
|
||||
/// // our setter describes how to mutate that slice, given a new value
|
||||
/// |state, n| state.name = n,
|
||||
/// );
|
||||
/// let count_token_updates = create_rw_signal(cx, 0);
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &0));
|
||||
/// create_isomorphic_effect(cx, move |_| {
|
||||
/// _ = token.with(|_| {});
|
||||
/// count_token_updates.update(|counter| *counter += 1)
|
||||
///
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // note: in the browser, use leptos::log! instead
|
||||
/// println!("name is {}", name());
|
||||
/// });
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &1));
|
||||
/// set_token.set("this is not a token!".into());
|
||||
/// // token was updated with the new token
|
||||
/// token.with(|token| assert_eq!(token, "this is not a token!"));
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &2));
|
||||
/// set_dark_mode.set(true);
|
||||
/// // since token didn't change, there was also no update emitted
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &2));
|
||||
/// create_effect(cx, move |_| {
|
||||
/// println!("count is {}", count());
|
||||
/// });
|
||||
///
|
||||
/// // setting count only causes count to log, not name
|
||||
/// set_count(42);
|
||||
///
|
||||
/// // setting name only causes name to log, not count
|
||||
/// set_name("Bob".into());
|
||||
/// ```
|
||||
pub fn create_slice<T, O>(
|
||||
cx: Scope,
|
||||
|
|
|
@ -10,7 +10,7 @@ where
|
|||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
|
||||
wasm_bindgen_futures::spawn_local(fut)
|
||||
}
|
||||
else if #[cfg(any(test, doctest))] {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
|
@ -23,8 +23,7 @@ cfg_if! {
|
|||
} else {
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
pub fn queue_microtask(task: impl FnOnce()) {
|
||||
pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
task();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_rw_signal, RwSignal, Scope, SignalGetUntracked, SignalSetUntracked,
|
||||
SignalUpdateUntracked, SignalWithUntracked,
|
||||
};
|
||||
use crate::{with_runtime, RuntimeId, Scope};
|
||||
use std::{cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a [StoredValue].
|
||||
pub(crate) struct StoredValueId;
|
||||
}
|
||||
|
||||
/// A **non-reactive** wrapper for any value, which can be created with [store_value].
|
||||
///
|
||||
|
@ -14,13 +17,22 @@ use crate::{
|
|||
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
|
||||
/// updating it does not notify anything else.
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct StoredValue<T>(RwSignal<T>)
|
||||
pub struct StoredValue<T>
|
||||
where
|
||||
T: 'static;
|
||||
T: 'static,
|
||||
{
|
||||
runtime: RuntimeId,
|
||||
id: StoredValueId,
|
||||
ty: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for StoredValue<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: self.ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,7 +101,7 @@ impl<T> StoredValue<T> {
|
|||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.get_untracked()
|
||||
self.try_get_value().expect("could not get stored value")
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::get`] but will not panic by default.
|
||||
|
@ -110,7 +122,7 @@ impl<T> StoredValue<T> {
|
|||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.try_get_untracked()
|
||||
self.try_with_value(T::clone)
|
||||
}
|
||||
|
||||
/// Applies a function to the current stored value.
|
||||
|
@ -163,7 +175,7 @@ impl<T> StoredValue<T> {
|
|||
// track the stored value. This method will also be removed in \
|
||||
// a future version of `leptos`"]
|
||||
pub fn with_value<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
self.0.with_untracked(f)
|
||||
self.try_with_value(f).expect("could not get stored value")
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
|
||||
|
@ -178,7 +190,15 @@ impl<T> StoredValue<T> {
|
|||
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
|
||||
/// the signal is still valid. [`None`] otherwise.
|
||||
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0.try_with_untracked(f)
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
let value = values.get(self.id)?;
|
||||
let value = value.borrow();
|
||||
let value = value.downcast_ref::<T>()?;
|
||||
Some(f(value))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Updates the stored value.
|
||||
|
@ -259,7 +279,8 @@ impl<T> StoredValue<T> {
|
|||
/// ```
|
||||
#[track_caller]
|
||||
pub fn update_value(&self, f: impl FnOnce(&mut T)) {
|
||||
self.0.update_untracked(f);
|
||||
self.try_update_value(f)
|
||||
.expect("could not set stored value");
|
||||
}
|
||||
|
||||
/// Updates the stored value.
|
||||
|
@ -277,7 +298,15 @@ impl<T> StoredValue<T> {
|
|||
/// Same as [`Self::update`], but returns [`Some(O)`] if the
|
||||
/// signal is still valid, [`None`] otherwise.
|
||||
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
self.0.try_update_untracked(f)
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
let value = values.get(self.id)?;
|
||||
let mut value = value.borrow_mut();
|
||||
let value = value.downcast_mut::<T>()?;
|
||||
Some(f(value))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Sets the stored value.
|
||||
|
@ -320,13 +349,26 @@ impl<T> StoredValue<T> {
|
|||
/// ```
|
||||
#[track_caller]
|
||||
pub fn set_value(&self, value: T) {
|
||||
self.0.set_untracked(value);
|
||||
self.try_set_value(value);
|
||||
}
|
||||
|
||||
/// Same as [`Self::set`], but returns [`None`] if the signal is
|
||||
/// still valid, [`Some(T)`] otherwise.
|
||||
pub fn try_set_value(&self, value: T) -> Option<T> {
|
||||
self.0.try_set_untracked(value)
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
let n = values.get(self.id);
|
||||
let mut n = n.map(|n| n.borrow_mut());
|
||||
let n = n.as_mut().and_then(|n| n.downcast_mut::<T>());
|
||||
if let Some(n) = n {
|
||||
*n = value;
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -369,7 +411,18 @@ pub fn store_value<T>(cx: Scope, value: T) -> StoredValue<T>
|
|||
where
|
||||
T: 'static,
|
||||
{
|
||||
StoredValue(create_rw_signal(cx, value))
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
runtime
|
||||
.stored_values
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
StoredValue {
|
||||
runtime: cx.runtime,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl_get_fn_traits!(StoredValue(get_value));
|
||||
|
|
|
@ -33,20 +33,20 @@ fn memo_with_computed_value() {
|
|||
#[test]
|
||||
fn nested_memos() {
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
let c = create_memo(cx, move |_| a() + b());
|
||||
let d = create_memo(cx, move |_| c() * 2);
|
||||
let e = create_memo(cx, move |_| d() + 1);
|
||||
let (a, set_a) = create_signal(cx, 0); // 1
|
||||
let (b, set_b) = create_signal(cx, 0); // 2
|
||||
let c = create_memo(cx, move |_| a() + b()); // 3
|
||||
let d = create_memo(cx, move |_| c() * 2); // 4
|
||||
let e = create_memo(cx, move |_| d() + 1); // 5
|
||||
assert_eq!(d(), 0);
|
||||
set_a(5);
|
||||
assert_eq!(c(), 5);
|
||||
assert_eq!(d(), 10);
|
||||
assert_eq!(e(), 11);
|
||||
assert_eq!(d(), 10);
|
||||
assert_eq!(c(), 5);
|
||||
set_b(1);
|
||||
assert_eq!(c(), 6);
|
||||
assert_eq!(d(), 12);
|
||||
assert_eq!(e(), 13);
|
||||
assert_eq!(d(), 12);
|
||||
assert_eq!(c(), 6);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
@ -73,7 +73,8 @@ fn memo_runs_only_when_inputs_change() {
|
|||
}
|
||||
});
|
||||
|
||||
assert_eq!(call_count.get(), 1);
|
||||
// initially the memo has not been called at all, because it's lazy
|
||||
assert_eq!(call_count.get(), 0);
|
||||
|
||||
// here we access the value a bunch of times
|
||||
assert_eq!(c(), 0);
|
||||
|
@ -92,3 +93,99 @@ fn memo_runs_only_when_inputs_change() {
|
|||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn diamond_problem() {
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (name, set_name) = create_signal(cx, "Greg Johnston".to_string());
|
||||
let first = create_memo(cx, move |_| {
|
||||
name().split_whitespace().next().unwrap().to_string()
|
||||
});
|
||||
let last = create_memo(cx, move |_| {
|
||||
name().split_whitespace().nth(1).unwrap().to_string()
|
||||
});
|
||||
|
||||
let combined_count = Rc::new(Cell::new(0));
|
||||
let combined = create_memo(cx, {
|
||||
let combined_count = Rc::clone(&combined_count);
|
||||
move |_| {
|
||||
combined_count.set(combined_count.get() + 1);
|
||||
format!("{} {}", first(), last())
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(first(), "Greg");
|
||||
assert_eq!(last(), "Johnston");
|
||||
|
||||
set_name("Will Smith".to_string());
|
||||
assert_eq!(first(), "Will");
|
||||
assert_eq!(last(), "Smith");
|
||||
assert_eq!(combined(), "Will Smith");
|
||||
// should not have run the memo logic twice, even
|
||||
// though both paths have been updated
|
||||
assert_eq!(combined_count.get(), 1);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn dynamic_dependencies() {
|
||||
use leptos_reactive::create_isomorphic_effect;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (first, set_first) = create_signal(cx, "Greg");
|
||||
let (last, set_last) = create_signal(cx, "Johnston");
|
||||
let (use_last, set_use_last) = create_signal(cx, true);
|
||||
let name = create_memo(cx, move |_| {
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
first().to_string()
|
||||
}
|
||||
});
|
||||
|
||||
let combined_count = Rc::new(Cell::new(0));
|
||||
|
||||
create_isomorphic_effect(cx, {
|
||||
let combined_count = Rc::clone(&combined_count);
|
||||
move |_| {
|
||||
_ = name();
|
||||
combined_count.set(combined_count.get() + 1);
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(combined_count.get(), 1);
|
||||
|
||||
set_first("Bob");
|
||||
assert_eq!(name(), "Bob Johnston");
|
||||
|
||||
assert_eq!(combined_count.get(), 2);
|
||||
|
||||
set_last("Thompson");
|
||||
|
||||
assert_eq!(combined_count.get(), 3);
|
||||
|
||||
set_use_last(false);
|
||||
|
||||
assert_eq!(name(), "Bob");
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
set_last("Jones");
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
set_last("Smith");
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
set_last("Stevens");
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
|
||||
set_use_last(true);
|
||||
assert_eq!(name(), "Bob Stevens");
|
||||
assert_eq!(combined_count.get(), 5);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
|
57
leptos_reactive/tests/slice.rs
Normal file
57
leptos_reactive/tests/slice.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
#[test]
|
||||
fn slice() {
|
||||
use leptos_reactive::*;
|
||||
let (cx, disposer) = raw_scope_and_disposer(create_runtime());
|
||||
// this could be serialized to and from localstorage with miniserde
|
||||
pub struct State {
|
||||
token: String,
|
||||
dark_mode: bool,
|
||||
}
|
||||
|
||||
let state = create_rw_signal(
|
||||
cx,
|
||||
State {
|
||||
token: "".into(),
|
||||
// this would cause flickering on reload,
|
||||
// use a cookie for the initial value in real projects
|
||||
dark_mode: false,
|
||||
},
|
||||
);
|
||||
|
||||
let (token, set_token) = create_slice(
|
||||
cx,
|
||||
state,
|
||||
|state| state.token.clone(),
|
||||
|state, value| state.token = value,
|
||||
);
|
||||
|
||||
let (_, set_dark_mode) = create_slice(
|
||||
cx,
|
||||
state,
|
||||
|state| state.dark_mode,
|
||||
|state, value| state.dark_mode = value,
|
||||
);
|
||||
|
||||
let count_token_updates = Rc::new(std::cell::Cell::new(0));
|
||||
|
||||
assert_eq!(count_token_updates.get(), 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
let count_token_updates = Rc::clone(&count_token_updates);
|
||||
move |_| {
|
||||
token.track();
|
||||
count_token_updates.set(count_token_updates.get() + 1);
|
||||
}
|
||||
});
|
||||
assert_eq!(count_token_updates.get(), 1);
|
||||
set_token.set("this is not a token!".into());
|
||||
// token was updated with the new token
|
||||
token.with(|token| assert_eq!(token, "this is not a token!"));
|
||||
assert_eq!(count_token_updates.get(), 2);
|
||||
set_dark_mode.set(true);
|
||||
// since token didn't change, there was also no update emitted
|
||||
assert_eq!(count_token_updates.get(), 2);
|
||||
|
||||
disposer.dispose();
|
||||
}
|
Loading…
Reference in a new issue