Compare commits

..

5 Commits

Author SHA1 Message Date
Greg Johnston
9f704f91fe Use local observers 2023-01-27 13:52:59 -05:00
Greg Johnston
4b0e212f1c Rc/RefCell => Arc/RwLock 2023-01-27 13:07:34 -05:00
Greg Johnston
8fa89cffef Make tests work on stable 2023-01-27 12:54:44 -05:00
Greg Johnston
18ceb4c4f8 Remove unnecessary log dependency 2023-01-27 12:54:11 -05:00
Greg Johnston
0d3e2baa9b remove unused base64 dependency 2023-01-27 12:52:08 -05:00
34 changed files with 372 additions and 443 deletions

View File

@@ -24,17 +24,17 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.1.3"
version = "0.1.1"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.1.3" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.3" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.3" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.3" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.3" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.3" }
leptos_router = { path = "./router", version = "0.1.3" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.3" }
leptos = { path = "./leptos", default-features = false, version = "0.1.1" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.1" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.1" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.1" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.1" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.1" }
leptos_router = { path = "./router", version = "0.1.1" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.1" }
[profile.release]
codegen-units = 1

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

@@ -1,5 +1,5 @@
use leptos::*;
fn main() {
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
}

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

@@ -4,9 +4,7 @@ fn main() {
mount_to_body(|cx| {
let name = "gbj";
let userid = 0;
// This will be filled by _ref=input below.
let input_element = NodeRef::<HtmlElement<Input>>::new(cx);
let _input_element: Element;
view! {
cx,
@@ -19,7 +17,7 @@ fn main() {
prop:value="todo" // `prop:` lets you set a property on a DOM node
value="initial" // side note: the DOM `value` attribute only sets *initial* value
// this is very important when working with forms!
_ref=input_element // `_ref` stores tis element in a variable
_ref=_input_element // `_ref` stores tis element in a variable
/>
<ul data-user=userid> // attributes can take expressions as values
<li class="todo my-todo" // here we set the `class` attribute

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

@@ -6,7 +6,7 @@ const APP_ID: &str = "dev.leptos.Counter";
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
fn main() {
_ = create_scope(create_runtime(), |cx| {
_ = create_scope(|cx| {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();

View File

@@ -11,7 +11,6 @@ console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
web-sys = { version = "0.3", features = ["Storage"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -2,29 +2,6 @@ use leptos_dom::{Errors, Fragment, IntoView};
use leptos_macro::component;
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
/// This component lets you define a fallback that should be rendered in that
/// error case, allowing you to handle errors within a section of the interface.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, Ok(0));
/// let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
///
/// view! { cx,
/// <input type="text" on:input=on_input/>
/// <ErrorBoundary
/// fallback=move |_, _| view! { cx, <p class="error">"Enter a valid number."</p>}
/// >
/// <p>"Value is: " {value}</p>
/// </ErrorBoundary>
/// }
/// # });
/// ```
#[component(transparent)]
pub fn ErrorBoundary<F, IV>(
cx: Scope,

View File

@@ -3,27 +3,8 @@ use leptos_dom::{Fragment, IntoView};
use leptos_reactive::Scope;
use once_cell::sync::Lazy;
/// A component that will show its children when the `when` condition is `true`,
/// and show the fallback when it is `false`, without rerendering every time
/// the condition changes.
///
/// ```rust
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, 0);
///
/// view! { cx,
/// <Show
/// when=move || value() < 5
/// fallback=|cx| view! { cx, "Big number!" }
/// >
/// "Small number!"
/// </Show>
/// }
/// # });
/// ```
/// A component that will show it's children when the passed in closure is True, and show the fallback
/// when the closure is false
#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in

View File

@@ -12,6 +12,7 @@ cfg-if = "1"
drain_filter_polyfill = "0.1"
educe = "0.4"
futures = "0.3"
gloo = { version = "0.8", features = ["futures"] }
html-escape = "0.2"
indexmap = "1.9"
itertools = "0.10"
@@ -33,11 +34,8 @@ leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
"Comment",
"Document",
"DomTokenList",
"Location",
"Range",
"Text",
"HtmlCollection",
@@ -51,7 +49,6 @@ features = [
"DeviceMotionEvent",
"DeviceOrientationEvent",
"DragEvent",
"ErrorEvent",
"FocusEvent",
"GamepadEvent",
"HashChangeEvent",

View File

@@ -19,28 +19,23 @@ where
match use_context::<RwSignal<Errors>>(cx) {
Some(errors) => {
let id = HydrationCtx::id();
errors.update({
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let id = id.clone();
move |errors: &mut Errors| errors.insert(id, error)
});
errors.update(move |errors: &mut Errors| errors.insert(id, error));
// remove the error from the list if this drops,
// i.e., if it's in a DynChild that switches from Err to Ok
// Only can run on the client, will panic on the server
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
crate::log!("removing error at {id}");
errors.remove::<E>(&id);
});
});
if #[cfg(any(feature = "hydrate", feature="csr"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
});
});
});
}
}
}
}
None => {
#[cfg(debug_assertions)]

View File

@@ -6,8 +6,12 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use crate::events::*;
use crate::macro_helpers::*;
use crate::macro_helpers::Property;
use crate::macro_helpers::{
attribute_expression, class_expression, property_expression,
};
use crate::{mount_child, MountKind};
use leptos_reactive::create_render_effect;
use once_cell::unsync::Lazy as LazyCell;
use wasm_bindgen::JsCast;
@@ -42,7 +46,7 @@ cfg_if! {
use crate::{
ev::EventDescriptor,
hydration::HydrationCtx,
macro_helpers::{IntoAttribute, IntoClass, IntoProperty},
macro_helpers::{Attribute, Class, IntoAttribute, IntoClass, IntoProperty},
Element, Fragment, IntoView, NodeRef, Text, View,
};
use leptos_reactive::Scope;
@@ -199,8 +203,10 @@ impl Custom {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
crate::document().create_element(&name).unwrap()
@@ -489,18 +495,26 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
attribute_helper(
self.element.as_ref(),
name,
attr.into_attribute(self.cx),
);
let el = self.element.as_ref();
let value = attr.into_attribute(self.cx);
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
new
});
}
_ => attribute_expression(el, &name, value),
};
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Attribute;
let mut this = self;
let mut attr = attr.into_attribute(this.cx);
@@ -540,16 +554,26 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
let el = self.element.as_ref();
let class_list = el.class_list();
let value = class.into_class(self.cx);
class_helper(el, name, value);
match value {
Class::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new)
}
new
});
}
Class::Value(value) => class_expression(&class_list, &name, value),
};
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Class;
let mut this = self;
let class = class.into_class(this.cx);
@@ -585,7 +609,25 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
let name = name.into();
let value = value.into_property(self.cx);
let el = self.element.as_ref();
property_helper(el, name, value);
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
if old.as_ref() != Some(&new)
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
@@ -806,18 +848,63 @@ macro_rules! generate_html_tags {
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let element = create_leptos_element(
&stringify!([<$tag:upper>]),
id,
|| {
let element = if HydrationCtx::is_hydrating() {
if let Some(el) = crate::document().get_element_by_id(
&format!("_{id}")
) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper>]),
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("id").unwrap();
el.unchecked_into()
} else if let Ok(Some(el)) = crate::document().query_selector(
&format!("[leptos-hk=_{id}]")
) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper>]),
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();
el.unchecked_into()
} else {
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
[<$tag:upper>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
} else {
[<$tag:upper>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
);
};
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -892,70 +979,24 @@ macro_rules! generate_html_tags {
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn create_leptos_element(
tag: &str,
id: crate::HydrationKey,
clone_element: fn() -> web_sys::HtmlElement,
) -> web_sys::HtmlElement {
if HydrationCtx::is_hydrating() {
if let Some(el) = crate::document().get_element_by_id(&format!("_{id}")) {
#[cfg(debug_assertions)]
assert_eq!(
&el.node_name().to_ascii_uppercase(),
tag,
"SSR and CSR elements have the same `TopoId` but different node \
kinds. This is either a discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it can also be a leptos \
hydration issue."
);
el.remove_attribute("id").unwrap();
el.unchecked_into()
} else if let Ok(Some(el)) =
crate::document().query_selector(&format!("[leptos-hk=_{id}]"))
{
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
tag,
"SSR and CSR elements have the same `TopoId` but different node \
kinds. This is either a discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it can also be a leptos \
hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();
el.unchecked_into()
} else {
crate::warn!("element with id {id} not found, ignoring it for hydration");
clone_element()
}
} else {
clone_element()
}
}
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
fn warn_on_ambiguous_a(parent: &web_sys::Element, child: &View) {
if let View::Element(el) = &child {
if (el.name == "a"
if el.name == "a"
|| el.name == "script"
|| el.name == "style"
|| el.name == "title")
&& parent.namespace_uri() != el.element.namespace_uri()
|| el.name == "title"
{
crate::warn!(
"Warning: you are appending an SVG element to an HTML element, or an \
HTML element to an SVG. Typically, this occurs when you create an \
<a/> or <script/> with the `view` macro and append it to an SVG, but \
the framework assumed it was HTML when you created it. To specify \
that it is an SVG element, use <svg::{{tag name}}/> in the view \
macro."
)
if parent.namespace_uri() != el.element.namespace_uri() {
crate::warn!(
"Warning: you are appending an SVG element to an HTML element, or \
an HTML element to an SVG. Typically, this occurs when you create \
an <a/> or <script/> with the `view` macro and append it to an \
SVG, but the framework assumed it was HTML when you created it. To \
specify that it is an SVG element, use <svg::{{tag name}}/> in the \
view macro."
)
}
}
}
}

View File

@@ -95,8 +95,10 @@ macro_rules! generate_math_tags {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]

View File

@@ -92,8 +92,10 @@ macro_rules! generate_svg_tags {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]

View File

@@ -21,7 +21,7 @@ cfg_if! {
while let Ok(Some(node)) = walker.next_node() {
if let Some(content) = node.text_content() {
if let Some(hk) = content.strip_prefix("hk=") {
if let Some(hk) = hk.split('|').next() {
if let Some(hk) = hk.split("|").next() {
map.insert(hk.into(), node.unchecked_into());
}
}

View File

@@ -308,8 +308,10 @@ impl Comment {
marker.remove();
} else {
crate::warn!(
"component with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"component with id",
id,
"not found, ignoring it for hydration"
);
}
}

View File

@@ -169,31 +169,7 @@ attr_type!(f64);
attr_type!(char);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Attribute,
) {
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
new
});
}
_ => attribute_expression(el, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_expression(
pub fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,
value: Attribute,

View File

@@ -67,33 +67,7 @@ impl<T: IntoClass> IntoClass for (Scope, T) {
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Class,
) {
use leptos_reactive::create_render_effect;
let class_list = el.class_list();
match value {
Class::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new)
}
new
});
}
Class::Value(value) => class_expression(&class_list, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_expression(
pub fn class_expression(
class_list: &web_sys::DomTokenList,
class_name: &str,
value: bool,

View File

@@ -77,39 +77,7 @@ prop_type!(f64);
prop_type!(bool);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn property_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Property,
) {
use leptos_reactive::create_render_effect;
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
if old.as_ref() != Some(&new)
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn property_expression(
pub fn property_expression(
el: &web_sys::Element,
prop_name: &str,
value: JsValue,

View File

@@ -8,16 +8,16 @@ repository = "https://github.com/leptos-rs/leptos"
description = "Reactive system for the Leptos web framework."
[dependencies]
log = "0.4"
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.3", optional = true }
futures = { version = "0.3" }
js-sys = "0.3"
lazy_static = "1"
miniserde = { version = "0.1", optional = true }
parking_lot = "0.12"
serde-wasm-bindgen = "0.4"
serde_json = "1"
base64 = "0.21"
thiserror = "1"
tokio = { version = "1", features = ["rt"], optional = true }
tracing = "0.1"

View File

@@ -57,7 +57,7 @@ where
let id = value.type_id();
_ = with_runtime(cx.runtime, |runtime| {
let mut contexts = runtime.scope_contexts.borrow_mut();
let mut contexts = runtime.scope_contexts.write();
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
context.insert(id, Box::new(value) as Box<dyn Any>);
});
@@ -116,7 +116,7 @@ where
let id = TypeId::of::<T>();
with_runtime(cx.runtime, |runtime| {
let local_value = {
let contexts = runtime.scope_contexts.borrow();
let contexts = runtime.scope_contexts.read();
let context = contexts.get(cx.id);
context
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
@@ -124,16 +124,12 @@ where
};
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,
})
}),
None => runtime.scope_parents.read().get(cx.id).and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
}
})
.ok()

View File

@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
use crate::runtime::{with_runtime, RuntimeId};
use crate::runtime::{with_runtime, LocalObserver, RuntimeId};
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::cell::RefCell;
use parking_lot::RwLock;
use std::fmt::Debug;
/// Effects run a certain chunk of code whenever the signals they depend on change.
@@ -23,7 +23,6 @@ use std::fmt::Debug;
/// the server, use [create_isomorphic_effect].
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
@@ -31,7 +30,7 @@ use std::fmt::Debug;
/// // ✅ use effects to interact between reactive state and the outside world
/// create_effect(cx, move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a());
/// println!("Value: {}", a());
/// });
///
/// set_a(1);
@@ -80,7 +79,6 @@ where
/// the server as well as the client.
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
@@ -88,7 +86,7 @@ where
/// // ✅ use effects to interact between reactive state and the outside world
/// create_isomorphic_effect(cx, move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a());
/// println!("Value: {}", a());
/// });
///
/// set_a(1);
@@ -152,7 +150,7 @@ where
F: Fn(Option<T>) -> T,
{
pub(crate) f: F,
pub(crate) value: RefCell<Option<T>>,
pub(crate) value: RwLock<Option<T>>,
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
@@ -179,22 +177,22 @@ where
)
)
)]
fn run(&self, id: EffectId, runtime: RuntimeId) {
_ = with_runtime(runtime, |runtime| {
fn run(&self, id: EffectId, runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
// clear previous dependencies
id.cleanup(runtime);
// set this as the current observer
let prev_observer = runtime.observer.take();
runtime.observer.set(Some(id));
let prev_observer = LocalObserver::take(runtime_id);
LocalObserver::set(runtime_id, Some(id));
// run the effect
let value = self.value.take();
let value = std::mem::take(&mut *self.value.write());
let new_value = (self.f)(value);
*self.value.borrow_mut() = Some(new_value);
*self.value.write() = Some(new_value);
// restore the previous observer
runtime.observer.set(prev_observer);
LocalObserver::set(runtime_id, prev_observer);
})
}
}
@@ -203,7 +201,7 @@ impl EffectId {
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
let effect = {
let effects = runtime.effects.borrow();
let effects = runtime.effects.read();
effects.get(*self).cloned()
};
if let Some(effect) = effect {
@@ -226,12 +224,12 @@ impl EffectId {
)
)]
pub(crate) fn cleanup(&self, runtime: &Runtime) {
let sources = runtime.effect_sources.borrow();
let sources = runtime.effect_sources.read();
if let Some(sources) = sources.get(*self) {
let subs = runtime.signal_subscribers.borrow();
for source in sources.borrow().iter() {
let subs = runtime.signal_subscribers.read();
for source in sources.read().iter() {
if let Some(source) = subs.get(*source) {
source.borrow_mut().remove(self);
source.write().remove(self);
}
}
}

View File

@@ -146,7 +146,7 @@ macro_rules! debug_warn {
{
#[cfg(debug_assertions)]
{
log::warn!($($x)*)
tracing::warn!($($x)*)
}
#[cfg(not(debug_assertions))]
{ }

View File

@@ -33,7 +33,7 @@ use std::fmt::Debug;
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
/// create_effect(cx, move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// println!("expensive = {}", expensive());
/// });
/// create_effect(cx, move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
@@ -46,7 +46,7 @@ use std::fmt::Debug;
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// log::debug!("memoized = {}", memoized());
/// println!("memoized = {}", memoized());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**
@@ -103,7 +103,7 @@ where
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
/// create_effect(cx, move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// println!("expensive = {}", expensive());
/// });
/// create_effect(cx, move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
@@ -116,7 +116,7 @@ where
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// log::debug!("memoized = {}", memoized());
/// println!("memoized = {}", memoized());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**

View File

@@ -6,15 +6,10 @@ use crate::{
spawn::spawn_local,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
};
use parking_lot::RwLock;
use std::{
any::Any,
cell::{Cell, RefCell},
collections::HashSet,
fmt::Debug,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
any::Any, collections::HashSet, fmt::Debug, future::Future, marker::PhantomData, pin::Pin,
sync::Arc,
};
/// Creates [Resource](crate::Resource), which is a signal that reflects the
@@ -113,10 +108,10 @@ where
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let fetcher = Arc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
let r = Arc::new(ResourceState {
scope: cx,
value,
set_value,
@@ -124,18 +119,18 @@ where
set_loading,
source,
fetcher,
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
resolved: Arc::new(RwLock::new(resolved)),
scheduled: Arc::new(RwLock::new(false)),
suspense_contexts: Default::default(),
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_serializable_resource(Rc::clone(&r))
runtime.create_serializable_resource(Arc::clone(&r))
})
.expect("tried to create a Resource in a Runtime that has been disposed.");
create_isomorphic_effect(cx, {
let r = Rc::clone(&r);
let r = Arc::clone(&r);
move |_| {
load_resource(cx, id, r.clone());
}
@@ -233,10 +228,10 @@ where
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let fetcher = Arc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
let r = Arc::new(ResourceState {
scope: cx,
value,
set_value,
@@ -244,18 +239,18 @@ where
set_loading,
source,
fetcher,
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
resolved: Arc::new(RwLock::new(resolved)),
scheduled: Arc::new(RwLock::new(false)),
suspense_contexts: Default::default(),
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_unserializable_resource(Rc::clone(&r))
runtime.create_unserializable_resource(Arc::clone(&r))
})
.expect("tried to create a Resource in a runtime that has been disposed.");
create_effect(cx, {
let r = Rc::clone(&r);
let r = Arc::clone(&r);
// This is a local resource, so we're always going to handle it on the
// client
move |_| r.load(false)
@@ -274,7 +269,7 @@ where
}
#[cfg(not(feature = "hydrate"))]
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Arc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
T: 'static,
@@ -283,7 +278,7 @@ where
}
#[cfg(feature = "hydrate")]
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>)
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Arc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
T: Serializable + 'static,
@@ -291,12 +286,12 @@ where
use wasm_bindgen::{JsCast, UnwrapThrowExt};
_ = with_runtime(cx.runtime, |runtime| {
let mut context = runtime.shared_context.borrow_mut();
let mut context = runtime.shared_context.write();
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);
*r.resolved.write() = true;
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
@@ -318,7 +313,7 @@ where
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
*resolved.write() = true;
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
@@ -551,10 +546,10 @@ where
set_loading: WriteSignal<bool>,
source: Memo<S>,
#[allow(clippy::type_complexity)]
fetcher: Rc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
resolved: Rc<Cell<bool>>,
scheduled: Rc<Cell<bool>>,
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
fetcher: Arc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
resolved: Arc<RwLock<bool>>,
scheduled: Arc<RwLock<bool>>,
suspense_contexts: Arc<RwLock<HashSet<SuspenseContext>>>,
}
impl<S, T> ResourceState<S, T>
@@ -583,7 +578,7 @@ where
let increment = move |_: Option<()>| {
if let Some(s) = &suspense_cx {
let mut contexts = suspense_contexts.borrow_mut();
let mut contexts = suspense_contexts.write();
if !contexts.contains(s) {
contexts.insert(*s);
@@ -607,21 +602,22 @@ where
fn load(&self, refetching: bool) {
// doesn't refetch if already refetching
if refetching && self.scheduled.get() {
let mut scheduled = self.scheduled.write();
if refetching && *scheduled {
return;
}
self.scheduled.set(false);
*scheduled = false;
_ = self.source.try_with(|source| {
let fut = (self.fetcher)(source.clone());
// `scheduled` is true for the rest of this code only
self.scheduled.set(true);
*self.scheduled.write() = true;
queue_microtask({
let scheduled = Rc::clone(&self.scheduled);
let scheduled = Arc::clone(&self.scheduled);
move || {
scheduled.set(false);
*scheduled.write() = false;
}
});
@@ -630,7 +626,7 @@ where
// increment counter everywhere it's read
let suspense_contexts = self.suspense_contexts.clone();
for suspense_context in suspense_contexts.borrow().iter() {
for suspense_context in suspense_contexts.read().iter() {
suspense_context.increment();
}
@@ -642,12 +638,12 @@ where
async move {
let res = fut.await;
resolved.set(true);
*resolved.write() = true;
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
for suspense_context in suspense_contexts.borrow().iter() {
for suspense_context in suspense_contexts.read().iter() {
suspense_context.decrement();
}
}
@@ -686,8 +682,8 @@ where
}
pub(crate) enum AnyResource {
Unserializable(Rc<dyn UnserializableResource>),
Serializable(Rc<dyn SerializableResource>),
Unserializable(Arc<dyn UnserializableResource + Send + Sync>),
Serializable(Arc<dyn SerializableResource + Send + Sync>),
}
pub(crate) trait SerializableResource {

View File

@@ -6,28 +6,30 @@ use crate::{
};
use cfg_if::cfg_if;
use futures::stream::FuturesUnordered;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
cell::RefCell,
collections::{HashMap, HashSet},
fmt::Debug,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
sync::Arc,
};
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 = Runtime::new();
lazy_static! {
pub(crate) static ref RUNTIME: Runtime = Runtime::new();
}
} else {
thread_local! {
pub(crate) static RUNTIMES: RefCell<SlotMap<RuntimeId, Runtime>> = Default::default();
lazy_static! {
pub(crate) static ref RUNTIMES: RwLock<SlotMap<RuntimeId, Runtime>> = Default::default();
}
}
}
@@ -39,15 +41,13 @@ pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> R
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
_ = id;
Ok(RUNTIME.with(|runtime| f(runtime)))
Ok(f(&RUNTIME))
} else {
RUNTIMES.with(|runtimes| {
let runtimes = runtimes.borrow();
match runtimes.get(id) {
None => Err(()),
Some(runtime) => Ok(f(runtime))
}
})
let runtimes = RUNTIMES.read();
match runtimes.get(id) {
None => Err(()),
Some(runtime) => Ok(f(runtime))
}
}
}
}
@@ -60,7 +60,7 @@ pub fn create_runtime() -> RuntimeId {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
Default::default()
} else {
RUNTIMES.with(|runtimes| runtimes.borrow_mut().insert(Runtime::new()))
RUNTIMES.with(|runtimes| runtimes.write().insert(Runtime::new()))
}
}
}
@@ -75,7 +75,7 @@ impl RuntimeId {
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));
let runtime = RUNTIMES.with(move |runtimes| runtimes.write().remove(self));
drop(runtime);
}
}
@@ -83,7 +83,7 @@ impl RuntimeId {
pub(crate) fn raw_scope_and_disposer(self) -> (Scope, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
let id = { runtime.scopes.write().insert(Default::default()) };
let scope = Scope { runtime: self, id };
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
@@ -97,9 +97,9 @@ impl RuntimeId {
parent: Option<Scope>,
) -> (T, ScopeId, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
let id = { runtime.scopes.write().insert(Default::default()) };
if let Some(parent) = parent {
runtime.scope_parents.borrow_mut().insert(id, parent.id);
runtime.scope_parents.write().insert(id, parent.id);
}
let scope = Scope { runtime: self, id };
let val = f(scope);
@@ -121,10 +121,7 @@ impl RuntimeId {
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
runtime.signals.write().insert(Arc::new(RwLock::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
(
@@ -150,10 +147,7 @@ impl RuntimeId {
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
runtime.signals.write().insert(Arc::new(RwLock::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
RwSignal {
@@ -176,11 +170,11 @@ impl RuntimeId {
with_runtime(self, |runtime| {
let effect = Effect {
f,
value: RefCell::new(None),
value: RwLock::new(None),
#[cfg(debug_assertions)]
defined_at,
};
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
let id = { runtime.effects.write().insert(Arc::new(effect)) };
id.run::<T>(self);
id
})
@@ -219,33 +213,63 @@ impl RuntimeId {
#[derive(Default)]
pub(crate) struct Runtime {
pub shared_context: RefCell<SharedContext>,
pub observer: Cell<Option<EffectId>>,
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
pub shared_context: RwLock<SharedContext>,
pub scopes: RwLock<SlotMap<ScopeId, RwLock<Vec<ScopeProperty>>>>,
pub scope_parents: RwLock<SparseSecondaryMap<ScopeId, ScopeId>>,
pub scope_children: RwLock<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
#[allow(clippy::type_complexity)]
pub scope_contexts: RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
pub scope_contexts: RwLock<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
#[allow(clippy::type_complexity)]
pub scope_cleanups: RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
pub scope_cleanups: RwLock<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub signals: RwLock<SlotMap<SignalId, Arc<RwLock<dyn Any>>>>,
pub signal_subscribers: RwLock<SecondaryMap<SignalId, RwLock<HashSet<EffectId>>>>,
pub effects: RwLock<SlotMap<EffectId, Arc<dyn AnyEffect>>>,
pub effect_sources: RwLock<SecondaryMap<EffectId, RwLock<HashSet<SignalId>>>>,
pub resources: RwLock<SlotMap<ResourceId, AnyResource>>,
}
// track current observer thread-locally
// because effects run synchronously, the current observer
// *in this thread* will not change during the execution of an effect.
// but if we track this across threads, it's possible for overlapping
// executions to cause the stack to be out of order
// so we store at most one current observer per runtime, per thread
thread_local! {
static OBSERVER: RefCell<HashMap<RuntimeId, EffectId>> = Default::default();
}
pub(crate) struct LocalObserver {}
impl LocalObserver {
pub fn take(runtime: RuntimeId) -> Option<EffectId> {
OBSERVER.with(|observer| observer.borrow_mut().remove(&runtime))
}
pub fn get(runtime: RuntimeId) -> Option<EffectId> {
OBSERVER.with(|observer| observer.borrow().get(&runtime).copied())
}
pub fn set(runtime: RuntimeId, effect: Option<EffectId>) {
OBSERVER.with(|observer| {
if let Some(value) = effect {
observer.borrow_mut().insert(runtime, value)
} else {
observer.borrow_mut().remove(&runtime)
}
});
}
}
impl Debug for Runtime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runtime")
.field("shared_context", &self.shared_context)
.field("observer", &self.observer)
.field("scopes", &self.scopes)
.field("scope_parents", &self.scope_parents)
.field("scope_children", &self.scope_children)
.field("signals", &self.signals)
.field("signal_subscribers", &self.signal_subscribers)
.field("effects", &self.effects.borrow().len())
.field("effects", &self.effects.read().len())
.field("effect_sources", &self.effect_sources)
.finish()
}
@@ -258,27 +282,27 @@ impl Runtime {
pub(crate) fn create_unserializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,
state: Arc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: 'static,
{
self.resources
.borrow_mut()
.write()
.insert(AnyResource::Unserializable(state))
}
pub(crate) fn create_serializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,
state: Arc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: Serializable + 'static,
{
self.resources
.borrow_mut()
.write()
.insert(AnyResource::Serializable(state))
}
@@ -291,7 +315,7 @@ impl Runtime {
S: 'static,
T: 'static,
{
let resources = self.resources.borrow();
let resources = self.resources.read();
let res = resources.get(id);
if let Some(res) = res {
let res_state = match res {
@@ -317,7 +341,7 @@ impl Runtime {
/// Returns IDs for all [Resource]s found on any scope.
pub(crate) fn all_resources(&self) -> Vec<ResourceId> {
self.resources
.borrow()
.read()
.iter()
.map(|(resource_id, _)| resource_id)
.collect()
@@ -326,7 +350,7 @@ impl Runtime {
/// Returns IDs for all [Resource]s found on any scope, pending from the server.
pub(crate) fn pending_resources(&self) -> Vec<ResourceId> {
self.resources
.borrow()
.read()
.iter()
.filter_map(|(resource_id, res)| {
if matches!(res, AnyResource::Serializable(_)) {
@@ -342,7 +366,7 @@ impl Runtime {
&self,
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
let f = FuturesUnordered::new();
for (id, resource) in self.resources.borrow().iter() {
for (id, resource) in self.resources.read().iter() {
if let AnyResource::Serializable(resource) = resource {
f.push(resource.to_serialization_resolver(id));
}

View File

@@ -1,6 +1,6 @@
#![forbid(unsafe_code)]
use crate::{
runtime::{with_runtime, RuntimeId},
runtime::{with_runtime, LocalObserver, RuntimeId},
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
};
use futures::stream::FuturesUnordered;
@@ -108,7 +108,7 @@ impl Scope {
pub fn run_child_scope<T>(self, f: impl FnOnce(Scope) -> T) -> (T, ScopeDisposer) {
let (res, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
_ = with_runtime(self.runtime, |runtime| {
let mut children = runtime.scope_children.borrow_mut();
let mut children = runtime.scope_children.write();
children
.entry(self.id)
.expect("trying to add a child to a Scope that has already been disposed")
@@ -145,9 +145,9 @@ impl Scope {
/// ```
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, |runtime| {
let prev_observer = runtime.observer.take();
let prev_observer = LocalObserver::take(self.runtime);
let untracked_result = f();
runtime.observer.set(prev_observer);
LocalObserver::set(self.runtime, prev_observer);
untracked_result
})
.expect("tried to run untracked function in a runtime that has been disposed")
@@ -167,7 +167,7 @@ impl Scope {
_ = with_runtime(self.runtime, |runtime| {
// dispose of all child scopes
let children = {
let mut children = runtime.scope_children.borrow_mut();
let mut children = runtime.scope_children.write();
children.remove(self.id)
};
@@ -182,7 +182,7 @@ impl Scope {
}
// run cleanups
if let Some(cleanups) = runtime.scope_cleanups.borrow_mut().remove(self.id) {
if let Some(cleanups) = runtime.scope_cleanups.write().remove(self.id) {
for cleanup in cleanups {
cleanup();
}
@@ -190,34 +190,34 @@ impl Scope {
// remove everything we own and run cleanups
let owned = {
let owned = runtime.scopes.borrow_mut().remove(self.id);
owned.map(|owned| owned.take())
let owned = runtime.scopes.write().remove(self.id);
owned.map(|owned| std::mem::take(&mut *owned.write()))
};
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);
runtime.signals.write().remove(id);
let subs = runtime.signal_subscribers.write().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() {
let source_map = runtime.effect_sources.read();
for effect in subs.read().iter() {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&id);
effect_sources.write().remove(&id);
}
}
}
}
ScopeProperty::Effect(id) => {
runtime.effects.borrow_mut().remove(id);
runtime.effect_sources.borrow_mut().remove(id);
runtime.effects.write().remove(id);
runtime.effect_sources.write().remove(id);
}
ScopeProperty::Resource(id) => {
runtime.resources.borrow_mut().remove(id);
runtime.resources.write().remove(id);
}
}
}
@@ -227,18 +227,18 @@ impl Scope {
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
_ = with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
let scopes = runtime.scopes.read();
let scope = scopes
.get(self.id)
.expect("tried to add property to a scope that has been disposed");
f(&mut scope.borrow_mut());
f(&mut scope.write());
})
}
/// Returns the the parent Scope, if any.
pub fn parent(&self) -> Option<Scope> {
with_runtime(self.runtime, |runtime| {
runtime.scope_parents.borrow().get(self.id).copied()
runtime.scope_parents.read().get(self.id).copied()
})
.ok()
.flatten()
@@ -255,7 +255,7 @@ impl Scope {
/// are invalidated.
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
_ = with_runtime(cx.runtime, |runtime| {
let mut cleanups = runtime.scope_cleanups.borrow_mut();
let mut cleanups = runtime.scope_cleanups.write();
let cleanups = cleanups
.entry(cx.id)
.expect("trying to clean up a Scope that has already been disposed")
@@ -327,7 +327,7 @@ impl Scope {
use futures::StreamExt;
_ = with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let mut shared_context = runtime.shared_context.write();
let (tx, mut rx) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
@@ -355,7 +355,7 @@ impl Scope {
/// `<Suspense/>` HTML when all resources are resolved.
pub fn pending_fragments(&self) -> HashMap<String, (String, PinnedFuture<String>)> {
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let mut shared_context = runtime.shared_context.write();
std::mem::take(&mut shared_context.pending_fragments)
})
.unwrap_or_default()

View File

@@ -1,7 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
debug_warn,
runtime::{with_runtime, RuntimeId},
runtime::{with_runtime, LocalObserver, RuntimeId},
Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
};
use cfg_if::cfg_if;
@@ -246,7 +246,9 @@ where
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
_ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
_ = with_runtime(self.runtime, |runtime| {
self.id.subscribe(self.runtime, runtime)
})
}
/// Clones and returns the current value of the signal, and subscribes
@@ -286,7 +288,9 @@ 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> {
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f)) {
match with_runtime(self.runtime, |runtime| {
self.id.try_with(self.runtime, runtime, f)
}) {
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(e),
Err(_) => Err(SignalError::RuntimeDisposed),
@@ -1169,20 +1173,20 @@ pub(crate) enum SignalError {
}
impl SignalId {
pub(crate) fn subscribe(&self, runtime: &Runtime) {
pub(crate) fn subscribe(&self, runtime_id: RuntimeId, runtime: &Runtime) {
// add subscriber
if let Some(observer) = runtime.observer.get() {
if let Some(observer) = LocalObserver::get(runtime_id) {
// add this observer to the signal's dependencies (to allow notification)
let mut subs = runtime.signal_subscribers.borrow_mut();
let mut subs = runtime.signal_subscribers.write();
if let Some(subs) = subs.entry(*self) {
subs.or_default().borrow_mut().insert(observer);
subs.or_default().write().insert(observer);
}
// add this signal to the effect's sources (to allow cleanup)
let mut effect_sources = runtime.effect_sources.borrow_mut();
let mut effect_sources = runtime.effect_sources.write();
if let Some(effect_sources) = effect_sources.entry(observer) {
let sources = effect_sources.or_default();
sources.borrow_mut().insert(*self);
sources.write().insert(*self);
}
}
}
@@ -1197,7 +1201,7 @@ impl SignalId {
{
// get the value
let value = {
let signals = runtime.signals.borrow();
let signals = runtime.signals.read();
match signals.get(*self).cloned().ok_or(SignalError::Disposed) {
Ok(s) => Ok(s),
Err(e) => {
@@ -1206,12 +1210,13 @@ impl SignalId {
}
}
}?;
let value = value.try_borrow().unwrap_or_else(|e| {
debug_warn!(
"Signal::try_with_no_subscription failed on Signal<{}>. It seems you're trying to read the value of a signal within an effect caused by updating the signal.",
let value = value.try_read().unwrap_or_else(|| {
panic!(
"Signal::try_with_no_subscription failed on Signal<{}>. \
It seems you're trying to read the value of a signal within an effect \
caused by updating the signal.",
std::any::type_name::<T>()
);
panic!("{e}");
});
let value = value
.downcast_ref::<T>()
@@ -1221,13 +1226,14 @@ impl SignalId {
pub(crate) fn try_with<T, U>(
&self,
runtime_id: RuntimeId,
runtime: &Runtime,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError>
where
T: 'static,
{
self.subscribe(runtime);
self.subscribe(runtime_id, runtime);
self.try_with_no_subscription(runtime, f)
}
@@ -1246,12 +1252,14 @@ impl SignalId {
.expect("tried to access a signal in a runtime that has been disposed")
}
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
pub(crate) fn with<T, U>(&self, runtime_id: RuntimeId, f: impl FnOnce(&T) -> U) -> U
where
T: 'static,
{
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
.expect("tried to access a signal in a runtime that has been disposed")
with_runtime(runtime_id, |runtime| {
self.try_with(runtime_id, runtime, f).unwrap()
})
.expect("tried to access a signal in a runtime that has been disposed")
}
fn update_value<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T) -> U) -> Option<U>
@@ -1260,11 +1268,11 @@ impl SignalId {
{
with_runtime(runtime, |runtime| {
let value = {
let signals = runtime.signals.borrow();
let signals = runtime.signals.read();
signals.get(*self).cloned()
};
if let Some(value) = value {
let mut value = value.borrow_mut();
let mut value = value.write();
if let Some(value) = value.downcast_mut::<T>() {
Some(f(value))
} else {
@@ -1300,14 +1308,14 @@ impl SignalId {
// notify subscribers
if updated.is_some() {
let subs = {
let subs = runtime.signal_subscribers.borrow();
let subs = runtime.signal_subscribers.read();
let subs = subs.get(*self);
subs.map(|subs| subs.borrow().clone())
subs.map(|subs| subs.read().clone())
};
if let Some(subs) = subs {
for sub in subs {
let effect = {
let effects = runtime.effects.borrow();
let effects = runtime.effects.read();
effects.get(sub).cloned()
};
if let Some(effect) = effect {

View File

@@ -19,7 +19,7 @@ fn untracked_set_doesnt_trigger_effect() {
create_isomorphic_effect(cx, {
let b = b.clone();
move |_| {
let formatted = format!("Value is {}", a());
let formatted = format!("Value is {}", a.get());
*b.borrow_mut() = formatted;
}
});
@@ -52,7 +52,7 @@ fn untracked_get_doesnt_trigger_effect() {
create_isomorphic_effect(cx, {
let b = b.clone();
move |_| {
let formatted = format!("Values are {} and {}", a(), a2.get_untracked());
let formatted = format!("Values are {} and {}", a.get(), a2.get_untracked());
*b.borrow_mut() = formatted;
}
});

View File

@@ -96,11 +96,6 @@ where
self.0.with(|a| a.pending.read_only())
}
/// Updates whether the action is currently pending.
pub fn set_pending(&self, pending: bool) {
self.0.with(|a| a.pending.set(pending))
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -155,10 +155,7 @@ where
let on_form_data = Rc::new(move |form_data: &web_sys::FormData| {
let data = action_input_from_form_data(form_data);
match data {
Ok(data) => {
input.set(Some(data));
action.set_pending(true);
}
Ok(data) => input.set(Some(data)),
Err(e) => log::error!("{e}"),
}
});
@@ -170,6 +167,11 @@ where
JsFuture::from(resp.text().expect("couldn't get .text() from Response")).await;
match body {
Ok(json) => {
log::debug!(
"body is {:?}\nO is {:?}",
json.as_string().unwrap(),
std::any::type_name::<O>()
);
match O::from_json(
&json.as_string().expect("couldn't get String from JsString"),
) {
@@ -180,9 +182,7 @@ where
}
}
Err(e) => log::error!("{e:?}"),
};
input.set(None);
action.set_pending(false);
}
});
});