Support for multiple, independent Runtimes on a single thread without leaking

This commit is contained in:
Greg Johnston 2022-11-21 21:11:03 -05:00
parent 7c79cb1b1f
commit 739e7db49d
14 changed files with 651 additions and 501 deletions

View file

@ -3,7 +3,7 @@ use std::{
collections::HashMap,
};
use crate::Scope;
use crate::{runtime::with_runtime, Scope};
/// 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).
@ -14,7 +14,7 @@ use crate::Scope;
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
///
/// // Note: this example doesnt use Leptoss DOM model or component structure,
/// // so it ends up being a little silly.
@ -48,9 +48,11 @@ where
T: Clone + 'static,
{
let id = value.type_id();
let mut contexts = cx.runtime.scope_contexts.borrow_mut();
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
context.insert(id, Box::new(value) as Box<dyn Any>);
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);
context.insert(id, Box::new(value) as Box<dyn Any>);
});
}
/// Extracts a context value of type `T` from the reactive system by traversing
@ -64,7 +66,7 @@ where
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
///
/// // Note: this example doesnt use Leptoss DOM model or component structure,
/// // so it ends up being a little silly.
@ -98,25 +100,26 @@ where
T: Clone + 'static,
{
let id = TypeId::of::<T>();
let local_value = {
let contexts = cx.runtime.scope_contexts.borrow();
let context = contexts.get(cx.id);
context
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
.cloned()
};
match local_value {
Some(val) => Some(val),
None => cx
.runtime
.scope_parents
.borrow()
.get(cx.id)
.and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
}
with_runtime(cx.runtime, |runtime| {
let local_value = {
let contexts = runtime.scope_contexts.borrow();
let context = contexts.get(cx.id);
context
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
.cloned()
};
match local_value {
Some(val) => Some(val),
None => runtime
.scope_parents
.borrow()
.get(cx.id)
.and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
}
})
}

View file

@ -1,3 +1,4 @@
use crate::runtime::{with_runtime, RuntimeId};
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::cell::RefCell;
@ -22,7 +23,7 @@ use std::fmt::Debug;
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
///
@ -66,7 +67,7 @@ where
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
///
@ -118,7 +119,7 @@ where
}
pub(crate) trait AnyEffect {
fn run(&self, id: EffectId, runtime: &Runtime);
fn run(&self, id: EffectId, runtime: RuntimeId);
}
impl<T, F> AnyEffect for Effect<T, F>
@ -126,35 +127,39 @@ where
T: 'static,
F: Fn(Option<T>) -> T,
{
fn run(&self, id: EffectId, runtime: &Runtime) {
// clear previous dependencies
id.cleanup(runtime);
fn run(&self, id: EffectId, runtime: RuntimeId) {
with_runtime(runtime, |runtime| {
// clear previous dependencies
id.cleanup(runtime);
// set this as the current observer
let prev_observer = runtime.observer.take();
runtime.observer.set(Some(id));
// set this as the current observer
let prev_observer = runtime.observer.take();
runtime.observer.set(Some(id));
// run the effect
let value = self.value.take();
let new_value = (self.f)(value);
*self.value.borrow_mut() = Some(new_value);
// run the effect
let value = self.value.take();
let new_value = (self.f)(value);
*self.value.borrow_mut() = Some(new_value);
// restore the previous observer
runtime.observer.set(prev_observer);
// restore the previous observer
runtime.observer.set(prev_observer);
})
}
}
impl EffectId {
pub(crate) fn run<T>(&self, runtime: &Runtime) {
let effect = {
let effects = runtime.effects.borrow();
effects.get(*self).cloned()
};
if let Some(effect) = effect {
effect.run(*self, runtime);
} 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.")
}
pub(crate) fn run<T>(&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.")
}
})
}
pub(crate) fn cleanup(&self, runtime: &Runtime) {

View file

@ -37,7 +37,7 @@
//! // creates a new reactive Scope
//! // this is omitted from most of the examples in the docs
//! // you usually won't need to call it yourself
//! create_scope(|cx| {
//! create_scope(create_runtime(), |cx| {
//! // a signal: returns a (getter, setter) pair
//! let (count, set_count) = create_signal(cx, 0);
//!
@ -85,6 +85,7 @@ pub use effect::*;
pub use memo::*;
pub use resource::*;
use runtime::*;
pub use runtime::{create_runtime, RuntimeId};
pub use scope::*;
pub use selector::*;
pub use serialization::*;

View file

@ -20,7 +20,7 @@ use std::fmt::Debug;
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, 0);
///
/// // 🆗 we could create a derived signal with a simple function
@ -80,7 +80,7 @@ where
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, 0);
///
/// // 🆗 we could create a derived signal with a simple function
@ -155,7 +155,7 @@ where
/// the running effect to the memo.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
/// let double_count = create_memo(cx, move |_| count() * 2);
///
@ -178,7 +178,7 @@ where
/// the running effect to this memo.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = create_memo(cx, move |_| name().to_uppercase());
///

View file

