Compare commits

..

5 Commits

Author SHA1 Message Date
Greg Johnston
642ab9baf9 feels so good to remove this 2023-09-11 19:55:00 -04:00
Greg Johnston
ed58ac2c99 remove outdated section due to create_effect 2023-09-11 19:50:22 -04:00
Greg Johnston
4f1fafaea3 use create_render_effect within renderer 2023-09-11 19:50:11 -04:00
Greg Johnston
7a35d33402 make create_render_effect public and update create_effect docs 2023-09-11 19:50:02 -04:00
Greg Johnston
e6f053d39f change: run effects after a tick 2023-09-08 12:40:06 -04:00
20 changed files with 675 additions and 863 deletions

View File

@@ -87,43 +87,6 @@ The WASM version of your app, running in the browser, expects to find three item
Its pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If youre seeing warnings like this and you dont think its your fault, its much more likely that its a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
### Mutating the DOM during rendering
This is a slightly more common way to create a client/server mismatch: updating a signal _during rendering_ in a way that mutates the view.
```rust
#[component]
pub fn App() -> impl IntoView {
let (loaded, set_loaded) = create_signal(false);
// create_effect only runs on the client
create_effect(move |_| {
// do something like reading from localStorage
set_loaded(true);
});
move || {
if loaded() {
view! { <p>"Hello, world!"</p> }.into_any()
} else {
view! { <div class="loading">"Loading..."</div> }.into_any()
}
}
}
```
This one gives us the scary panic
```
panicked at 'assertion failed: `(left == right)`
left: `"DIV"`,
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
```
And a handy link to this page!
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
#### Solution
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.

View File

@@ -1,5 +1,4 @@
use leptos::{SignalWrite, *};
use std::cell::{Ref, RefMut};
use leptos::*;
/// A simple counter component.
///
@@ -13,20 +12,12 @@ pub fn SimpleCounter(
) -> impl IntoView {
let (value, set_value) = create_signal(initial_value);
let something: Ref<'_, i32> = value.read();
spawn_local(async move {
let mut something_else: RefMut<'_, i32> = set_value.write();
async {}.await;
*something_else = 30;
});
view! {
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| *set_value.write() -= step>"-1"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| *set_value.write() += step>"+1"</button>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
</div>
}
}

View File

