feat: new reactive system implementation (#637)

This commit is contained in:
Greg Johnston 2023-03-13 17:58:00 -04:00 committed by GitHub
parent 38daaf3b72
commit 817152ff39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1242 additions and 364 deletions

View file

@ -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"
]
]

View file

@ -2,6 +2,6 @@
extern crate test;
//mod reactive;
mod ssr;
mod todomvc;
mod reactive;
//mod ssr;
//mod todomvc;

View file

@ -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(|| {

View file

@ -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]

View file

@ -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"

View file

@ -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>);
});
}

View file

@ -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
}
}

View file

@ -75,6 +75,7 @@ mod context;
mod effect;
mod hydration;
mod memo;
mod node;
mod resource;
mod runtime;
mod scope;

View file

@ -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,
)
)
}

View 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,
}

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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),
}

View file

@ -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] Youre 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
})

View file

@ -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,

View file

@ -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))] {

View file

@ -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();
}
}

View file

@ -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));

View file

@ -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()
}

View 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();
}