@ -11,8 +11,10 @@ use std::{
use crate::{
create_effect, create_isomorphic_effect, create_memo, create_signal, queue_microtask,
runtime::Runtime, serialization::Serializable, spawn::spawn_local, use_context, Memo,
ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
runtime::{with_runtime, RuntimeId},
serialization::Serializable,
spawn::spawn_local,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
};
/// Creates [Resource](crate::Resource), which is a signal that reflects the
@ -31,7 +33,7 @@ use crate::{
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// // any old async function; maybe this is calling a REST API or something
/// async fn fetch_cat_picture_urls(how_many: i32) -> Vec<String> {
/// // pretend we're fetching cat pics
@ -121,7 +123,9 @@ where
suspense_contexts: Default::default(),
});
let id = cx.runtime.create_serializable_resource(Rc::clone(&r));
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_serializable_resource(Rc::clone(&r))
});
create_isomorphic_effect(cx, {
let r = Rc::clone(&r);
@ -153,7 +157,7 @@ where
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// #[derive(Debug, Clone)] // doesn't implement Serialize, Deserialize
/// struct ComplicatedUnserializableStruct {
/// // something here that can't be serialized
@ -223,7 +227,9 @@ where
suspense_contexts: Default::default(),
});
let id = cx.runtime.create_unserializable_resource(Rc::clone(&r));
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_unserializable_resource(Rc::clone(&r))
});
create_effect(cx, {
let r = Rc::clone(&r);
@ -259,61 +265,63 @@ where
{
use wasm_bindgen::{JsCast, UnwrapThrowExt};
if let Some(ref mut context) = *cx.runtime.shared_context.borrow_mut() {
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
with_runtime(cx.runtime, |runtime| {
if let Some(ref mut context) = *runtime.shared_context.borrow_mut() {
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
r.set_value.update(|n| *n = Some(res));
r.set_loading.update(|n| *n = false);
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
r.set_value.update(|n| *n = Some(res));
r.set_loading.update(|n| *n = false);
// for reactivity
r.source.subscribe();
} 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
// value
r.set_loading.update(|n| *n = true);
// for reactivity
r.source.subscribe();
} 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
// value
r.set_loading.update(|n| *n = true);
let resolve = {
let resolved = r.resolved.clone();
let set_value = r.set_value;
let set_loading = r.set_loading;
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
};
let resolve =
wasm_bindgen::closure::Closure::wrap(Box::new(resolve) as Box<dyn Fn(String)>);
let resource_resolvers = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
)
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
_ = js_sys::Reflect::set(
&resource_resolvers,
&wasm_bindgen::JsValue::from_str(&id),
resolve.as_ref().unchecked_ref(),
);
let resolve = {
let resolved = r.resolved.clone();
let set_value = r.set_value;
let set_loading = r.set_loading;
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
};
let resolve =
wasm_bindgen::closure::Closure::wrap(Box::new(resolve) as Box<dyn Fn(String)>);
let resource_resolvers = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
)
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
_ = js_sys::Reflect::set(
&resource_resolvers,
&wasm_bindgen::JsValue::from_str(&id),
resolve.as_ref().unchecked_ref(),
);
// for reactivity
r.source.subscribe()
// for reactivity
r.source.subscribe()
} else {
// Server didn't mark the resource as pending, so load it on the
// client
r.load(false);
}
} else {
// Server didn't mark the resource as pending, so load it on the
// client
r.load(false);
r.load(false)
}
} else {
r.load(false)
}
})
}
impl<S, T> Resource<S, T>
@ -331,8 +339,9 @@ where
where
T: Clone,
{
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.read())
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.read())
})
}
/// Applies a function to the current value of the resource, and subscribes
@ -343,20 +352,23 @@ where
/// If you want to get the value by cloning it, you can use
/// [Resource::read].
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.with(f))
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.with(f))
})
}
/// Returns a signal that indicates whether the resource is currently loading.
pub fn loading(&self) -> ReadSignal<bool> {
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.loading)
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.loading)
})
}
/// Re-runs the async function with the current source data.
pub fn refetch(&self) {
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.refetch())
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.refetch())
});
}
/// Returns a [std::future::Future] that will resolve when the resource has loaded,
@ -366,11 +378,12 @@ where
where
T: Serializable,
{
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| {
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.to_serialization_resolver(self.id)
})
.await
})
.await
}
}
@ -390,7 +403,7 @@ where
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// // any old async function; maybe this is calling a REST API or something
/// async fn fetch_cat_picture_urls(how_many: i32) -> Vec<String> {
/// // pretend we're fetching cat pics
@ -423,7 +436,7 @@ where
S: Debug + 'static,
T: Debug + 'static,
{
runtime: &'static Runtime,
runtime: RuntimeId,
pub(crate) id: ResourceId,
pub(crate) source_ty: PhantomData<S>,
pub(crate) out_ty: PhantomData<T>,

View file

@ -3,6 +3,7 @@ use crate::{
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
ScopeProperty, SignalId, WriteSignal,
};
use cfg_if::cfg_if;
use futures::stream::FuturesUnordered;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
@ -18,6 +19,185 @@ use std::{
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
thread_local! {
pub(crate) static RUNTIME: Runtime = Default::default();
}
} else {
thread_local! {
pub(crate) static RUNTIMES: RefCell<SlotMap<RuntimeId, Runtime>> = Default::default();
}
}
}
/// 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>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> T {
// in the browser, everything should exist under one runtime
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
_ = id;
RUNTIME.with(|runtime| f(runtime))
} else {
RUNTIMES.with(|runtimes| {
let runtimes = runtimes.borrow();
let runtime = runtimes
.get(id)
.expect("Tried to access a Runtime that no longer exists.");
f(runtime)
})
}
}
}
#[doc(hidden)]
#[must_use = "Runtime will leak memory if Runtime::dispose() is never called."]
/// Creates a new reactive [Runtime]. This should almost always be handled by the framework.
pub fn create_runtime() -> RuntimeId {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
Default::default()
} else {
RUNTIMES.with(|runtimes| runtimes.borrow_mut().insert(Runtime::new()))
}
}
}
slotmap::new_key_type! {
/// Unique ID assigned to a [Runtime](crate::Runtime).
pub struct RuntimeId;
}
impl RuntimeId {
/// Removes the runtime, disposing all its child [Scope](crate::Scope)s.
pub fn dispose(self) {
cfg_if! {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self));
if let Some(runtime) = runtime {
for (scope_id, _) in runtime.scopes.borrow().iter() {
let scope = Scope {
runtime: self,
id: scope_id,
};
scope.dispose();
}
}
}
}
}
pub(crate) fn raw_scope_and_disposer(self) -> (Scope, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
let scope = Scope { runtime: self, id };
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
})
}
pub(crate) fn run_scope_undisposed<T>(
self,
f: impl FnOnce(Scope) -> T,
parent: Option<Scope>,
) -> (T, ScopeId, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
if let Some(parent) = parent {
runtime.scope_parents.borrow_mut().insert(id, parent.id);
}
let scope = Scope { runtime: self, id };
let val = f(scope);
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(val, id, disposer)
})
}
pub(crate) fn run_scope<T>(self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
let (ret, _, disposer) = self.run_scope_undisposed(f, parent);
disposer.dispose();
ret
}
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
where
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
});
(
ReadSignal {
runtime: self,
id,
ty: PhantomData,
},
WriteSignal {
runtime: self,
id,
ty: PhantomData,
},
)
}
pub(crate) fn create_rw_signal<T>(self, value: T) -> RwSignal<T>
where
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
});
RwSignal {
runtime: self,
id,
ty: PhantomData,
}
}
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
where
T: Any + 'static,
{
with_runtime(self, |runtime| {
let effect = Effect {
f,
value: RefCell::new(None),
};
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
id.run::<T>(self);
id
})
}
pub(crate) fn create_memo<T>(self, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Any + 'static,
{
let (read, write) = self.create_signal(None);
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)
}
}
#[derive(Default)]
pub(crate) struct Runtime {
pub shared_context: RefCell<Option<SharedContext>>,
@ -57,105 +237,6 @@ impl Runtime {
Self::default()
}
pub fn raw_scope_and_disposer(&'static self) -> (Scope, ScopeDisposer) {
let id = { self.scopes.borrow_mut().insert(Default::default()) };
let scope = Scope { runtime: self, id };
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
}
pub fn run_scope_undisposed<T>(
&'static self,
f: impl FnOnce(Scope) -> T,
parent: Option<Scope>,
) -> (T, ScopeId, ScopeDisposer) {
let id = { self.scopes.borrow_mut().insert(Default::default()) };
if let Some(parent) = parent {
self.scope_parents.borrow_mut().insert(id, parent.id);
}
let scope = Scope { runtime: self, id };
let val = f(scope);
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(val, id, disposer)
}
pub fn run_scope<T>(&'static self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
let (ret, _, disposer) = self.run_scope_undisposed(f, parent);
disposer.dispose();
ret
}
pub(crate) fn create_signal<T>(&'static self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
where
T: Any + 'static,
{
let id = self
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)));
(
ReadSignal {
runtime: self,
id,
ty: PhantomData,
},
WriteSignal {
runtime: self,
id,
ty: PhantomData,
},
)
}
pub(crate) fn create_rw_signal<T>(&'static self, value: T) -> RwSignal<T>
where
T: Any + 'static,
{
let id = self
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)));
RwSignal {
runtime: self,
id,
ty: PhantomData,
}
}
pub(crate) fn create_effect<T>(&'static self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
where
T: Any + 'static,
{
let effect = Effect {
f,
value: RefCell::new(None),
};
let id = { self.effects.borrow_mut().insert(Rc::new(effect)) };
id.run::<T>(self);
id
}
pub(crate) fn create_memo<T>(&'static self, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Any + 'static,
{
let (read, write) = self.create_signal(None);
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)
}
pub(crate) fn create_unserializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,

View file

@ -1,52 +1,58 @@
use cfg_if::cfg_if;
use crate::{hydration::SharedContext, EffectId, ResourceId, Runtime, SignalId};
use crate::runtime::{with_runtime, RuntimeId};
use crate::{hydration::SharedContext, EffectId, ResourceId, SignalId};
use crate::{PinnedFuture, SuspenseContext};
use futures::stream::FuturesUnordered;
use std::collections::HashMap;
use std::fmt::Debug;
use std::{future::Future, pin::Pin};
#[doc(hidden)]
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a new reactive system and root reactive scope and runs the function within it.
///
/// This should usually only be used once, at the root of an application, because its reactive
/// values will not have access to values created under another `create_scope`.
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
// TODO leak
let runtime = Box::leak(Box::new(Runtime::new()));
///
/// You usually don't need to call this manually.
pub fn create_scope(runtime: RuntimeId, f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
runtime.run_scope_undisposed(f, None).2
}
#[doc(hidden)]
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a new reactive system and root reactive scope, and returns them.
///
/// This should usually only be used once, at the root of an application, because its reactive
/// values will not have access to values created under another `create_scope`.
pub fn raw_scope_and_disposer() -> (Scope, ScopeDisposer) {
// TODO leak
let runtime = Box::leak(Box::new(Runtime::new()));
///
/// You usually don't need to call this manually.
pub fn raw_scope_and_disposer(runtime: RuntimeId) -> (Scope, ScopeDisposer) {
runtime.raw_scope_and_disposer()
}
#[doc(hidden)]
/// Creates a temporary scope, runs the given function, disposes of the scope,
/// and returns the value returned from the function. This is very useful for short-lived
/// applications like SSR, where actual reactivity is not required beyond the end
/// of the synchronous operation.
pub fn run_scope<T>(f: impl FnOnce(Scope) -> T + 'static) -> T {
// TODO this leaks the runtime — should unsafely upgrade the lifetime, and then drop it after the scope is run
let runtime = Box::leak(Box::new(Runtime::new()));
///
/// You usually don't need to call this manually.
pub fn run_scope<T>(runtime: RuntimeId, f: impl FnOnce(Scope) -> T + 'static) -> T {
runtime.run_scope(f, None)
}
#[doc(hidden)]
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a temporary scope and run the given function without disposing of the scope.
/// If you do not dispose of the scope on your own, memory will leak.
///
/// You usually don't need to call this manually.
pub fn run_scope_undisposed<T>(
runtime: RuntimeId,
f: impl FnOnce(Scope) -> T + 'static,
) -> (T, ScopeId, ScopeDisposer) {
// TODO this leaks the runtime — should unsafely upgrade the lifetime, and then drop it after the scope is run
let runtime = Box::leak(Box::new(Runtime::new()));
runtime.run_scope_undisposed(f, None)
}
@ -67,7 +73,7 @@ pub fn run_scope_undisposed<T>(
/// is [Copy] and `'static` this does not add much overhead or lifetime complexity.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Scope {
pub(crate) runtime: &'static Runtime,
pub(crate) runtime: RuntimeId,
pub(crate) id: ScopeId,
}
@ -87,13 +93,15 @@ impl Scope {
/// has navigated away from the route.)
pub fn child_scope(self, f: impl FnOnce(Scope)) -> ScopeDisposer {
let (_, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
let mut children = self.runtime.scope_children.borrow_mut();
children
.entry(self.id)
.expect("trying to add a child to a Scope that has already been disposed")
.or_default()
.push(child_id);
disposer
with_runtime(self.runtime, |runtime| {
let mut children = runtime.scope_children.borrow_mut();
children
.entry(self.id)
.expect("trying to add a child to a Scope that has already been disposed")
.or_default()
.push(child_id);
disposer
})
}
/// Suspends reactive tracking while running the given function.
@ -102,7 +110,7 @@ impl Scope {
///
/// ```
/// # use leptos_reactive::*;
/// # run_scope(|cx| {
/// # run_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 |_| {
@ -122,10 +130,12 @@ impl Scope {
/// # });
/// ```
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
let prev_observer = self.runtime.observer.take();
let untracked_result = f();
self.runtime.observer.set(prev_observer);
untracked_result
with_runtime(self.runtime, |runtime| {
let prev_observer = runtime.observer.take();
let untracked_result = f();
runtime.observer.set(prev_observer);
untracked_result
})
}
}
@ -133,71 +143,75 @@ impl Scope {
impl Scope {
pub(crate) fn dispose(self) {
// dispose of all child scopes
let children = {
let mut children = self.runtime.scope_children.borrow_mut();
children.remove(self.id)
};
with_runtime(self.runtime, |runtime| {
// dispose of all child scopes
let children = {
let mut children = runtime.scope_children.borrow_mut();
children.remove(self.id)
};
if let Some(children) = children {
for id in children {
Scope {
runtime: self.runtime,
id,
if let Some(children) = children {
for id in children {
Scope {
runtime: self.runtime,
id,
}
.dispose();
}
.dispose();
}
}
// run cleanups
if let Some(cleanups) = self.runtime.scope_cleanups.borrow_mut().remove(self.id) {
for cleanup in cleanups {
cleanup();
// run cleanups
if let Some(cleanups) = runtime.scope_cleanups.borrow_mut().remove(self.id) {
for cleanup in cleanups {
cleanup();
}
}
}
// remove everything we own and run cleanups
let owned = {
let owned = self.runtime.scopes.borrow_mut().remove(self.id);
owned.map(|owned| owned.take())
};
if let Some(owned) = owned {
for property in owned {
match property {
ScopeProperty::Signal(id) => {
// remove the signal
self.runtime.signals.borrow_mut().remove(id);
let subs = self.runtime.signal_subscribers.borrow_mut().remove(id);
// remove everything we own and run cleanups
let owned = {
let owned = runtime.scopes.borrow_mut().remove(self.id);
owned.map(|owned| owned.take())
};
if let Some(owned) = owned {
for property in owned {
match property {
ScopeProperty::Signal(id) => {
// remove the signal
runtime.signals.borrow_mut().remove(id);
let subs = runtime.signal_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 = self.runtime.effect_sources.borrow();
for effect in subs.borrow().iter() {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.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();
for effect in subs.borrow().iter() {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&id);
}
}
}
}
}
ScopeProperty::Effect(id) => {
self.runtime.effects.borrow_mut().remove(id);
self.runtime.effect_sources.borrow_mut().remove(id);
}
ScopeProperty::Resource(id) => {
self.runtime.resources.borrow_mut().remove(id);
ScopeProperty::Effect(id) => {
runtime.effects.borrow_mut().remove(id);
runtime.effect_sources.borrow_mut().remove(id);
}
ScopeProperty::Resource(id) => {
runtime.resources.borrow_mut().remove(id);
}
}
}
}
}
})
}
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
let scopes = self.runtime.scopes.borrow();
let scope = scopes
.get(self.id)
.expect("tried to add property to a scope that has been disposed");
f(&mut scope.borrow_mut());
with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
let scope = scopes
.get(self.id)
.expect("tried to add property to a scope that has been disposed");
f(&mut scope.borrow_mut());
})
}
}
@ -206,12 +220,14 @@ impl Scope {
/// It runs after child scopes have been disposed, but before signals, effects, and resources
/// are invalidated.
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
let mut cleanups = cx.runtime.scope_cleanups.borrow_mut();
let cleanups = cleanups
.entry(cx.id)
.expect("trying to clean up a Scope that has already been disposed")
.or_insert_with(Default::default);
cleanups.push(Box::new(cleanup_fn));
with_runtime(cx.runtime, |runtime| {
let mut cleanups = runtime.scope_cleanups.borrow_mut();
let cleanups = cleanups
.entry(cx.id)
.expect("trying to clean up a Scope that has already been disposed")
.or_insert_with(Default::default);
cleanups.push(Box::new(cleanup_fn));
})
}
slotmap::new_key_type! {
@ -253,17 +269,23 @@ impl Scope {
if #[cfg(any(feature = "hydrate", doc))] {
/// `hydrate` only: Whether we're currently hydrating the page.
pub fn is_hydrating(&self) -> bool {
self.runtime.shared_context.borrow().is_some()
with_runtime(self.runtime, |runtime| {
runtime.shared_context.borrow().is_some()
})
}
/// `hydrate` only: Begins the hydration process.
pub fn start_hydration(&self, element: &web_sys::Element) {
self.runtime.start_hydration(element);
with_runtime(self.runtime, |runtime| {
runtime.start_hydration(element);
})
}
/// `hydrate` only: Ends the hydration process.
pub fn end_hydration(&self) {
self.runtime.end_hydration();
with_runtime(self.runtime, |runtime| {
runtime.end_hydration();
})
}
/// `hydrate` only: Gets the next element in the hydration queue, either from the
@ -283,27 +305,29 @@ impl Scope {
t
};
if let Some(ref mut shared_context) = &mut *self.runtime.shared_context.borrow_mut() {
if shared_context.context.is_some() {
let key = shared_context.next_hydration_key();
let node = shared_context.registry.remove(&key);
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = &mut *runtime.shared_context.borrow_mut() {
if shared_context.context.is_some() {
let key = shared_context.next_hydration_key();
let node = shared_context.registry.remove(&key);
//log::debug!("(hy) searching for {key}");
//log::debug!("(hy) searching for {key}");
if let Some(node) = node {
//log::debug!("(hy) found {key}");
shared_context.completed.push(node.clone());
node
if let Some(node) = node {
//log::debug!("(hy) found {key}");
shared_context.completed.push(node.clone());
node
} else {
//log::debug!("(hy) did NOT find {key}");
cloned_template(template)
}
} else {
//log::debug!("(hy) did NOT find {key}");
cloned_template(template)
}
} else {
cloned_template(template)
}
} else {
cloned_template(template)
}
})
}
}
}
@ -317,102 +341,108 @@ impl Scope {
let mut current = Vec::new();
let mut start = start.clone();
if self
.runtime
.shared_context
.borrow()
.as_ref()
.map(|sc| sc.context.as_ref())
.is_some()
{
while let Some(curr) = end {
start = curr.clone();
if curr.node_type() == 8 {
// COMMENT
let v = curr.node_value();
if v == Some("#".to_string()) {
count += 1;
} else if v == Some("/".to_string()) {
count -= 1;
if count == 0 {
current.push(curr.clone());
return (curr, current);
with_runtime(self.runtime, |runtime| {
if runtime
.shared_context
.borrow()
.as_ref()
.map(|sc| sc.context.as_ref())
.is_some()
{
while let Some(curr) = end {
start = curr.clone();
if curr.node_type() == 8 {
// COMMENT
let v = curr.node_value();
if v == Some("#".to_string()) {
count += 1;
} else if v == Some("/".to_string()) {
count -= 1;
if count == 0 {
current.push(curr.clone());
return (curr, current);
}
}
}
current.push(curr.clone());
end = curr.next_sibling();
}
current.push(curr.clone());
end = curr.next_sibling();
}
}
(start, current)
(start, current)
})
}
/// On either the server side or the browser side, generates the next key in the hydration process.
pub fn next_hydration_key(&self) -> String {
let mut sc = self.runtime.shared_context.borrow_mut();
if let Some(ref mut sc) = *sc {
sc.next_hydration_key()
} else {
let mut new_sc = SharedContext::default();
let id = new_sc.next_hydration_key();
*sc = Some(new_sc);
id
}
with_runtime(self.runtime, |runtime| {
let mut sc = runtime.shared_context.borrow_mut();
if let Some(ref mut sc) = *sc {
sc.next_hydration_key()
} else {
let mut new_sc = SharedContext::default();
let id = new_sc.next_hydration_key();
*sc = Some(new_sc);
id
}
})
}
/// Runs the given function with the next hydration context.
pub fn with_next_context<T>(&self, f: impl FnOnce() -> T) -> T {
if self
.runtime
.shared_context
.borrow()
.as_ref()
.and_then(|sc| sc.context.as_ref())
.is_some()
{
let c = {
if let Some(ref mut sc) = *self.runtime.shared_context.borrow_mut() {
if let Some(ref mut context) = sc.context {
let next = context.next_hydration_context();
Some(std::mem::replace(context, next))
with_runtime(self.runtime, |runtime| {
if runtime
.shared_context
.borrow()
.as_ref()
.and_then(|sc| sc.context.as_ref())
.is_some()
{
let c = {
if let Some(ref mut sc) = *runtime.shared_context.borrow_mut() {
if let Some(ref mut context) = sc.context {
let next = context.next_hydration_context();
Some(std::mem::replace(context, next))
} else {
None
}
} else {
None
}
} else {
None
};
let res = self.untrack(f);
if let Some(ref mut sc) = *runtime.shared_context.borrow_mut() {
sc.context = c;
}
};
let res = self.untrack(f);
if let Some(ref mut sc) = *self.runtime.shared_context.borrow_mut() {
sc.context = c;
res
} else {
self.untrack(f)
}
res
} else {
self.untrack(f)
}
})
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn all_resources(&self) -> Vec<ResourceId> {
self.runtime.all_resources()
with_runtime(self.runtime, |runtime| runtime.all_resources())
}
/// The current key for an HTML fragment created by server-rendering a `<Suspense/>` component.
pub fn current_fragment_key(&self) -> String {
self.runtime
.shared_context
.borrow()
.as_ref()
.map(|context| context.current_fragment_key())
.unwrap_or_else(|| String::from("0f"))
with_runtime(self.runtime, |runtime| {
runtime
.shared_context
.borrow()
.as_ref()
.map(|context| context.current_fragment_key())
.unwrap_or_else(|| String::from("0f"))
})
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn serialization_resolvers(&self) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
self.runtime.serialization_resolvers()
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers())
}
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
@ -426,33 +456,37 @@ impl Scope {
use crate::create_isomorphic_effect;
use futures::StreamExt;
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = *runtime.shared_context.borrow_mut() {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.unbounded_send(());
}
});
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.unbounded_send(());
}
});
shared_context.pending_fragments.insert(
key.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
}),
);
}
shared_context.pending_fragments.insert(
key.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
}),
);
}
})
}
/// The set of all HTML fragments current pending, by their keys (see [Self::current_fragment_key]).
pub fn pending_fragments(&self) -> HashMap<String, Pin<Box<dyn Future<Output = String>>>> {
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
std::mem::take(&mut shared_context.pending_fragments)
} else {
HashMap::new()
}
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = *runtime.shared_context.borrow_mut() {
std::mem::take(&mut shared_context.pending_fragments)
} else {
HashMap::new()
}
})
}
}