@@ -210,13 +210,7 @@ pub fn TodoMVC() -> impl IntoView {
// focus the main input on load
create_effect(move |_| {
if let Some(input) = input_ref.get() {
// We use request_animation_frame here because the NodeRef
// is filled when the element is created, but before it's mounted
// to the DOM. Calling .focus() before it's mounted does nothing.
// So inside, we wait a tick for the browser to mount it, then .focus()
request_animation_frame(move || {
let _ = input.focus();
});
let _ = input.focus();
}
});

View File

@@ -7,7 +7,7 @@ use std::{cell::RefCell, fmt, ops::Deref, rc::Rc};
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable, Text};
use leptos_reactive::create_effect;
use leptos_reactive::create_render_effect;
use wasm_bindgen::JsCast;
}
}
@@ -181,194 +181,204 @@ where
let span = tracing::Span::current();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_effect(move |prev_run: Option<Option<web_sys::Node>>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
create_render_effect(
move |prev_run: Option<Option<web_sys::Node>>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
let new_child = child_fn().into_view();
let new_child = child_fn().into_view();
let mut child_borrow = child.borrow_mut();
let mut child_borrow = child.borrow_mut();
// Is this at least the second time we are loading a child?
if let Some(prev_t) = prev_run {
let child = child_borrow.take().unwrap();
// Is this at least the second time we are loading a child?
if let Some(prev_t) = prev_run {
let child = child_borrow.take().unwrap();
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child
.get_closing_node()
.next_non_view_marker_sibling()
.as_ref()
!= Some(&closing);
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child
.get_closing_node()
.next_non_view_marker_sibling()
.as_ref()
!= Some(&closing);
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
// nb: the match/ownership gymnastics here
// are so that, if we can reuse the text node,
// we can take ownership of new_t so we don't clone
// the contents, which in O(n) on the length of the text
if matches!(new_child, View::Text(_)) {
if !was_child_moved && child != new_child {
let mut new_t = match new_child {
View::Text(t) => t,
_ => unreachable!(),
};
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
// nb: the match/ownership gymnastics here
// are so that, if we can reuse the text node,
// we can take ownership of new_t so we don't clone
// the contents, which in O(n) on the length of the text
if matches!(new_child, View::Text(_)) {
if !was_child_moved && child != new_child {
let mut new_t = match new_child {
View::Text(t) => t,
_ => unreachable!(),
};
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
// replace new_t's text node with the prev node
// see discussion: https://github.com/leptos-rs/leptos/pull/1472
new_t.node = prev_t.clone();
// replace new_t's text node with the prev node
// see discussion: https://github.com/leptos-rs/leptos/pull/1472
new_t.node = prev_t.clone();
let new_child = View::Text(new_t);
**child_borrow = Some(new_child);
let new_child = View::Text(new_t);
**child_borrow = Some(new_child);
Some(prev_t)
} else {
let new_t = new_child.as_text().unwrap();
Some(prev_t)
} else {
let new_t = new_child.as_text().unwrap();
mount_child(
MountKind::Before(&closing),
&new_child,
);
**child_borrow = Some(new_child.clone());
Some(new_t.node.clone())
}
}
// Child is not a text node, so we can remove the previous
// text node
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
// Mount the new child, and we're done
mount_child(
MountKind::Before(&closing),
&new_child,
);
**child_borrow = Some(new_child.clone());
**child_borrow = Some(new_child);
Some(new_t.node.clone())
None
}
}
// Child is not a text node, so we can remove the previous
// text node
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
if !HydrationCtx::is_hydrating() {
let same_child = child == new_child;
if !was_child_moved && !same_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
match child {
View::CoreComponent(
crate::CoreComponent::DynChild(
child,
),
) => {
let start =
child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
View::Component(child) => {
let start =
child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
_ => unmount_child(&start, end),
}
}
// Mount the new child
// If it's the same child, don't re-mount
if !same_child {
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
}
// Mount the new child, and we're done
// We want to reuse text nodes, so hold onto it if
// our child is one
let t =
new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
t
};
ret
}
// Otherwise, we know for sure this is our first time
else {
// If it's a text node, we want to use the old text node
// as the text node for the DynChild, rather than the new
// text node being created during hydration
let new_child = if HydrationCtx::is_hydrating()
&& new_child.get_text().is_some()
{
let t = closing
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Text>();
let new_child = match new_child {
View::Text(text) => text,
_ => unreachable!(),
};
t.set_data(&new_child.content);
View::Text(Text {
node: t.unchecked_into(),
content: new_child.content,
})
} else {
new_child
};
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(
MountKind::Before(&closing),
&new_child,
);
**child_borrow = Some(new_child);
None
}
}
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
if !HydrationCtx::is_hydrating() {
let same_child = child == new_child;
if !was_child_moved && !same_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
match child {
View::CoreComponent(
crate::CoreComponent::DynChild(child),
) => {
let start = child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
View::Component(child) => {
let start = child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
_ => unmount_child(&start, end),
}
}
// Mount the new child
// If it's the same child, don't re-mount
if !same_child {
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
}
// We want to reuse text nodes, so hold onto it if
// our child is one
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
t
};
ret
}
// Otherwise, we know for sure this is our first time
else {
// If it's a text node, we want to use the old text node
// as the text node for the DynChild, rather than the new
// text node being created during hydration
let new_child = if HydrationCtx::is_hydrating()
&& new_child.get_text().is_some()
{
let t = closing
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Text>();
let new_child = match new_child {
View::Text(text) => text,
_ => unreachable!(),
};
t.set_data(&new_child.content);
View::Text(Text {
node: t.unchecked_into(),
content: new_child.content,
})
} else {
new_child
};
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(MountKind::Before(&closing), &new_child);
}
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
t
}
});
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{

View File

@@ -12,7 +12,7 @@ mod web {
mount_child, prepare_to_move, MountKind, Mountable, RANGE,
};
pub use drain_filter_polyfill::VecExt as VecDrainFilterExt;
pub use leptos_reactive::create_effect;
pub use leptos_reactive::create_render_effect;
pub use std::cell::OnceCell;
pub use wasm_bindgen::JsCast;
}
@@ -406,90 +406,100 @@ where
let each_fn = as_child_of_current_owner(each_fn);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_effect(move |prev_hash_run: Option<HashRun<FxIndexSet<K>>>| {
let mut children_borrow = children.borrow_mut();
create_render_effect(
move |prev_hash_run: Option<HashRun<FxIndexSet<K>>>| {
let mut children_borrow = children.borrow_mut();
#[cfg(all(
not(debug_assertions),
target_arch = "wasm32",
feature = "web"
))]
let opening = if let Some(Some(child)) = children_borrow.get(0) {
// correctly remove opening <!--<EachItem/>-->
let child_opening = child.get_opening_node();
#[cfg(debug_assertions)]
#[cfg(all(
not(debug_assertions),
target_arch = "wasm32",
feature = "web"
))]
let opening = if let Some(Some(child)) = children_borrow.get(0)
{
use crate::components::dyn_child::NonViewMarkerSibling;
child_opening
.previous_non_view_marker_sibling()
.unwrap_or(child_opening)
// correctly remove opening <!--<EachItem/>-->
let child_opening = child.get_opening_node();
#[cfg(debug_assertions)]
{
use crate::components::dyn_child::NonViewMarkerSibling;
child_opening
.previous_non_view_marker_sibling()
.unwrap_or(child_opening)
}
#[cfg(not(debug_assertions))]
{
child_opening
}
} else {
closing.clone()
};
let items_iter = items_fn().into_iter();
let (capacity, _) = items_iter.size_hint();
let mut hashed_items = FxIndexSet::with_capacity_and_hasher(
capacity,
Default::default(),
);
if let Some(HashRun(prev_hash_run)) = prev_hash_run {
if !prev_hash_run.is_empty() {
let mut items = Vec::with_capacity(capacity);
for item in items_iter {
hashed_items.insert(key_fn(&item));
items.push(Some(item));
}
let cmds = diff(&prev_hash_run, &hashed_items);
apply_diff(
#[cfg(all(
target_arch = "wasm32",
feature = "web"
))]
&opening,
#[cfg(all(
target_arch = "wasm32",
feature = "web"
))]
&closing,
cmds,
&mut children_borrow,
items,
&each_fn,
);
return HashRun(hashed_items);
}
}
#[cfg(not(debug_assertions))]
{
child_opening
}
} else {
closing.clone()
};
let items_iter = items_fn().into_iter();
// if previous run is empty
*children_borrow = Vec::with_capacity(capacity);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let fragment = crate::document().create_document_fragment();
let (capacity, _) = items_iter.size_hint();
let mut hashed_items = FxIndexSet::with_capacity_and_hasher(
capacity,
Default::default(),
);
for item in items_iter {
hashed_items.insert(key_fn(&item));
let (child, disposer) = each_fn(item);
let each_item = EachItem::new(disposer, child.into_view());
if let Some(HashRun(prev_hash_run)) = prev_hash_run {
if !prev_hash_run.is_empty() {
let mut items = Vec::with_capacity(capacity);
for item in items_iter {
hashed_items.insert(key_fn(&item));
items.push(Some(item));
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
_ = fragment
.append_child(&each_item.get_mountable_node());
}
let cmds = diff(&prev_hash_run, &hashed_items);
apply_diff(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
&opening,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
&closing,
cmds,
&mut children_borrow,
items,
&each_fn,
);
return HashRun(hashed_items);
children_borrow.push(Some(each_item));
}
}
// if previous run is empty
*children_borrow = Vec::with_capacity(capacity);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let fragment = crate::document().create_document_fragment();
for item in items_iter {
hashed_items.insert(key_fn(&item));
let (child, disposer) = each_fn(item);
let each_item = EachItem::new(disposer, child.into_view());
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
_ = fragment.append_child(&each_item.get_mountable_node());
}
closing
.unchecked_ref::<web_sys::Element>()
.before_with_node_1(&fragment)
.expect("before to not err");
children_borrow.push(Some(each_item));
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
closing
.unchecked_ref::<web_sys::Element>()
.before_with_node_1(&fragment)
.expect("before to not err");
HashRun(hashed_items)
});
HashRun(hashed_items)
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{

View File

@@ -719,7 +719,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
let class_list = self.element.as_ref().class_list();
leptos_reactive::create_effect(
leptos_reactive::create_render_effect(
move |prev_classes: Option<
SmallVec<[Oco<'static, str>; 4]>,
>| {

View File

@@ -1,6 +1,6 @@
use crate::{html::ElementDescriptor, HtmlElement};
use leptos_reactive::{
create_effect, create_rw_signal, signal_prelude::*, RwSignal,
create_render_effect, create_rw_signal, signal_prelude::*, RwSignal,
};
use std::cell::Cell;
@@ -134,7 +134,7 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
{
let f = Cell::new(Some(f));
create_effect(move |_| {
create_render_effect(move |_| {
if let Some(node_ref) = self.get() {
f.take().unwrap()(node_ref);
}

View File

@@ -8,7 +8,6 @@ repository = "https://github.com/leptos-rs/leptos"
description = "Reactive system for the Leptos web framework."
[dependencies]
elsa = "1"
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.4", optional = true }

View File

@@ -1,161 +0,0 @@
use elsa::FrozenVec;
use std::{
any::Any,
cell::{Cell, Ref, RefCell, RefMut},
fmt::Debug,
marker::PhantomData,
rc::Rc,
};
#[derive(Clone)]
pub(crate) struct Arena {
#[allow(clippy::type_complexity)]
values: &'static FrozenVec<Box<Slot>>,
open: Rc<RefCell<Vec<u32>>>,
}
impl Arena {
pub fn new() -> Self {
Self {
values: Box::leak(Box::new(FrozenVec::new())),
open: Default::default(),
}
}
fn push<T: 'static>(&self, value: T) -> NodeId {
self.push_erased(Box::new(value))
}
pub fn get<T: 'static>(&self, id: &NodeId) -> Option<Ref<'_, T>> {
let node = self.get_any(id)?;
Ref::filter_map(node, |node| {
let node = node.as_ref()?;
match node.downcast_ref::<T>() {
Some(t) => Some(t),
None => None,
}
})
.ok()
}
pub fn get_mut<T: 'static>(&self, id: &NodeId) -> Option<RefMut<'_, T>> {
let node = self.get_any_mut(id)?;
RefMut::filter_map(node, |node| {
let node = node.as_mut()?;
match node.downcast_mut::<T>() {
Some(t) => Some(t),
None => None,
}
})
.ok()
}
pub fn remove(&self, id: &NodeId) -> Option<Box<dyn Any>> {
self.recycle(id)
}
fn get_any(&self, id: &NodeId) -> Option<Ref<'_, Option<Box<dyn Any>>>> {
let node = self.values.get(id.idx as usize)?;
if id.generation == node.generation.get() {
Some(node.value.borrow())
} else {
None
}
}
pub fn get_any_mut(
&self,
id: &NodeId,
) -> Option<RefMut<'_, Option<Box<dyn Any>>>> {
let node = self.values.get(id.idx as usize)?;
if id.generation == node.generation.get() {
Some(node.value.borrow_mut())
} else {
None
}
}
fn recycle(&self, id: &NodeId) -> Option<Box<dyn Any>> {
let node = self.values.get(id.idx as usize)?;
node.value.borrow_mut().take()
}
fn push_erased(&self, value: Box<dyn Any>) -> NodeId {
if let Some(next_node) = self.open.borrow_mut().pop() {
let slot = self.values.get(next_node as usize).unwrap();
let generation = slot.generation.get() + 1;
slot.generation.set(generation);
*slot.value.borrow_mut() = Some(value);
NodeId {
idx: next_node,
generation,
}
} else {
self.values.push(Box::new(Slot {
generation: Cell::new(0),
value: RefCell::new(Some(value)),
}));
let idx = (self.values.len() - 1) as u32;
NodeId { idx, generation: 0 }
}
}
}
impl Debug for Arena {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Arena")
.field("values", &self.values.len())
.field("open", &self.open)
.finish()
}
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
/// TODO
pub struct NodeId {
idx: u32,
generation: u32,
}
#[derive(Debug)]
pub(crate) struct Slot {
generation: Cell<u32>,
value: RefCell<Option<Box<dyn Any>>>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct Value<T> {
id: NodeId,
ty: PhantomData<T>,
}
#[derive(Debug)]
pub(crate) struct ArenaOwner {
pub(crate) arena: &'static Arena,
pub(crate) owned: RefCell<Vec<NodeId>>,
}
impl ArenaOwner {
pub fn insert<T: 'static>(&self, value: T) -> NodeId {
let id = self.arena.push(value);
self.register(id)
}
pub fn insert_boxed(&self, value: Box<dyn Any>) -> NodeId {
let id = self.arena.push_erased(value);
self.register(id)
}
fn register(&self, id: NodeId) -> NodeId {
self.owned.borrow_mut().push(id);
id
}
}
impl Drop for ArenaOwner {
fn drop(&mut self) {
for location in self.owned.borrow().iter() {
drop(self.arena.recycle(location));
}
}
}

View File

@@ -60,7 +60,7 @@ where
let mut contexts = runtime.contexts.borrow_mut();
let owner = runtime.owner.get();
if let Some(owner) = owner {
let context = contexts.entry(owner).or_default();
let context = contexts.entry(owner).unwrap().or_default();
context.insert(id, Box::new(value) as Box<dyn Any>);
} else {
crate::macros::debug_warn!(

View File

@@ -1,20 +1,24 @@
use crate::{
arena::NodeId, runtime::ThreadArena, with_runtime, Disposer, Runtime,
SignalDispose,
};
use crate::{node::NodeId, with_runtime, Disposer, Runtime, SignalDispose};
use cfg_if::cfg_if;
use std::marker::PhantomData;
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
/// `create_effect` queues the given function to run once, tracks its dependence
/// on any signal values read within it, and reruns the function whenever the value
/// of a dependency changes.
///
/// Effects are intended to run *side-effects* of the system, not to synchronize state
/// *within* the system. In other words: don't write to signals within effects.
/// *within* the system. In other words: don't write to signals within effects, unless
/// youre coordinating with some other non-reactive side effect.
/// (If you need to define a signal that depends on the value of other signals, use a
/// derived signal or [`create_memo`](crate::create_memo)).
///
/// This first run is queued for the next microtask, i.e., it runs after all other
/// synchronous code has completed. In practical terms, this means that if you use
/// `create_effect` in the body of the component, it will run *after* the view has been
/// created and (presumably) mounted. (If you need an effect that runs immediately, use
/// [`create_render_effect`].)
///
/// The effect function is called with an argument containing whatever value it returned
/// the last time it ran. On the initial run, this is `None`.
///
@@ -66,12 +70,20 @@ where
{
cfg_if! {
if #[cfg(not(feature = "ssr"))] {
use crate::{Owner, queue_microtask, with_owner};
let runtime = Runtime::current();
let owner = Owner::current();
let id = runtime.create_effect(f);
//crate::macros::debug_warn!("creating effect {e:?}");
_ = with_runtime( |runtime| {
runtime.update_if_necessary(id);
queue_microtask(move || {
with_owner(owner.unwrap(), move || {
_ = with_runtime( |runtime| {
runtime.update_if_necessary(id);
});
});
});
Effect { id, ty: PhantomData }
} else {
// clear warnings
@@ -180,8 +192,16 @@ where
&self,
f: impl FnOnce(&mut Option<T>) -> U,
) -> Option<U> {
let mut value = ThreadArena::get_mut::<Option<T>>(&self.id)?;
Some(f(&mut *value))
with_runtime(|runtime| {
let nodes = runtime.nodes.borrow();
let node = nodes.get(self.id)?;
let value = node.value.clone()?;
let mut value = value.borrow_mut();
let value = value.downcast_mut()?;
Some(f(value))
})
.ok()
.flatten()
}
}
@@ -241,7 +261,10 @@ where
}
}
#[doc(hidden)]
/// Creates an effect exactly like [`create_effect`], but runs immediately rather
/// than being queued until the end of the current microtask. This is mostly used
/// inside the renderer but is available for use cases in which scheduling the effect
/// for the next tick is not optimal.
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
@@ -253,11 +276,26 @@ where
)
)]
#[inline(always)]
pub fn create_render_effect<T>(f: impl Fn(Option<T>) -> T + 'static)
pub fn create_render_effect<T>(
f: impl Fn(Option<T>) -> T + 'static,
) -> Effect<T>
where
T: 'static,
{
create_effect(f);
cfg_if! {
if #[cfg(not(feature = "ssr"))] {
let runtime = Runtime::current();
let id = runtime.create_effect(f);
_ = with_runtime( |runtime| {
runtime.update_if_necessary(id);
});
Effect { id, ty: PhantomData }
} else {
// clear warnings
_ = f;
Effect { id: Default::default(), ty: PhantomData }
}
}
}
/// A handle to an effect, can be used to explicitly dispose of the effect.
@@ -291,7 +329,7 @@ where
}
pub(crate) trait AnyComputation {
fn run(&self, node: NodeId) -> bool;
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool;
}
impl<T, F> AnyComputation for EffectState<T, F>
@@ -311,13 +349,15 @@ where
)
)
)]
fn run(&self, node: NodeId) -> bool {
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 = ThreadArena::get_mut::<Option<T>>(&node)
let mut value = value.borrow_mut();
let value = value
.downcast_mut::<Option<T>>()
.expect("to downcast effect value");
value.take()
};
@@ -326,7 +366,9 @@ where
let new_value = (self.f)(curr_value);
// set new value
let mut value = ThreadArena::get_mut::<Option<T>>(&node)
let mut value = value.borrow_mut();
let value = value
.downcast_mut::<Option<T>>()
.expect("to downcast effect value");
*value = Some(new_value);

View File

@@ -2,7 +2,7 @@
#![deny(missing_docs)]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![feature(type_name_of_val)]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
//! The reactive system for the [Leptos](https://docs.rs/leptos/latest/leptos/) Web framework.
//!
@@ -76,7 +76,6 @@
#[cfg_attr(any(debug_assertions, feature = "ssr"), macro_use)]
extern crate tracing;
mod arena;
#[macro_use]
mod signal;
mod context;

View File

@@ -1,10 +1,9 @@
use crate::{
arena::NodeId, create_effect, diagnostics::AccessDiagnostics, on_cleanup,
runtime::ThreadArena, with_runtime, AnyComputation, Runtime, SignalDispose,
SignalGet, SignalGetUntracked, SignalStream, SignalWith,
SignalWithUntracked,
create_effect, diagnostics::AccessDiagnostics, node::NodeId, on_cleanup,
with_runtime, AnyComputation, Runtime, SignalDispose, SignalGet,
SignalGetUntracked, SignalStream, SignalWith, SignalWithUntracked,
};
use std::{fmt, marker::PhantomData};
use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
// IMPLEMENTATION NOTE:
// Memos are implemented "lazily," i.e., the inner computation is not run
@@ -283,8 +282,8 @@ impl<T: Clone> SignalGetUntracked for Memo<T> {
.expect("invariant: must have already been initialized")
};
match self.id.try_with_no_subscription(runtime, f) {
Some(t) => t,
None => panic_getting_dead_memo(
Ok(t) => t,
Err(_) => panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
@@ -331,8 +330,8 @@ impl<T> SignalWithUntracked for Memo<T> {
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
with_runtime(|runtime| {
match self.id.try_with_no_subscription(runtime, forward_ref_to(f)) {
Some(t) => t,
None => panic_getting_dead_memo(
Ok(t) => t,
Err(_) => panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
@@ -357,7 +356,7 @@ impl<T> SignalWithUntracked for Memo<T> {
#[inline]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
with_runtime(|runtime| {
self.id.try_with_no_subscription(runtime, |v: &T| f(v))
self.id.try_with_no_subscription(runtime, |v: &T| f(v)).ok()
})
.ok()
.flatten()
@@ -469,7 +468,9 @@ impl<T> SignalWith for Memo<T> {
with_runtime(|runtime| {
self.id.subscribe(runtime, diagnostics);
self.id.try_with_no_subscription(runtime, forward_ref_to(f))
self.id
.try_with_no_subscription(runtime, forward_ref_to(f))
.ok()
})
.ok()
.flatten()
@@ -543,12 +544,12 @@ where
)
)
)]
fn run(&self, node: NodeId) -> bool {
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
let (new_value, is_different) = {
let curr_value = match ThreadArena::get::<Option<T>>(&node) {
Some(t) => t,
_ => return false,
};
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());
@@ -556,8 +557,11 @@ where
(new_value, is_different)
};
if is_different {
let mut value = ThreadArena::get_mut::<Option<T>>(&node).unwrap();
*value = Some(new_value);
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

View File

@@ -1,7 +1,10 @@
use crate::{
arena::NodeId, runtime::ThreadArena, with_runtime, AnyComputation,
};
use std::rc::Rc;
use crate::{with_runtime, AnyComputation};
use std::{any::Any, cell::RefCell, rc::Rc};
slotmap::new_key_type! {
/// Unique ID assigned to a signal.
pub struct NodeId;
}
/// Handle to dispose of a reactive node.
#[derive(Debug, PartialEq, Eq)]
@@ -11,7 +14,6 @@ impl Drop for Disposer {
fn drop(&mut self) {
let id = self.0;
_ = with_runtime(|runtime| {
ThreadArena::remove(&id);
runtime.cleanup_node(id);
runtime.dispose_node(id);
});
@@ -20,10 +22,19 @@ impl Drop for Disposer {
#[derive(Clone)]
pub(crate) struct ReactiveNode {
pub value: Option<Rc<RefCell<dyn Any>>>,
pub state: ReactiveNodeState,
pub node_type: ReactiveNodeType,
}
impl ReactiveNode {
pub fn value(&self) -> Rc<RefCell<dyn Any>> {
self.value
.clone()
.expect("ReactiveNode.value to have a value")
}
}
#[derive(Clone)]
pub(crate) enum ReactiveNodeType {
Trigger,

View File

@@ -1116,7 +1116,8 @@ where
let v = self
.value
.try_with(|n| n.as_ref().map(|n| Some(f(n))))?
.try_with(|n| n.as_ref().map(|n| Some(f(n))))
.ok()?
.flatten();
self.handle_result(location, global_suspense_cx, suspense_cx, v)
@@ -1131,7 +1132,7 @@ where
let global_suspense_cx = use_context::<GlobalSuspenseContext>();
let suspense_cx = use_context::<SuspenseContext>();
let v = self.value.try_with(|n| f(n));
let v = self.value.try_with(|n| f(n)).ok();
self.handle_result(location, global_suspense_cx, suspense_cx, v)
}

View File

@@ -1,22 +1,23 @@
#[cfg(debug_assertions)]
use crate::SpecialNonReactiveZone;
use crate::{
arena::{Arena, ArenaOwner, NodeId},
hydration::SharedContext,
node::{Disposer, ReactiveNode, ReactiveNodeState, ReactiveNodeType},
node::{
Disposer, NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType,
},
AnyComputation, AnyResource, EffectState, Memo, MemoState, ReadSignal,
ResourceId, ResourceState, RwSignal, SerializableResource, Trigger,
UnserializableResource, WriteSignal,
ResourceId, ResourceState, RwSignal, SerializableResource, StoredValueId,
Trigger, UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
use core::hash::BuildHasherDefault;
use futures::stream::FuturesUnordered;
use indexmap::IndexSet;
use rustc_hash::{FxHashMap, FxHasher};
use slotmap::SlotMap;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
any::{Any, TypeId},
cell::{Cell, Ref, RefCell, RefMut},
cell::{Cell, RefCell},
fmt::Debug,
future::Future,
marker::PhantomData,
@@ -29,13 +30,12 @@ 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 ARENA: &'static Arena = Box::leak(Box::new(Arena::new()));
pub(crate) static RUNTIME: Runtime = Runtime::new();
}
} else {
thread_local! {
pub(crate) static ARENA: &'static Arena = Box::leak(Box::new(Arena::new()));
pub(crate) static RUNTIMES: RefCell<SlotMap<RuntimeId, Runtime>> = Default::default();
pub(crate) static CURRENT_RUNTIME: Cell<Option<RuntimeId>> = Default::default();
}
}
@@ -53,54 +53,29 @@ type FxIndexSet<T> = IndexSet<T, BuildHasherDefault<FxHasher>>;
// and other data included in the reactive system.
#[derive(Default)]
pub(crate) struct Runtime {
pub arena: ArenaOwner,
pub shared_context: RefCell<SharedContext>,
pub owner: Cell<Option<NodeId>>,
pub observer: Cell<Option<NodeId>>,
#[allow(clippy::type_complexity)]
pub on_cleanups: RefCell<FxHashMap<NodeId, Vec<Box<dyn FnOnce()>>>>,
pub nodes: RefCell<FxHashMap<NodeId, ReactiveNode>>,
pub on_cleanups:
RefCell<SparseSecondaryMap<NodeId, Vec<Box<dyn FnOnce()>>>>,
pub stored_values: RefCell<SlotMap<StoredValueId, Rc<RefCell<dyn Any>>>>,
pub nodes: RefCell<SlotMap<NodeId, ReactiveNode>>,
pub node_subscribers:
RefCell<FxHashMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
pub node_sources: RefCell<FxHashMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
pub node_owners: RefCell<FxHashMap<NodeId, NodeId>>,
pub node_properties: RefCell<FxHashMap<NodeId, Vec<ScopeProperty>>>,
RefCell<SecondaryMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
pub node_sources:
RefCell<SecondaryMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
pub node_owners: RefCell<SecondaryMap<NodeId, NodeId>>,
pub node_properties:
RefCell<SparseSecondaryMap<NodeId, Vec<ScopeProperty>>>,
#[allow(clippy::type_complexity)]
pub contexts: RefCell<FxHashMap<NodeId, FxHashMap<TypeId, Box<dyn Any>>>>,
pub contexts:
RefCell<SparseSecondaryMap<NodeId, FxHashMap<TypeId, Box<dyn Any>>>>,
pub pending_effects: RefCell<Vec<NodeId>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
pub batching: Cell<bool>,
}
impl Default for ArenaOwner {
fn default() -> ArenaOwner {
ArenaOwner {
arena: ARENA.with(|a| *a),
owned: Default::default(),
}
}
}
pub(crate) struct ThreadArena;
impl ThreadArena {
fn this() -> &'static Arena {
ARENA.with(|a| *a)
}
pub fn get<T: 'static>(id: &NodeId) -> Option<Ref<'_, T>> {
Self::this().get::<T>(id)
}
pub fn get_mut<T: 'static>(id: &NodeId) -> Option<RefMut<'_, T>> {
Self::this().get_mut::<T>(id)
}
pub fn remove(id: &NodeId) -> Option<Box<dyn Any>> {
Self::this().remove(id)
}
}
/// The current reactive runtime.
pub fn current_runtime() -> RuntimeId {
Runtime::current()
@@ -167,7 +142,7 @@ impl Runtime {
let sources = self.node_sources.borrow();
// rather than cloning the entire FxIndexSet, only allocate a `Vec` for the node ids
sources.get(&node_id).map(|n| {
sources.get(node_id).map(|n| {
let sources = n.borrow();
// in case Vec::from_iterator specialization doesn't work, do it manually
let mut sources_vec = Vec::with_capacity(sources.len());
@@ -200,7 +175,7 @@ impl Runtime {
pub(crate) fn cleanup_node(&self, node_id: NodeId) {
// first, run our cleanups, if any
let c = { self.on_cleanups.borrow_mut().remove(&node_id) };
let c = { self.on_cleanups.borrow_mut().remove(node_id) };
if let Some(cleanups) = c {
for cleanup in cleanups {
cleanup();
@@ -208,7 +183,7 @@ impl Runtime {
}
// dispose of any of our properties
let properties = { self.node_properties.borrow_mut().remove(&node_id) };
let properties = { self.node_properties.borrow_mut().remove(node_id) };
if let Some(properties) = properties {
for property in properties {
self.cleanup_property(property);
@@ -219,8 +194,9 @@ impl Runtime {
pub(crate) fn update(&self, node_id: NodeId) {
let node = {
let nodes = self.nodes.borrow();
nodes.get(&node_id).cloned()
nodes.get(node_id).cloned()
};
if let Some(node) = node {
// memos and effects rerun
// signals simply have their value
@@ -228,12 +204,13 @@ impl Runtime {
ReactiveNodeType::Signal | ReactiveNodeType::Trigger => true,
ReactiveNodeType::Memo { ref f }
| ReactiveNodeType::Effect { ref f } => {
let value = node.value();
// set this node as the observer
self.with_observer(node_id, move || {
// clean up sources of this memo/effect
self.cleanup_sources(node_id);
f.run(node_id)
f.run(value)
})
}
};
@@ -242,10 +219,10 @@ impl Runtime {
if changed {
let subs = self.node_subscribers.borrow();
if let Some(subs) = subs.get(&node_id) {
if let Some(subs) = subs.get(node_id) {
let mut nodes = self.nodes.borrow_mut();
for sub_id in subs.borrow().iter() {
if let Some(sub) = nodes.get_mut(sub_id) {
if let Some(sub) = nodes.get_mut(*sub_id) {
sub.state = ReactiveNodeState::Dirty;
}
}
@@ -264,53 +241,53 @@ impl Runtime {
| ScopeProperty::Trigger(node)
| ScopeProperty::Effect(node) => {
// run all cleanups for this node
let cleanups = { self.on_cleanups.borrow_mut().remove(&node) };
let cleanups = { self.on_cleanups.borrow_mut().remove(node) };
for cleanup in cleanups.into_iter().flatten() {
cleanup();
}
// clean up all children
let properties =
{ self.node_properties.borrow_mut().remove(&node) };
{ self.node_properties.borrow_mut().remove(node) };
for property in properties.into_iter().flatten() {
self.cleanup_property(property);
}
// each of the subs needs to remove the node from its dependencies
// so that it doesn't try to read the (now disposed) signal
let subs = self.node_subscribers.borrow_mut().remove(&node);
let subs = self.node_subscribers.borrow_mut().remove(node);
if let Some(subs) = subs {
let source_map = self.node_sources.borrow();
for effect in subs.borrow().iter() {
if let Some(effect_sources) = source_map.get(effect) {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&node);
}
}
}
// no longer needs to track its sources
self.node_sources.borrow_mut().remove(&node);
self.node_sources.borrow_mut().remove(node);
// remove the node from the graph
let node = ThreadArena::remove(&node);
let node = { self.nodes.borrow_mut().remove(node) };
drop(node);
}
ScopeProperty::Resource(id) => {
self.resources.borrow_mut().remove(id);
}
ScopeProperty::StoredValue(id) => {
ThreadArena::remove(&id);
self.stored_values.borrow_mut().remove(id);
}
}
}
pub(crate) fn cleanup_sources(&self, node_id: NodeId) {
let sources = self.node_sources.borrow();
if let Some(sources) = sources.get(&node_id) {
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) {
if let Some(source) = subs.get(*source) {
source.borrow_mut().remove(&node_id);
}
}
@@ -318,7 +295,7 @@ impl Runtime {
}
fn current_state(&self, node: NodeId) -> ReactiveNodeState {
match self.nodes.borrow().get(&node) {
match self.nodes.borrow().get(node) {
None => ReactiveNodeState::Clean,
Some(node) => node.state,
}
@@ -339,7 +316,7 @@ impl Runtime {
fn mark_clean(&self, node: NodeId) {
let mut nodes = self.nodes.borrow_mut();
if let Some(node) = nodes.get_mut(&node) {
if let Some(node) = nodes.get_mut(node) {
node.state = ReactiveNodeState::Clean;
}
}
@@ -347,7 +324,7 @@ impl Runtime {
pub(crate) fn mark_dirty(&self, node: NodeId) {
let mut nodes = self.nodes.borrow_mut();
if let Some(current_node) = nodes.get_mut(&node) {
if let Some(current_node) = nodes.get_mut(node) {
if current_node.state == ReactiveNodeState::DirtyMarked {
return;
}
@@ -399,7 +376,7 @@ impl Runtime {
let mut stack = Vec::new();
if let Some(children) = subscribers.get(&node) {
if let Some(children) = subscribers.get(node) {
stack.push(RefIter::new(children.borrow(), |children| {
children.iter()
}));
@@ -411,7 +388,7 @@ impl Runtime {
return IterResult::Empty;
};
while let Some(node) = nodes.get_mut(&child) {
while let Some(node) = nodes.get_mut(child) {
if node.state == ReactiveNodeState::Check
|| node.state == ReactiveNodeState::DirtyMarked
{
@@ -426,7 +403,7 @@ impl Runtime {
current_observer,
);
if let Some(children) = subscribers.get(&child) {
if let Some(children) = subscribers.get(child) {
let children = children.borrow();
if !children.is_empty() {
@@ -493,9 +470,9 @@ impl Runtime {
}
pub(crate) fn dispose_node(&self, node: NodeId) {
self.node_sources.borrow_mut().remove(&node);
self.node_subscribers.borrow_mut().remove(&node);
ThreadArena::remove(&node);
self.node_sources.borrow_mut().remove(node);
self.node_subscribers.borrow_mut().remove(node);
self.nodes.borrow_mut().remove(node);
}
#[track_caller]
@@ -508,8 +485,10 @@ impl Runtime {
) {
let mut properties = self.node_properties.borrow_mut();
if let Some(owner) = self.owner.get() {
let mut entry = properties.entry(owner).or_default();
entry.push(property);
if let Some(entry) = properties.entry(owner) {
let entry = entry.or_default();
entry.push(property);
}
if let Some(node) = property.to_node_id() {
let mut owners = self.node_owners.borrow_mut();
@@ -530,7 +509,7 @@ impl Runtime {
) -> Option<T> {
let contexts = self.contexts.borrow();
let context = contexts.get(&node);
let context = contexts.get(node);
let local_value = context.and_then(|context| {
context
.get(&ty)
@@ -542,7 +521,7 @@ impl Runtime {
None => self
.node_owners
.borrow()
.get(&node)
.get(node)
.and_then(|parent| self.get_context(*parent, ty)),
}
}
@@ -573,7 +552,7 @@ impl Runtime {
property: ScopeProperty,
) {
let mut properties = self.node_properties.borrow_mut();
if let Some(properties) = properties.get_mut(&owner) {
if let Some(properties) = properties.get_mut(owner) {
// remove this property from the list, if found
if let Some(index) = properties.iter().position(|p| p == &property)
{
@@ -585,7 +564,7 @@ impl Runtime {
if let Some(node) = property.to_node_id() {
let mut owners = self.node_owners.borrow_mut();
owners.remove(&node);
owners.remove(node);
}
}
}
@@ -679,14 +658,11 @@ where
runtime.owner.set(owner);
runtime.observer.set(owner);
let id = runtime.arena.insert(None::<()>);
runtime.nodes.borrow_mut().insert(
id,
ReactiveNode {
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Trigger,
},
);
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
value: None,
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Trigger,
});
runtime.push_scope_property(ScopeProperty::Trigger(id));
let disposer = Disposer(id);
@@ -833,14 +809,11 @@ impl RuntimeId {
#[inline(always)] // only because it's placed here to fit in with the other create methods
pub(crate) fn create_trigger(self) -> Trigger {
let id = with_runtime(|runtime| {
let id = runtime.arena.insert(None::<()>);
runtime.nodes.borrow_mut().insert(
id,
ReactiveNode {
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Trigger,
},
);
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
value: None,
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Trigger,
});
runtime.push_scope_property(ScopeProperty::Trigger(id));
id
})
@@ -855,17 +828,16 @@ impl RuntimeId {
}
}
pub(crate) fn create_concrete_signal(self, value: Box<dyn Any>) -> NodeId {
pub(crate) fn create_concrete_signal(
self,
value: Rc<RefCell<dyn Any>>,
) -> NodeId {
with_runtime(|runtime| {
let id = runtime.arena.insert_boxed(value);
runtime.nodes.borrow_mut().insert(
id,
ReactiveNode {
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Signal,
},
);
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
value: Some(value),
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Signal,
});
runtime.push_scope_property(ScopeProperty::Signal(id));
id
})
@@ -881,7 +853,9 @@ impl RuntimeId {
where
T: Any + 'static,
{
let id = self.create_concrete_signal(Box::new(value));
let id = self.create_concrete_signal(
Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>
);
(
ReadSignal {
@@ -905,7 +879,9 @@ impl RuntimeId {
where
T: Any + 'static,
{
let id = self.create_concrete_signal(Box::new(value));
let id = self.create_concrete_signal(
Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>
);
RwSignal {
id,
ty: PhantomData,
@@ -916,20 +892,17 @@ impl RuntimeId {
pub(crate) fn create_concrete_effect(
self,
value: Box<dyn Any>,
value: Rc<RefCell<dyn Any>>,
effect: Rc<dyn AnyComputation>,
) -> NodeId {
with_runtime(|runtime| {
let id = runtime.arena.insert_boxed(value);
runtime.nodes.borrow_mut().insert(
id,
ReactiveNode {
state: ReactiveNodeState::Dirty,
node_type: ReactiveNodeType::Effect {
f: Rc::clone(&effect),
},
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
value: Some(Rc::clone(&value)),
state: ReactiveNodeState::Dirty,
node_type: ReactiveNodeType::Effect {
f: Rc::clone(&effect),
},
);
});
runtime.push_scope_property(ScopeProperty::Effect(id));
id
})
@@ -938,20 +911,17 @@ impl RuntimeId {
pub(crate) fn create_concrete_memo(
self,
value: Box<dyn Any>,
value: Rc<RefCell<dyn Any>>,
computation: Rc<dyn AnyComputation>,
) -> NodeId {
with_runtime(|runtime| {
let id = runtime.arena.insert_boxed(value);
runtime.nodes.borrow_mut().insert(
id,
ReactiveNode {
// 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: computation },
},
);
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
value: Some(value),
// 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: computation },
});
runtime.push_scope_property(ScopeProperty::Effect(id));
id
})
@@ -968,7 +938,7 @@ impl RuntimeId {
T: Any + 'static,
{
self.create_concrete_effect(
Box::new(None::<T>),
Rc::new(RefCell::new(None::<T>)),
Rc::new(EffectState {
f,
ty: PhantomData,
@@ -1032,7 +1002,7 @@ impl RuntimeId {
};
let id = self.create_concrete_effect(
Box::new(None::<()>),
Rc::new(RefCell::new(None::<()>)),
Rc::new(EffectState {
f: effect_fn,
ty: PhantomData,
@@ -1043,9 +1013,8 @@ impl RuntimeId {
(id, move || {
with_runtime(|runtime| {
ThreadArena::remove(&id);
runtime.nodes.borrow_mut().remove(&id);
runtime.node_sources.borrow_mut().remove(&id);
runtime.nodes.borrow_mut().remove(id);
runtime.node_sources.borrow_mut().remove(id);
})
.expect(
"tried to stop a watch in a runtime that has been disposed",
@@ -1064,7 +1033,7 @@ impl RuntimeId {
{
Memo {
id: self.create_concrete_memo(
Box::new(None::<T>),
Rc::new(RefCell::new(None::<T>)),
Rc::new(MemoState {
f,
t: PhantomData,
@@ -1081,20 +1050,16 @@ impl RuntimeId {
impl Runtime {
pub fn new() -> Self {
let mut arena = ArenaOwner::default();
let mut nodes: FxHashMap<NodeId, ReactiveNode> = Default::default();
let root_id = arena.insert(None::<()>);
nodes.insert(
root_id,
ReactiveNode {
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Trigger,
},
);
let root = ReactiveNode {
value: None,
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Trigger,
};
let mut nodes: SlotMap<NodeId, ReactiveNode> = SlotMap::default();
let root_id = nodes.insert(root);
Self {
owner: Cell::new(Some(root_id)),
arena,
nodes: RefCell::new(nodes),
..Self::default()
}
@@ -1194,6 +1159,15 @@ impl Runtime {
}
f
}
/// Do not call on triggers
pub(crate) fn get_value(
&self,
node_id: NodeId,
) -> Option<Rc<RefCell<dyn Any>>> {
let signals = self.nodes.borrow();
signals.get(node_id).map(|node| node.value())
}
}
impl PartialEq for Runtime {
@@ -1285,7 +1259,7 @@ fn push_cleanup(cleanup_fn: Box<dyn FnOnce()>) {
_ = with_runtime(|runtime| {
if let Some(owner) = runtime.owner.get() {
let mut cleanups = runtime.on_cleanups.borrow_mut();
if let Some(entries) = cleanups.get_mut(&owner) {
if let Some(entries) = cleanups.get_mut(owner) {
entries.push(cleanup_fn);
} else {
cleanups.insert(owner, vec![cleanup_fn]);
@@ -1300,7 +1274,7 @@ pub(crate) enum ScopeProperty {
Signal(NodeId),
Effect(NodeId),
Resource(ResourceId),
StoredValue(NodeId),
StoredValue(StoredValueId),
}
impl ScopeProperty {

View File

@@ -1,20 +1,19 @@
use crate::{
arena::NodeId,
console_warn, create_effect, diagnostics,
diagnostics::*,
macros::debug_warn,
on_cleanup, queue_microtask,
runtime::{with_runtime, ThreadArena},
console_warn, create_effect, diagnostics, diagnostics::*,
macros::debug_warn, node::NodeId, on_cleanup, runtime::with_runtime,
Runtime,
};
use futures::Stream;
use std::{
cell::{Ref, RefMut},
any::Any,
cell::RefCell,
fmt,
hash::{Hash, Hasher},
marker::PhantomData,
pin::Pin,
rc::Rc,
};
use thiserror::Error;
macro_rules! impl_get_fn_traits {
($($ty:ident $(($method_name:ident))?),*) => {
@@ -104,43 +103,6 @@ pub mod prelude {
};
}
/// This trait allows getting a reference to a signal's inner type.
pub trait SignalRead {
/// The value held by the signal.
type Value;
/// Returns a [Ref] to the current value held by the signal, and subscribes
/// the running effect to this signal.
///
/// # Panics
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
#[track_caller]
fn read(&self) -> Ref<'_, Self::Value>;
/// Returns a [Ref] to the current value held by the signal, and subscribes
/// the running effect to this signal, returning [`Some`] if the signal
/// is still alive, and [`None`] otherwise.
fn try_read(&self) -> Option<Ref<'_, Self::Value>>;
}
/// This trait allows getting a mutable reference to a signal's inner type.
pub trait SignalWrite {
/// The value held by the signal.
type Value;
/// Returns a [RefMut] to the current value held by the signal,
/// and notifies subscribers that the signal has changed.
///
/// # Panics
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
#[track_caller]
fn write(&self) -> RefMut<'_, Self::Value>;
/// Returns a [Ref] to the current value held by the signal, and notifies subscribers that the
/// signal has changed, returning [`Some`] if the signal is still alive, and [`None`] otherwise.
fn try_write(&self) -> Option<RefMut<'_, Self::Value>>;
}
/// This trait allows getting an owned value of the signals
/// inner type.
pub trait SignalGet {
@@ -479,24 +441,6 @@ where
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T> SignalRead for ReadSignal<T> {
type Value = T;
fn read(&self) -> Ref<'_, Self::Value> {
self.id.read(
#[cfg(debug_assesrtions)]
self.defined_at,
)
}
fn try_read(&self) -> Option<Ref<'_, Self::Value>> {
self.id.try_read(
#[cfg(debug_assertions)]
self.defined_at,
)
}
}
impl<T: Clone> SignalGetUntracked for ReadSignal<T> {
type Value = T;
@@ -519,8 +463,8 @@ impl<T: Clone> SignalGetUntracked for ReadSignal<T> {
})
.expect("runtime to be alive")
{
Some(t) => t,
None => panic_getting_dead_signal(
Ok(t) => t,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
@@ -543,7 +487,7 @@ impl<T: Clone> SignalGetUntracked for ReadSignal<T> {
#[track_caller]
fn try_get_untracked(&self) -> Option<T> {
with_runtime(|runtime| {
self.id.try_with_no_subscription(runtime, Clone::clone)
self.id.try_with_no_subscription(runtime, Clone::clone).ok()
})
.ok()
.flatten()
@@ -591,7 +535,7 @@ impl<T> SignalWithUntracked for ReadSignal<T> {
match with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
{
Ok(Some(o)) => Some(o),
Ok(Ok(o)) => Some(o),
_ => None,
}
}
@@ -639,8 +583,8 @@ impl<T> SignalWith for ReadSignal<T> {
match with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
.expect("runtime to be alive")
{
Some(o) => o,
None => panic_getting_dead_signal(
Ok(o) => o,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
@@ -665,7 +609,7 @@ impl<T> SignalWith for ReadSignal<T> {
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics).ok())
.ok()
.flatten()
}
@@ -709,8 +653,8 @@ impl<T: Clone> SignalGet for ReadSignal<T> {
})
.expect("runtime to be alive")
{
Some(t) => t,
None => panic_getting_dead_signal(
Ok(t) => t,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
@@ -731,7 +675,7 @@ impl<T: Clone> SignalGet for ReadSignal<T> {
)
)]
fn try_get(&self) -> Option<T> {
self.try_with(Clone::clone)
self.try_with(Clone::clone).ok()
}
}
@@ -784,11 +728,21 @@ where
self.id
.try_with_no_subscription_by_id(f)
.unwrap_or_else(|| {
panic_getting_dead_signal(
#[cfg(debug_assertions)]
self.defined_at,
)
.unwrap_or_else(|_| {
#[cfg(not(debug_assertions))]
{
panic!("tried to access ReadSignal that has been disposed")
}
#[cfg(debug_assertions)]
{
panic!(
"at {}, tried to access ReadSignal<{}> defined at {}, \
but it has already been disposed",
caller,
std::any::type_name::<T>(),
self.defined_at
)
}
})
}
@@ -796,13 +750,17 @@ where
/// the running effect.
#[track_caller]
#[inline(always)]
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
pub(crate) fn try_with<U>(
&self,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError> {
let diagnostics = diagnostics!(self);
match with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
{
Ok(Some(v)) => Some(v),
_ => None,
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(e),
Err(_) => Err(SignalError::RuntimeDisposed),
}
}
}
@@ -897,24 +855,6 @@ where
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T> SignalWrite for WriteSignal<T> {
type Value = T;
fn write(&self) -> RefMut<'_, Self::Value> {
self.id.write(
#[cfg(debug_assertions)]
self.defined_at,
)
}
fn try_write(&self) -> Option<RefMut<'_, Self::Value>> {
self.id.try_write(
#[cfg(debug_assertions)]
self.defined_at,
)
}
}
impl<T> SignalSetUntracked<T> for WriteSignal<T>
where
T: 'static,
@@ -1334,11 +1274,21 @@ impl<T: Clone> SignalGetUntracked for RwSignal<T> {
self.id
.try_with_no_subscription_by_id(Clone::clone)
.unwrap_or_else(|| {
panic_getting_dead_signal(
#[cfg(debug_assertions)]
self.defined_at,
)
.unwrap_or_else(|_| {
#[cfg(not(debug_assertions))]
{
panic!("tried to access RwSignal that has been disposed")
}
#[cfg(debug_assertions)]
{
panic!(
"at {}, tried to access RwSignal<{}> defined at {}, \
but it has already been disposed",
caller,
std::any::type_name::<T>(),
self.defined_at
)
}
})
}
@@ -1358,7 +1308,7 @@ impl<T: Clone> SignalGetUntracked for RwSignal<T> {
#[track_caller]
fn try_get_untracked(&self) -> Option<T> {
with_runtime(|runtime| {
self.id.try_with_no_subscription(runtime, Clone::clone)
self.id.try_with_no_subscription(runtime, Clone::clone).ok()
})
.ok()
.flatten()
@@ -1385,11 +1335,20 @@ impl<T> SignalWithUntracked for RwSignal<T> {
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
self.id
.try_with_no_subscription_by_id(f)
.unwrap_or_else(|| {
panic_getting_dead_signal(
#[cfg(debug_assertions)]
self.defined_at,
)
.unwrap_or_else(|_| {
#[cfg(not(debug_assertions))]
{
panic!("tried to access RwSignal that has been disposed")
}
#[cfg(debug_assertions)]
{
panic!(
"tried to access RwSignal<{}> defined at {}, but it \
has already been disposed",
std::any::type_name::<T>(),
self.defined_at
)
}
})
}
@@ -1413,7 +1372,7 @@ impl<T> SignalWithUntracked for RwSignal<T> {
match with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
{
Ok(Some(o)) => Some(o),
Ok(Ok(o)) => Some(o),
_ => None,
}
}
@@ -1559,8 +1518,8 @@ impl<T> SignalWith for RwSignal<T> {
match with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
.expect("runtime to be alive")
{
Some(o) => o,
None => panic_getting_dead_signal(
Ok(o) => o,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
@@ -1585,7 +1544,7 @@ impl<T> SignalWith for RwSignal<T> {
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics).ok())
.ok()
.flatten()
}
@@ -1633,8 +1592,8 @@ impl<T: Clone> SignalGet for RwSignal<T> {
})
.expect("runtime to be alive")
{
Some(t) => t,
None => panic_getting_dead_signal(
Ok(t) => t,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
@@ -1659,7 +1618,7 @@ impl<T: Clone> SignalGet for RwSignal<T> {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| {
self.id.try_with(runtime, Clone::clone, diagnostics)
self.id.try_with(runtime, Clone::clone, diagnostics).ok()
})
.ok()
.flatten()
@@ -1981,6 +1940,16 @@ impl<T> RwSignal<T> {
}
}
#[derive(Debug, Error)]
pub(crate) enum SignalError {
#[error("tried to access a signal in a runtime that had been disposed")]
RuntimeDisposed,
#[error("tried to access a signal that had been disposed")]
Disposed,
#[error("error casting signal to type {0}")]
Type(&'static str),
}
impl NodeId {
#[track_caller]
pub(crate) fn subscribe(
@@ -1992,15 +1961,16 @@ impl NodeId {
if let Some(observer) = runtime.observer.get() {
// add this observer to this node's dependencies (to allow notification)
let mut subs = runtime.node_subscribers.borrow_mut();
subs.entry(*self).or_default().borrow_mut().insert(observer);
if let Some(subs) = subs.entry(*self) {
subs.or_default().borrow_mut().insert(observer);
}
// add this node to the observer's sources (to allow cleanup)
let mut sources = runtime.node_sources.borrow_mut();
sources
.entry(observer)
.or_default()
.borrow_mut()
.insert(*self);
if let Some(sources) = sources.entry(observer) {
let sources = sources.or_default();
sources.borrow_mut().insert(*self);
}
} else {
#[cfg(all(debug_assertions, not(feature = "ssr")))]
{
@@ -2030,11 +2000,21 @@ impl NodeId {
}
}
fn try_with_no_subscription_inner(
&self,
runtime: &Runtime,
) -> Result<Rc<RefCell<dyn Any>>, SignalError> {
runtime.update_if_necessary(*self);
let nodes = runtime.nodes.borrow();
let node = nodes.get(*self).ok_or(SignalError::Disposed)?;
Ok(node.value())
}
#[inline(always)]
pub(crate) fn try_with_no_subscription_by_id<T, U>(
&self,
f: impl FnOnce(&T) -> U,
) -> Option<U>
) -> Result<U, SignalError>
where
T: 'static,
{
@@ -2048,13 +2028,17 @@ impl NodeId {
&self,
runtime: &Runtime,
f: impl FnOnce(&T) -> U,
) -> Option<U>
) -> Result<U, SignalError>
where
T: 'static,
{
runtime.update_if_necessary(*self);
let value = ThreadArena::get::<T>(self)?;
Some(f(&value))
let value = self.try_with_no_subscription_inner(runtime)?;
let value = value.borrow();
let value = value
.downcast_ref::<T>()
.ok_or_else(|| SignalError::Type(std::any::type_name::<T>()))
.expect("to downcast signal type");
Ok(f(value))
}
#[track_caller]
@@ -2064,11 +2048,12 @@ impl NodeId {
runtime: &Runtime,
f: impl FnOnce(&T) -> U,
diagnostics: AccessDiagnostics,
) -> Option<U>
) -> Result<U, SignalError>
where
T: 'static,
{
self.subscribe(runtime, diagnostics);
self.try_with_no_subscription(runtime, f)
}
@@ -2076,6 +2061,7 @@ impl NodeId {
#[track_caller]
fn update_value<T, U>(
&self,
f: impl FnOnce(&mut T) -> U,
#[cfg(debug_assertions)] defined_at: Option<
&'static std::panic::Location<'static>,
@@ -2088,8 +2074,18 @@ impl NodeId {
let location = std::panic::Location::caller();
with_runtime(|runtime| {
if let Some(ref mut value) = ThreadArena::get_mut::<T>(self) {
Some(f(value))
if let Some(value) = runtime.get_value(*self) {
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 {
#[cfg(debug_assertions)]
{
@@ -2113,73 +2109,6 @@ impl NodeId {
.unwrap_or_default()
}
pub(crate) fn try_read<T: 'static>(
&self,
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<
'static,
>,
) -> Option<Ref<'_, T>> {
with_runtime(|runtime| {
let diagnostics = diagnostics!(self);
self.subscribe(runtime, diagnostics);
});
ThreadArena::get::<T>(self)
}
pub(crate) fn read<T: 'static>(
&self,
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<
'static,
>,
) -> Ref<'_, T> {
self.try_read(
#[cfg(debug_assertions)]
defined_at,
)
.unwrap_or_else(|| {
panic_getting_dead_signal(
#[cfg(debug_assertions)]
defined_at,
)
})
}
pub(crate) fn try_write<T: 'static>(
&self,
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<
'static,
>,
) -> Option<RefMut<'_, T>> {
// delay notification a tick, until you've hopefully done whatever work
let id = *self;
queue_microtask(move || {
with_runtime(|runtime| {
runtime.mark_dirty(id);
runtime.run_effects();
});
});
ThreadArena::get_mut::<T>(self)
}
pub(crate) fn write<T: 'static>(
&self,
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<
'static,
>,
) -> RefMut<'_, T> {
self.try_write(
#[cfg(debug_assertions)]
defined_at,
)
.unwrap_or_else(|| {
panic_getting_dead_signal(
#[cfg(debug_assertions)]
defined_at,
)
})
}
#[inline(always)]
#[track_caller]
pub(crate) fn update<T, U>(
@@ -2196,10 +2125,18 @@ impl NodeId {
let location = std::panic::Location::caller();
with_runtime(|runtime| {
let updated = if let Some(ref mut value) =
ThreadArena::get_mut::<T>(self)
{
Some(f(value))
let updated = if let Some(value) = runtime.get_value(*self) {
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 {
#[cfg(debug_assertions)]
{

View File

@@ -273,7 +273,7 @@ impl<T> SignalWith for Signal<T> {
)]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
match self.inner {
SignalTypes::ReadSignal(r) => r.try_with(f),
SignalTypes::ReadSignal(r) => r.try_with(f).ok(),
SignalTypes::Memo(m) => m.try_with(f),
SignalTypes::DerivedSignal(s) => s.try_with_value(|t| f(&t())),

View File

@@ -1,12 +1,17 @@
use crate::{
arena::NodeId, runtime::ThreadArena, with_runtime, Runtime, ScopeProperty,
};
use crate::{with_runtime, Runtime, ScopeProperty};
use std::{
cell::RefCell,
fmt,
hash::{Hash, Hasher},
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`].
///
/// If you want a reactive wrapper, use [`create_signal`](crate::create_signal).
@@ -20,7 +25,7 @@ pub struct StoredValue<T>
where
T: 'static,
{
id: NodeId,
id: StoredValueId,
ty: PhantomData<T>,
}
@@ -84,9 +89,7 @@ impl<T> StoredValue<T> {
where
T: Clone,
{
ThreadArena::get::<T>(&self.id)
.expect("could not get stored value")
.clone()
self.try_get_value().expect("could not get stored value")
}
/// Same as [`StoredValue::get_value`] but will not panic by default.
@@ -95,7 +98,7 @@ impl<T> StoredValue<T> {
where
T: Clone,
{
ThreadArena::get::<T>(&self.id).map(|n| n.clone())
self.try_with_value(T::clone)
}
/// Applies a function to the current stored value and returns the result.
@@ -127,8 +130,17 @@ impl<T> StoredValue<T> {
/// Same as [`StoredValue::with_value`] but returns [`Some(O)]` only if
/// the stored value has not yet been disposed. [`None`] otherwise.
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let value = ThreadArena::get(&self.id)?;
Some(f(&value))
with_runtime(|runtime| {
let value = {
let values = runtime.stored_values.borrow();
values.get(self.id)?.clone()
};
let value = value.borrow();
let value = value.downcast_ref::<T>()?;
Some(f(value))
})
.ok()
.flatten()
}
/// Updates the stored value.
@@ -178,8 +190,17 @@ impl<T> StoredValue<T> {
/// Same as [`Self::update_value`], but returns [`Some(O)`] if the
/// stored value has not yet been disposed, [`None`] otherwise.
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
let mut value = ThreadArena::get_mut::<T>(&self.id)?;
Some(f(&mut *value))
with_runtime(|runtime| {
let value = {
let values = runtime.stored_values.borrow();
values.get(self.id)?.clone()
};
let mut value = value.borrow_mut();
let value = value.downcast_mut::<T>()?;
Some(f(value))
})
.ok()
.flatten()
}
/// Sets the stored value.
@@ -205,13 +226,27 @@ impl<T> StoredValue<T> {
/// Same as [`Self::set_value`], but returns [`None`] if the
/// stored value has not yet been disposed, [`Some(T)`] otherwise.
pub fn try_set_value(&self, value: T) -> Option<T> {
match ThreadArena::get_mut::<T>(&self.id) {
Some(mut curr) => {
*curr = value;
None
with_runtime(|runtime| {
let n = {
let values = runtime.stored_values.borrow();
values.get(self.id).map(Rc::clone)
};
if let Some(n) = n {
let mut n = n.borrow_mut();
let n = n.downcast_mut::<T>();
if let Some(n) = n {
*n = value;
None
} else {
Some(value)
}
} else {
Some(value)
}
None => Some(value),
}
})
.ok()
.flatten()
}
}
@@ -259,7 +294,10 @@ where
T: 'static,
{
let id = with_runtime(|runtime| {
let id = runtime.arena.insert(value);
let id = runtime
.stored_values
.borrow_mut()
.insert(Rc::new(RefCell::new(value)));
runtime.push_scope_property(ScopeProperty::StoredValue(id));
id
})

View File

@@ -1,7 +1,7 @@
use crate::{
arena::NodeId,
diagnostics,
diagnostics::*,
node::NodeId,
runtime::{with_runtime, Runtime},
SignalGet, SignalSet, SignalUpdate,
};