View file

@ -11,10 +11,10 @@ use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSig
/// because it reduces them from `O(n)` to `O(1)`.
///
/// ```
/// # use leptos_reactive::{create_isomorphic_effect, create_scope, create_selector, create_signal};
/// # use leptos_reactive::*;
/// # use std::rc::Rc;
/// # use std::cell::RefCell;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let is_selected = create_selector(cx, a);
/// let total_notifications = Rc::new(RefCell::new(0));

View file

@ -1,6 +1,7 @@
use crate::{
debug_warn, spawn_local, Runtime, Scope, ScopeProperty, UntrackedGettableSignal,
UntrackedSettableSignal,
debug_warn,
runtime::{with_runtime, RuntimeId},
spawn_local, Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
};
use futures::Stream;
use std::{fmt::Debug, marker::PhantomData};
@ -18,7 +19,7 @@ use thiserror::Error;
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // ✅ calling the getter clones and returns the value
@ -85,7 +86,7 @@ pub fn create_signal_from_stream<T>(
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // ✅ calling the getter clones and returns the value
@ -116,7 +117,7 @@ pub struct ReadSignal<T>
where
T: 'static,
{
pub(crate) runtime: &'static Runtime,
pub(crate) runtime: RuntimeId,
pub(crate) id: SignalId,
pub(crate) ty: PhantomData<T>,
}
@ -142,7 +143,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
///
/// // ❌ unnecessarily clones the string
@ -166,7 +167,7 @@ where
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
self.id.subscribe(self.runtime);
with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
}
/// Clones and returns the current value of the signal, and subscribes
@ -176,7 +177,7 @@ where
/// (`value.get()` is equivalent to `value.with(T::clone)`.)
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // calling the getter clones and returns the value
@ -193,7 +194,7 @@ where
/// Applies the function to the current Signal, if it exists, and subscribes
/// the running effect.
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Result<U, SignalError> {
self.id.try_with(self.runtime, f)
with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
}
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
@ -273,7 +274,7 @@ where
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // ✅ calling the setter sets the value
@ -294,7 +295,7 @@ pub struct WriteSignal<T>
where
T: 'static,
{
pub(crate) runtime: &'static Runtime,
pub(crate) runtime: RuntimeId,
pub(crate) id: SignalId,
pub(crate) ty: PhantomData<T>,
}
@ -324,7 +325,7 @@ where
/// even if the value has not actually changed.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // notifies subscribers
@ -347,7 +348,7 @@ where
/// even if the value has not actually changed.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // notifies subscribers
@ -414,7 +415,7 @@ where
/// or as a function argument.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// // ✅ set the value
@ -441,7 +442,7 @@ pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
/// its style, or it may be easier to pass around in a context or as a function argument.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// // ✅ set the value
@ -462,7 +463,7 @@ pub struct RwSignal<T>
where
T: 'static,
{
pub(crate) runtime: &'static Runtime,
pub(crate) runtime: RuntimeId,
pub(crate) id: SignalId,
pub(crate) ty: PhantomData<T>,
}
@ -496,11 +497,11 @@ impl<T> UntrackedGettableSignal<T> for RwSignal<T> {
impl<T> UntrackedSettableSignal<T> for RwSignal<T> {
fn set_untracked(&self, new_value: T) {
self.id
.update_with_no_effect(self.runtime, |v| *v = new_value);
.update_with_no_effect(self.runtime, |v| *v = new_value)
}
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
self.id.update_with_no_effect(self.runtime, f);
self.id.update_with_no_effect(self.runtime, f)
}
}
@ -512,7 +513,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let name = create_rw_signal(cx, "Alice".to_string());
///
/// // ❌ unnecessarily clones the string
@ -535,7 +536,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// assert_eq!(count.get(), 0);
@ -556,7 +557,7 @@ where
/// and notifies subscribers that the signal has changed.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// // notifies subscribers
@ -579,7 +580,7 @@ where
/// even if the value has not actually changed.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// assert_eq!(count(), 0);
@ -597,7 +598,7 @@ where
/// to the signal and cause other parts of the DOM to update.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
/// let read_count = count.read_only();
/// assert_eq!(count(), 0);
@ -726,78 +727,88 @@ impl SignalId {
self.try_with_no_subscription(runtime, f)
}
pub(crate) fn with_no_subscription<T, U>(&self, runtime: &Runtime, f: impl FnOnce(&T) -> U) -> U
pub(crate) fn with_no_subscription<T, U>(
&self,
runtime: RuntimeId,
f: impl FnOnce(&T) -> U,
) -> U
where
T: 'static,
{
self.try_with_no_subscription(runtime, f).unwrap()
with_runtime(runtime, |runtime| {
self.try_with_no_subscription(runtime, f).unwrap()
})
}
pub(crate) fn with<T, U>(&self, runtime: &Runtime, f: impl FnOnce(&T) -> U) -> U
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
where
T: 'static,
{
self.try_with(runtime, f).unwrap()
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
}
fn update_value<T>(&self, runtime: &Runtime, f: impl FnOnce(&mut T)) -> bool
fn update_value<T>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T)) -> bool
where
T: 'static,
{
let value = {
let signals = runtime.signals.borrow();
signals.get(*self).cloned()
};
if let Some(value) = value {
let mut value = value.borrow_mut();
if let Some(value) = value.downcast_mut::<T>() {
f(value);
true
with_runtime(runtime, |runtime| {
let value = {
let signals = runtime.signals.borrow();
signals.get(*self).cloned()
};
if let Some(value) = value {
let mut value = value.borrow_mut();
if let Some(value) = value.downcast_mut::<T>() {
f(value);
true
} else {
debug_warn!(
"[Signal::update] failed when downcasting to Signal<{}>",
std::any::type_name::<T>()
);
false
}
} else {
debug_warn!(
"[Signal::update] failed when downcasting to Signal<{}>",
"[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>()
);
false
}
} 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>()
);
false
}
})
}
pub(crate) fn update<T>(&self, runtime: &Runtime, f: impl FnOnce(&mut T))
pub(crate) fn update<T>(&self, runtime_id: RuntimeId, f: impl FnOnce(&mut T))
where
T: 'static,
{
// update the value
let updated = self.update_value(runtime, f);
with_runtime(runtime_id, |runtime| {
// update the value
let updated = self.update_value(runtime_id, f);
// notify subscribers
if updated {
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);
// notify subscribers
if updated {
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);
}
}
}
}
}
})
}
pub(crate) fn update_with_no_effect<T>(&self, runtime: &Runtime, f: impl FnOnce(&mut T))
pub(crate) fn update_with_no_effect<T>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T))
where
T: 'static,
{

View file

@ -10,8 +10,8 @@ use crate::{Memo, ReadSignal, RwSignal, Scope, UntrackedGettableSignal};
/// function call, `with()`, and `get()` APIs as over signals.
///
/// ```rust
/// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal};
/// # create_scope(|cx| {
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
@ -73,8 +73,8 @@ where
/// Wraps a derived signal, i.e., any computation that accesses one or more
/// reactive signals.
/// ```rust
/// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal};
/// # create_scope(|cx| {
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
///
@ -95,7 +95,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = Signal::derive(cx, move || name.with(|n| n.to_uppercase()));
/// let memoized_lower = create_memo(cx, move |_| name.with(|n| n.to_lowercase()));
@ -134,8 +134,8 @@ where
/// If you want to get the value without cloning it, use [ReadSignal::with].
/// (Theres no difference in behavior for derived signals: they re-run in any case.)
/// ```
/// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal};
/// # create_scope(|cx| {
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
@ -258,7 +258,7 @@ where
///
/// ```rust
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = MaybeSignal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
@ -317,8 +317,8 @@ where
/// Wraps a derived signal, i.e., any computation that accesses one or more
/// reactive signals.
/// ```rust
/// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal};
/// # create_scope(|cx| {
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
///
@ -339,7 +339,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = MaybeSignal::derive(cx, move || name.with(|n| n.to_uppercase()));
/// let memoized_lower = create_memo(cx, move |_| name.with(|n| n.to_lowercase()));
@ -381,7 +381,7 @@ where
/// (Theres no difference in behavior for derived signals: they re-run in any case.)
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = MaybeSignal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);

View file

@ -1,5 +1,7 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{create_isomorphic_effect, create_memo, create_scope, create_signal};
use leptos_reactive::{
create_isomorphic_effect, create_memo, create_runtime, create_scope, create_signal,
};
#[cfg(not(feature = "stable"))]
#[test]
@ -7,7 +9,7 @@ fn effect_runs() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
@ -36,7 +38,7 @@ fn effect_tracks_memo() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
let b = create_memo(cx, move |_| format!("Value is {}", a()));
@ -67,7 +69,7 @@ fn untrack_mutes_effect() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect

View file

@ -1,10 +1,10 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{create_memo, create_scope, create_signal};
use leptos_reactive::{create_memo, create_runtime, create_scope, create_signal};
#[cfg(not(feature = "stable"))]
#[test]
fn basic_memo() {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let a = create_memo(cx, |_| 5);
assert_eq!(a(), 5);
})
@ -14,7 +14,7 @@ fn basic_memo() {
#[cfg(not(feature = "stable"))]
#[test]
fn memo_with_computed_value() {
create_scope(|cx| {
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());
@ -30,7 +30,7 @@ fn memo_with_computed_value() {
#[cfg(not(feature = "stable"))]
#[test]
fn nested_memos() {
create_scope(|cx| {
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());
@ -54,7 +54,7 @@ fn nested_memos() {
fn memo_runs_only_when_inputs_change() {
use std::{cell::Cell, rc::Rc};
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let call_count = Rc::new(Cell::new(0));
let (a, set_a) = create_signal(cx, 0);
let (b, _) = create_signal(cx, 0);

View file

@ -1,10 +1,10 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{create_scope, create_signal};
use leptos_reactive::{create_runtime, create_scope, create_signal};
#[cfg(not(feature = "stable"))]
#[test]
fn basic_signal() {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
assert_eq!(a(), 0);
set_a(5);
@ -16,7 +16,7 @@ fn basic_signal() {
#[cfg(not(feature = "stable"))]
#[test]
fn derived_signals() {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();

View file

@ -1,6 +1,6 @@
//#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_scope, create_signal, UntrackedGettableSignal,
create_isomorphic_effect, create_runtime, create_scope, create_signal, UntrackedGettableSignal,
UntrackedSettableSignal,
};
@ -10,7 +10,7 @@ fn untracked_set_doesnt_trigger_effect() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
@ -42,7 +42,7 @@ fn untracked_get_doesnt_trigger_effect() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
let (a2, set_a2) = create_signal(cx, 1);