From 4e4fb8ab108cef6b6c28c55c121d9b2ab7b8a6b1 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sun, 8 Sep 2024 19:39:15 -0400 Subject: [PATCH 01/13] chore(examples): `SsrMode` is no longer clone (#2954) --- examples/axum_js_ssr/src/app.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/axum_js_ssr/src/app.rs b/examples/axum_js_ssr/src/app.rs index e3b050bb5..b4cbded87 100644 --- a/examples/axum_js_ssr/src/app.rs +++ b/examples/axum_js_ssr/src/app.rs @@ -32,7 +32,6 @@ pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); let fallback = || view! { "Page not found." }.into_view(); - let ssr = SsrMode::Async; view! { @@ -79,19 +78,19 @@ pub fn App() -> impl IntoView {

"Leptos JavaScript Integration Demo with SSR in Axum"

- - } ssr/> - } ssr/> + + } ssr=SsrMode::Async/> + } ssr=SsrMode::Async/> - } ssr/> - - - - - - - + } ssr=SsrMode::Async/> + + + + + + + From 73f0207a7d7002a5ca73b5bd578624abe17e0056 Mon Sep 17 00:00:00 2001 From: jk <47693+sectore@users.noreply.github.com> Date: Mon, 9 Sep 2024 01:39:40 +0200 Subject: [PATCH 02/13] feat: add a copyable Trigger type (closes #2901) (#2939) --- .../async_derived/arc_async_derived.rs | 6 +- .../computed/async_derived/async_derived.rs | 8 +- reactive_graph/src/signal.rs | 2 + reactive_graph/src/signal/arc_rw.rs | 6 +- reactive_graph/src/signal/arc_trigger.rs | 6 +- reactive_graph/src/signal/arc_write.rs | 6 +- reactive_graph/src/signal/guards.rs | 16 +-- reactive_graph/src/signal/rw.rs | 6 +- reactive_graph/src/signal/trigger.rs | 103 ++++++++++++++++++ reactive_graph/src/signal/write.rs | 8 +- reactive_graph/src/traits.rs | 6 +- reactive_stores/src/arc_field.rs | 8 +- reactive_stores/src/field.rs | 8 +- reactive_stores/src/iter.rs | 8 +- reactive_stores/src/lib.rs | 17 ++- reactive_stores/src/patch.rs | 4 +- reactive_stores/src/subfield.rs | 8 +- router/src/nested_router.rs | 6 +- 18 files changed, 168 insertions(+), 64 deletions(-) create mode 100644 reactive_graph/src/signal/trigger.rs diff --git a/reactive_graph/src/computed/async_derived/arc_async_derived.rs b/reactive_graph/src/computed/async_derived/arc_async_derived.rs index edd531ecd..c2ae334b5 100644 --- a/reactive_graph/src/computed/async_derived/arc_async_derived.rs +++ b/reactive_graph/src/computed/async_derived/arc_async_derived.rs @@ -18,7 +18,7 @@ use crate::{ ArcTrigger, }, traits::{ - DefinedAt, ReadUntracked, Track, Trigger, UntrackableGuard, Writeable, + DefinedAt, Notify, ReadUntracked, Track, UntrackableGuard, Writeable, }, transition::AsyncTransition, }; @@ -580,8 +580,8 @@ impl ReadUntracked for ArcAsyncDerived { } } -impl Trigger for ArcAsyncDerived { - fn trigger(&self) { +impl Notify for ArcAsyncDerived { + fn notify(&self) { Self::notify_subs(&self.wakers, &self.inner, &self.loading, None); } } diff --git a/reactive_graph/src/computed/async_derived/async_derived.rs b/reactive_graph/src/computed/async_derived/async_derived.rs index fedeb60cf..3f986e718 100644 --- a/reactive_graph/src/computed/async_derived/async_derived.rs +++ b/reactive_graph/src/computed/async_derived/async_derived.rs @@ -7,7 +7,7 @@ use crate::{ owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage}, signal::guards::{AsyncPlain, ReadGuard, WriteGuard}, traits::{ - DefinedAt, Dispose, ReadUntracked, Trigger, UntrackableGuard, Writeable, + DefinedAt, Dispose, Notify, ReadUntracked, UntrackableGuard, Writeable, }, unwrap_signal, }; @@ -291,13 +291,13 @@ where } } -impl Trigger for AsyncDerived +impl Notify for AsyncDerived where T: 'static, S: Storage>, { - fn trigger(&self) { - self.inner.try_with_value(|inner| inner.trigger()); + fn notify(&self) { + self.inner.try_with_value(|inner| inner.notify()); } } diff --git a/reactive_graph/src/signal.rs b/reactive_graph/src/signal.rs index affebee9b..68cc75f0e 100644 --- a/reactive_graph/src/signal.rs +++ b/reactive_graph/src/signal.rs @@ -9,6 +9,7 @@ pub mod guards; mod read; mod rw; mod subscriber_traits; +mod trigger; mod write; use crate::owner::LocalStorage; @@ -18,6 +19,7 @@ pub use arc_trigger::*; pub use arc_write::*; pub use read::*; pub use rw::*; +pub use trigger::*; pub use write::*; /// Creates a reference-counted signal. diff --git a/reactive_graph/src/signal/arc_rw.rs b/reactive_graph/src/signal/arc_rw.rs index 508160e90..2519b5065 100644 --- a/reactive_graph/src/signal/arc_rw.rs +++ b/reactive_graph/src/signal/arc_rw.rs @@ -5,7 +5,7 @@ use super::{ }; use crate::{ graph::{ReactiveNode, SubscriberSet}, - prelude::{IsDisposed, Trigger}, + prelude::{IsDisposed, Notify}, traits::{DefinedAt, ReadUntracked, UntrackableGuard, Writeable}, }; use core::fmt::{Debug, Formatter, Result}; @@ -247,8 +247,8 @@ impl ReadUntracked for ArcRwSignal { } } -impl Trigger for ArcRwSignal { - fn trigger(&self) { +impl Notify for ArcRwSignal { + fn notify(&self) { self.mark_dirty(); } } diff --git a/reactive_graph/src/signal/arc_trigger.rs b/reactive_graph/src/signal/arc_trigger.rs index 2af5a079e..e1a7cedee 100644 --- a/reactive_graph/src/signal/arc_trigger.rs +++ b/reactive_graph/src/signal/arc_trigger.rs @@ -1,7 +1,7 @@ use super::subscriber_traits::AsSubscriberSet; use crate::{ graph::{ReactiveNode, SubscriberSet}, - traits::{DefinedAt, IsDisposed, Trigger}, + traits::{DefinedAt, IsDisposed, Notify}, }; use std::{ fmt::{Debug, Formatter, Result}, @@ -83,8 +83,8 @@ impl DefinedAt for ArcTrigger { } } -impl Trigger for ArcTrigger { - fn trigger(&self) { +impl Notify for ArcTrigger { + fn notify(&self) { self.inner.mark_dirty(); } } diff --git a/reactive_graph/src/signal/arc_write.rs b/reactive_graph/src/signal/arc_write.rs index c3eaad581..6626fe729 100644 --- a/reactive_graph/src/signal/arc_write.rs +++ b/reactive_graph/src/signal/arc_write.rs @@ -1,7 +1,7 @@ use super::guards::{UntrackedWriteGuard, WriteGuard}; use crate::{ graph::{ReactiveNode, SubscriberSet}, - prelude::{IsDisposed, Trigger}, + prelude::{IsDisposed, Notify}, traits::{DefinedAt, UntrackableGuard, Writeable}, }; use core::fmt::{Debug, Formatter, Result}; @@ -116,8 +116,8 @@ impl IsDisposed for ArcWriteSignal { } } -impl Trigger for ArcWriteSignal { - fn trigger(&self) { +impl Notify for ArcWriteSignal { + fn notify(&self) { self.inner.mark_dirty(); } } diff --git a/reactive_graph/src/signal/guards.rs b/reactive_graph/src/signal/guards.rs index 3c8e95bd5..317e46c96 100644 --- a/reactive_graph/src/signal/guards.rs +++ b/reactive_graph/src/signal/guards.rs @@ -2,7 +2,7 @@ use crate::{ computed::BlockingLock, - traits::{Trigger, UntrackableGuard}, + traits::{Notify, UntrackableGuard}, }; use core::fmt::Debug; use guardian::{ArcRwLockReadGuardian, ArcRwLockWriteGuardian}; @@ -259,7 +259,7 @@ where #[derive(Debug)] pub struct WriteGuard where - S: Trigger, + S: Notify, { pub(crate) triggerable: Option, pub(crate) guard: Option, @@ -267,7 +267,7 @@ where impl WriteGuard where - S: Trigger, + S: Notify, { /// Creates a new guard from the inner mutable guard type, and the signal that should be /// triggered on drop. @@ -281,7 +281,7 @@ where impl UntrackableGuard for WriteGuard where - S: Trigger, + S: Notify, G: DerefMut, { /// Removes the triggerable type, so that it is no longer notifies when dropped. @@ -292,7 +292,7 @@ where impl Deref for WriteGuard where - S: Trigger, + S: Notify, G: Deref, { type Target = G::Target; @@ -310,7 +310,7 @@ where impl DerefMut for WriteGuard where - S: Trigger, + S: Notify, G: DerefMut, { fn deref_mut(&mut self) -> &mut Self::Target { @@ -354,7 +354,7 @@ impl DerefMut for UntrackedWriteGuard { // Dropping the write guard will notify dependencies. impl Drop for WriteGuard where - S: Trigger, + S: Notify, { fn drop(&mut self) { // first, drop the inner guard @@ -362,7 +362,7 @@ where // then, notify about a change if let Some(triggerable) = self.triggerable.as_ref() { - triggerable.trigger(); + triggerable.notify(); } } } diff --git a/reactive_graph/src/signal/rw.rs b/reactive_graph/src/signal/rw.rs index 3ef5f9496..2fc4d9770 100644 --- a/reactive_graph/src/signal/rw.rs +++ b/reactive_graph/src/signal/rw.rs @@ -8,7 +8,7 @@ use crate::{ owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage}, signal::guards::{UntrackedWriteGuard, WriteGuard}, traits::{ - DefinedAt, Dispose, IsDisposed, ReadUntracked, Trigger, + DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked, UntrackableGuard, Writeable, }, unwrap_signal, @@ -340,11 +340,11 @@ where } } -impl Trigger for RwSignal +impl Notify for RwSignal where S: Storage>, { - fn trigger(&self) { + fn notify(&self) { self.mark_dirty(); } } diff --git a/reactive_graph/src/signal/trigger.rs b/reactive_graph/src/signal/trigger.rs new file mode 100644 index 000000000..a35c812e6 --- /dev/null +++ b/reactive_graph/src/signal/trigger.rs @@ -0,0 +1,103 @@ +use super::{subscriber_traits::AsSubscriberSet, ArcTrigger}; +use crate::{ + graph::{ReactiveNode, SubscriberSet}, + owner::StoredValue, + traits::{DefinedAt, Dispose, IsDisposed, Notify}, +}; +use std::{ + fmt::{Debug, Formatter, Result}, + panic::Location, + sync::{Arc, RwLock}, +}; + +/// A trigger is a data-less signal with the sole purpose of notifying other reactive code of a change. +/// +/// This can be useful for when using external data not stored in signals, for example. +/// +/// This is an arena-allocated Trigger, which is `Copy` and is disposed when its reactive +/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted trigger that lives +/// as long as a reference to it is alive, see [`ArcTrigger`]. +pub struct Trigger { + #[cfg(debug_assertions)] + pub(crate) defined_at: &'static Location<'static>, + pub(crate) inner: StoredValue, +} + +impl Trigger { + /// Creates a new trigger. + #[track_caller] + pub fn new() -> Self { + Self { + #[cfg(debug_assertions)] + defined_at: Location::caller(), + inner: StoredValue::new(ArcTrigger::new()), + } + } +} + +impl Default for Trigger { + fn default() -> Self { + Self::new() + } +} + +impl Clone for Trigger { + #[track_caller] + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Trigger {} + +impl Debug for Trigger { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.debug_struct("Trigger").finish() + } +} + +impl Dispose for Trigger { + fn dispose(self) { + self.inner.dispose() + } +} + +impl IsDisposed for Trigger { + #[inline(always)] + fn is_disposed(&self) -> bool { + self.inner.is_disposed() + } +} + +impl AsSubscriberSet for Trigger { + type Output = Arc>; + + #[inline(always)] + fn as_subscriber_set(&self) -> Option { + self.inner + .try_get_value() + .and_then(|arc_trigger| arc_trigger.as_subscriber_set()) + } +} + +impl DefinedAt for Trigger { + #[inline(always)] + fn defined_at(&self) -> Option<&'static Location<'static>> { + #[cfg(debug_assertions)] + { + Some(self.defined_at) + } + #[cfg(not(debug_assertions))] + { + None + } + } +} + +impl Notify for Trigger { + fn notify(&self) { + if let Some(inner) = self.inner.try_get_value() { + inner.mark_dirty(); + } + } +} diff --git a/reactive_graph/src/signal/write.rs b/reactive_graph/src/signal/write.rs index 3668e3c5a..5e37bc337 100644 --- a/reactive_graph/src/signal/write.rs +++ b/reactive_graph/src/signal/write.rs @@ -2,7 +2,7 @@ use super::{guards::WriteGuard, ArcWriteSignal}; use crate::{ owner::{Storage, StoredValue, SyncStorage}, traits::{ - DefinedAt, Dispose, IsDisposed, Trigger, UntrackableGuard, Writeable, + DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Writeable, }, }; use core::fmt::Debug; @@ -116,14 +116,14 @@ impl IsDisposed for WriteSignal { } } -impl Trigger for WriteSignal +impl Notify for WriteSignal where T: 'static, S: Storage>, { - fn trigger(&self) { + fn notify(&self) { if let Some(inner) = self.inner.try_get_value() { - inner.trigger(); + inner.notify(); } } } diff --git a/reactive_graph/src/traits.rs b/reactive_graph/src/traits.rs index dc5b0476b..a886844e4 100644 --- a/reactive_graph/src/traits.rs +++ b/reactive_graph/src/traits.rs @@ -209,7 +209,7 @@ pub trait UntrackableGuard: DerefMut { /// Gives mutable access to a signal's value through a guard type. When the guard is dropped, the /// signal's subscribers will be notified. -pub trait Writeable: Sized + DefinedAt + Trigger { +pub trait Writeable: Sized + DefinedAt + Notify { /// The type of the signal's value. type Value: Sized + 'static; @@ -381,9 +381,9 @@ where } /// Notifies subscribers of a change in this signal. -pub trait Trigger { +pub trait Notify { /// Notifies subscribers of a change in this signal. - fn trigger(&self); + fn notify(&self); } /// Updates the value of a signal by applying a function that updates it in place, diff --git a/reactive_stores/src/arc_field.rs b/reactive_stores/src/arc_field.rs index b42beb919..f318758b3 100644 --- a/reactive_stores/src/arc_field.rs +++ b/reactive_stores/src/arc_field.rs @@ -5,7 +5,7 @@ use crate::{ use reactive_graph::{ signal::ArcTrigger, traits::{ - DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard, + DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard, }, }; use std::{ @@ -180,9 +180,9 @@ impl DefinedAt for ArcField { } } -impl Trigger for ArcField { - fn trigger(&self) { - self.trigger.trigger(); +impl Notify for ArcField { + fn notify(&self) { + self.trigger.notify(); } } diff --git a/reactive_stores/src/field.rs b/reactive_stores/src/field.rs index bfac83066..fb4fcb6f9 100644 --- a/reactive_stores/src/field.rs +++ b/reactive_stores/src/field.rs @@ -6,7 +6,7 @@ use crate::{ use reactive_graph::{ owner::{Storage, StoredValue, SyncStorage}, signal::ArcTrigger, - traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Trigger}, + traits::{DefinedAt, IsDisposed, Notify, ReadUntracked, Track}, unwrap_signal, }; use std::{ops::IndexMut, panic::Location}; @@ -108,13 +108,13 @@ impl DefinedAt for Field { } } -impl Trigger for Field +impl Notify for Field where S: Storage>, { - fn trigger(&self) { + fn notify(&self) { if let Some(inner) = self.inner.try_get_value() { - inner.trigger(); + inner.notify(); } } } diff --git a/reactive_stores/src/iter.rs b/reactive_stores/src/iter.rs index 9da3aef7b..0959c3d59 100644 --- a/reactive_stores/src/iter.rs +++ b/reactive_stores/src/iter.rs @@ -8,7 +8,7 @@ use reactive_graph::{ ArcTrigger, }, traits::{ - DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard, + DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard, Writeable, }, }; @@ -136,15 +136,15 @@ where } } -impl Trigger for AtIndex +impl Notify for AtIndex where Inner: StoreField, Prev: IndexMut + 'static, Prev::Output: Sized, { - fn trigger(&self) { + fn notify(&self) { let trigger = self.get_trigger(self.path().into_iter().collect()); - trigger.trigger(); + trigger.notify(); } } diff --git a/reactive_stores/src/lib.rs b/reactive_stores/src/lib.rs index 502801ad8..64419aebe 100644 --- a/reactive_stores/src/lib.rs +++ b/reactive_stores/src/lib.rs @@ -4,7 +4,7 @@ use reactive_graph::{ guards::{Plain, ReadGuard}, ArcTrigger, }, - traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Trigger}, + traits::{DefinedAt, IsDisposed, Notify, ReadUntracked, Track}, }; use rustc_hash::FxHashMap; use std::{ @@ -123,14 +123,13 @@ where impl Track for ArcStore { fn track(&self) { - self.get_trigger(Default::default()).trigger(); + self.get_trigger(Default::default()).notify(); } } -impl Trigger for ArcStore { - fn trigger(&self) { - self.get_trigger(self.path().into_iter().collect()) - .trigger(); +impl Notify for ArcStore { + fn notify(&self) { + self.get_trigger(self.path().into_iter().collect()).notify(); } } @@ -235,14 +234,14 @@ where } } -impl Trigger for Store +impl Notify for Store where T: 'static, S: Storage>, { - fn trigger(&self) { + fn notify(&self) { if let Some(inner) = self.inner.try_get_value() { - inner.trigger(); + inner.notify(); } } } diff --git a/reactive_stores/src/patch.rs b/reactive_stores/src/patch.rs index 8d7c45290..fd3966f5c 100644 --- a/reactive_stores/src/patch.rs +++ b/reactive_stores/src/patch.rs @@ -1,6 +1,6 @@ use crate::{path::StorePath, StoreField}; use itertools::{EitherOrBoth, Itertools}; -use reactive_graph::traits::{Trigger, UntrackableGuard}; +use reactive_graph::traits::{Notify, UntrackableGuard}; use std::{ borrow::Cow, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, @@ -33,7 +33,7 @@ where writer.untrack(); let mut notify = |path: &StorePath| { println!("notifying on {path:?}"); - self.get_trigger(path.to_owned()).trigger(); + self.get_trigger(path.to_owned()).notify(); }; writer.patch_field(new, &path, &mut notify); } diff --git a/reactive_stores/src/subfield.rs b/reactive_stores/src/subfield.rs index deca5fa14..f073e3550 100644 --- a/reactive_stores/src/subfield.rs +++ b/reactive_stores/src/subfield.rs @@ -8,7 +8,7 @@ use reactive_graph::{ ArcTrigger, }, traits::{ - DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard, + DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard, Writeable, }, }; @@ -130,14 +130,14 @@ where } } -impl Trigger for Subfield +impl Notify for Subfield where Inner: StoreField, Prev: 'static, { - fn trigger(&self) { + fn notify(&self) { let trigger = self.get_trigger(self.path().into_iter().collect()); - trigger.trigger(); + trigger.notify(); } } diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index 2a4b280cb..b5be3eae1 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -15,7 +15,7 @@ use reactive_graph::{ computed::{ArcMemo, ScopedFuture}, owner::{provide_context, use_context, Owner}, signal::{ArcRwSignal, ArcTrigger}, - traits::{Get, GetUntracked, ReadUntracked, Set, Track, Trigger}, + traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track}, wrappers::write::SignalSetter, }; use send_wrapper::SendWrapper; @@ -124,7 +124,7 @@ where ScopedFuture::new(async move { let triggers = join_all(loaders).await; for trigger in triggers { - trigger.trigger(); + trigger.notify(); } matched_view.rebuild(&mut *view.borrow_mut()); }) @@ -179,7 +179,7 @@ where let triggers = join_all(loaders).await; // tell each one of the outlet triggers that it's ready for trigger in triggers { - trigger.trigger(); + trigger.notify(); } if let Some(loc) = location { loc.ready_to_complete(); From 5a57d48913650eba7082a82e993f4b6ff42238fd Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sun, 8 Sep 2024 19:40:32 -0400 Subject: [PATCH 03/13] beta5 --- Cargo.toml | 42 ++++++++++++------------ any_error/Cargo.toml | 2 +- hydration_context/Cargo.toml | 2 +- leptos_macro/Cargo.toml | 2 +- meta/Cargo.toml | 2 +- next_tuple/Cargo.toml | 2 +- reactive_graph/Cargo.toml | 2 +- reactive_graph/src/owner/stored_value.rs | 2 +- reactive_stores/Cargo.toml | 2 +- reactive_stores_macro/Cargo.toml | 2 +- router/Cargo.toml | 2 +- router_macro/Cargo.toml | 2 +- tachys/Cargo.toml | 2 +- 13 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f0a7ca276..efa12e571 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,36 +40,36 @@ members = [ exclude = ["benchmarks", "examples", "projects"] [workspace.package] -version = "0.7.0-beta4" +version = "0.7.0-beta5" edition = "2021" rust-version = "1.76" [workspace.dependencies] -throw_error = { path = "./any_error/", version = "0.2.0-beta4" } +throw_error = { path = "./any_error/", version = "0.2.0-beta5" } any_spawner = { path = "./any_spawner/", version = "0.1.0" } const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" } either_of = { path = "./either_of/", version = "0.1.0" } -hydration_context = { path = "./hydration_context", version = "0.2.0-beta4" } -leptos = { path = "./leptos", version = "0.7.0-beta4" } -leptos_config = { path = "./leptos_config", version = "0.7.0-beta4" } -leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta4" } -leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta4" } -leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta4" } -leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta4" } -leptos_router = { path = "./router", version = "0.7.0-beta4" } -leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta4" } -leptos_server = { path = "./leptos_server", version = "0.7.0-beta4" } -leptos_meta = { path = "./meta", version = "0.7.0-beta4" } -next_tuple = { path = "./next_tuple", version = "0.1.0-beta4" } +hydration_context = { path = "./hydration_context", version = "0.2.0-beta5" } +leptos = { path = "./leptos", version = "0.7.0-beta5" } +leptos_config = { path = "./leptos_config", version = "0.7.0-beta5" } +leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta5" } +leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta5" } +leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta5" } +leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta5" } +leptos_router = { path = "./router", version = "0.7.0-beta5" } +leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta5" } +leptos_server = { path = "./leptos_server", version = "0.7.0-beta5" } +leptos_meta = { path = "./meta", version = "0.7.0-beta5" } +next_tuple = { path = "./next_tuple", version = "0.1.0-beta5" } oco_ref = { path = "./oco", version = "0.2.0" } or_poisoned = { path = "./or_poisoned", version = "0.1.0" } -reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta4" } -reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta4" } -reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta4" } -server_fn = { path = "./server_fn", version = "0.7.0-beta4" } -server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta4" } -server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta4" } -tachys = { path = "./tachys", version = "0.1.0-beta4" } +reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta5" } +reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta5" } +reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta5" } +server_fn = { path = "./server_fn", version = "0.7.0-beta5" } +server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta5" } +server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta5" } +tachys = { path = "./tachys", version = "0.1.0-beta5" } [profile.release] codegen-units = 1 diff --git a/any_error/Cargo.toml b/any_error/Cargo.toml index e12c3d7e1..9f7bd0939 100644 --- a/any_error/Cargo.toml +++ b/any_error/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "throw_error" -version = "0.2.0-beta4" +version = "0.2.0-beta5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/hydration_context/Cargo.toml b/hydration_context/Cargo.toml index f5abfcc13..cffa17773 100644 --- a/hydration_context/Cargo.toml +++ b/hydration_context/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydration_context" -version = "0.2.0-beta4" +version = "0.2.0-beta5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index def0e2a07..1b3fe6655 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_macro" -version = "0.7.0-beta4" +version = "0.7.0-beta5" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/meta/Cargo.toml b/meta/Cargo.toml index 8a9815a4c..e245c4cf9 100644 --- a/meta/Cargo.toml +++ b/meta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_meta" -version = "0.7.0-beta4" +version = "0.7.0-beta5" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/next_tuple/Cargo.toml b/next_tuple/Cargo.toml index ac4f7fe27..deead181e 100644 --- a/next_tuple/Cargo.toml +++ b/next_tuple/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "next_tuple" -version = "0.1.0-beta4" +version = "0.1.0-beta5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/reactive_graph/Cargo.toml b/reactive_graph/Cargo.toml index fa5666a9f..b7b503f09 100644 --- a/reactive_graph/Cargo.toml +++ b/reactive_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_graph" -version = "0.1.0-beta4" +version = "0.1.0-beta5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/reactive_graph/src/owner/stored_value.rs b/reactive_graph/src/owner/stored_value.rs index 91aca2851..bfa052854 100644 --- a/reactive_graph/src/owner/stored_value.rs +++ b/reactive_graph/src/owner/stored_value.rs @@ -582,7 +582,7 @@ impl Dispose for StoredValue { #[inline(always)] #[track_caller] #[deprecated( - since = "0.7.0-beta4", + since = "0.7.0-beta5", note = "This function is being removed to conform to Rust idioms. Please \ use `StoredValue::new()` or `StoredValue::new_local()` instead." )] diff --git a/reactive_stores/Cargo.toml b/reactive_stores/Cargo.toml index c6d950fad..edc5f9b72 100644 --- a/reactive_stores/Cargo.toml +++ b/reactive_stores/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_stores" -version = "0.1.0-beta4" +version = "0.1.0-beta5" rust-version.workspace = true edition.workspace = true diff --git a/reactive_stores_macro/Cargo.toml b/reactive_stores_macro/Cargo.toml index 50390e539..713dbd720 100644 --- a/reactive_stores_macro/Cargo.toml +++ b/reactive_stores_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_stores_macro" -version = "0.1.0-beta4" +version = "0.1.0-beta5" rust-version.workspace = true edition.workspace = true diff --git a/router/Cargo.toml b/router/Cargo.toml index c8c775543..07c680430 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router" -version = "0.7.0-beta4" +version = "0.7.0-beta5" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/router_macro/Cargo.toml b/router_macro/Cargo.toml index 2b2a4c16f..d55050414 100644 --- a/router_macro/Cargo.toml +++ b/router_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router_macro" -version = "0.7.0-beta4" +version = "0.7.0-beta5" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/tachys/Cargo.toml b/tachys/Cargo.toml index 6a6bc8104..ed72430c9 100644 --- a/tachys/Cargo.toml +++ b/tachys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tachys" -version = "0.1.0-beta4" +version = "0.1.0-beta5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" From 6baf20275fe58f86a1f9df50e20f3a279e9166c8 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sun, 8 Sep 2024 21:41:44 -0400 Subject: [PATCH 04/13] fix: Resource::refetch() --- leptos_server/src/resource.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/leptos_server/src/resource.rs b/leptos_server/src/resource.rs index 6ab55793b..af9aa12c8 100644 --- a/leptos_server/src/resource.rs +++ b/leptos_server/src/resource.rs @@ -81,13 +81,15 @@ where let is_ready = initial.is_some(); let refetch = ArcRwSignal::new(0); - let source = ArcMemo::new(move |_| source()); + let source = ArcMemo::new({ + let refetch = refetch.clone(); + move |_| (refetch.get(), source()) + }); let fun = { let source = source.clone(); - let refetch = refetch.clone(); move || { - refetch.track(); - fetcher(source.get()) + let (_, source) = source.get(); + fetcher(source) } }; From 7c0889e87321781ba339476f98e49ce19245d08f Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Mon, 9 Sep 2024 18:10:48 -0400 Subject: [PATCH 05/13] fix: do not retrigger parent effect when Suspend's resources resolve (closes #2956) --- tachys/src/reactive_graph/class.rs | 13 ++- tachys/src/reactive_graph/mod.rs | 11 +- tachys/src/reactive_graph/style.rs | 13 ++- tachys/src/reactive_graph/suspense.rs | 146 ++++++++++++++++++++------ 4 files changed, 135 insertions(+), 48 deletions(-) diff --git a/tachys/src/reactive_graph/class.rs b/tachys/src/reactive_graph/class.rs index 4d5bc3e1b..f25e3ef19 100644 --- a/tachys/src/reactive_graph/class.rs +++ b/tachys/src/reactive_graph/class.rs @@ -719,7 +719,7 @@ where } fn to_html(self, style: &mut String) { - if let Some(inner) = self.now_or_never() { + if let Some(inner) = self.inner.now_or_never() { inner.to_html(style); } else { panic!("You cannot use Suspend on an attribute outside Suspense"); @@ -736,7 +736,8 @@ where let state = Rc::clone(&state); async move { *state.borrow_mut() = - Some(self.await.hydrate::(&el)); + Some(self.inner.await.hydrate::(&el)); + self.subscriber.forward(); } }); state @@ -748,7 +749,8 @@ where Executor::spawn_local({ let state = Rc::clone(&state); async move { - *state.borrow_mut() = Some(self.await.build(&el)); + *state.borrow_mut() = Some(self.inner.await.build(&el)); + self.subscriber.forward(); } }); state @@ -758,11 +760,12 @@ where Executor::spawn_local({ let state = Rc::clone(state); async move { - let value = self.await; + let value = self.inner.await; let mut state = state.borrow_mut(); if let Some(state) = state.as_mut() { value.rebuild(state); } + self.subscriber.forward(); } }); } @@ -778,6 +781,6 @@ where fn dry_resolve(&mut self) {} async fn resolve(self) -> Self::AsyncOutput { - self.await + self.inner.await } } diff --git a/tachys/src/reactive_graph/mod.rs b/tachys/src/reactive_graph/mod.rs index 05112414e..59232a909 100644 --- a/tachys/src/reactive_graph/mod.rs +++ b/tachys/src/reactive_graph/mod.rs @@ -401,7 +401,8 @@ where let state = Rc::clone(&state); async move { *state.borrow_mut() = - Some(self.await.hydrate::(&key, &el)); + Some(self.inner.await.hydrate::(&key, &el)); + self.subscriber.forward(); } }); state @@ -414,7 +415,8 @@ where Executor::spawn_local({ let state = Rc::clone(&state); async move { - *state.borrow_mut() = Some(self.await.build(&el, &key)); + *state.borrow_mut() = Some(self.inner.await.build(&el, &key)); + self.subscriber.forward(); } }); state @@ -425,11 +427,12 @@ where Executor::spawn_local({ let state = Rc::clone(state); async move { - let value = self.await; + let value = self.inner.await; let mut state = state.borrow_mut(); if let Some(state) = state.as_mut() { value.rebuild(&key, state); } + self.subscriber.forward(); } }); } @@ -447,7 +450,7 @@ where fn dry_resolve(&mut self) {} async fn resolve(self) -> Self::AsyncOutput { - self.await + self.inner.await } } diff --git a/tachys/src/reactive_graph/style.rs b/tachys/src/reactive_graph/style.rs index 4302fa98d..52b0e3104 100644 --- a/tachys/src/reactive_graph/style.rs +++ b/tachys/src/reactive_graph/style.rs @@ -447,7 +447,7 @@ where type CloneableOwned = Self; fn to_html(self, style: &mut String) { - if let Some(inner) = self.now_or_never() { + if let Some(inner) = self.inner.now_or_never() { inner.to_html(style); } else { panic!("You cannot use Suspend on an attribute outside Suspense"); @@ -464,7 +464,8 @@ where let state = Rc::clone(&state); async move { *state.borrow_mut() = - Some(self.await.hydrate::(&el)); + Some(self.inner.await.hydrate::(&el)); + self.subscriber.forward(); } }); state @@ -476,7 +477,8 @@ where Executor::spawn_local({ let state = Rc::clone(&state); async move { - *state.borrow_mut() = Some(self.await.build(&el)); + *state.borrow_mut() = Some(self.inner.await.build(&el)); + self.subscriber.forward(); } }); state @@ -486,11 +488,12 @@ where Executor::spawn_local({ let state = Rc::clone(state); async move { - let value = self.await; + let value = self.inner.await; let mut state = state.borrow_mut(); if let Some(state) = state.as_mut() { value.rebuild(state); } + self.subscriber.forward(); } }); } @@ -506,6 +509,6 @@ where fn dry_resolve(&mut self) {} async fn resolve(self) -> Self::AsyncOutput { - self.await + self.inner.await } } diff --git a/tachys/src/reactive_graph/suspense.rs b/tachys/src/reactive_graph/suspense.rs index c2d9333f6..5acb3f341 100644 --- a/tachys/src/reactive_graph/suspense.rs +++ b/tachys/src/reactive_graph/suspense.rs @@ -9,57 +9,123 @@ use crate::{ }, }; use any_spawner::Executor; -use futures::{select, FutureExt}; +use futures::{channel::oneshot, select, FutureExt}; +use or_poisoned::OrPoisoned; use reactive_graph::{ computed::{ suspense::{LocalResourceNotifier, SuspenseContext}, ScopedFuture, }, + graph::{ + AnySource, AnySubscriber, Observer, ReactiveNode, Source, Subscriber, + ToAnySubscriber, WithObserver, + }, owner::{provide_context, use_context}, }; use std::{ cell::RefCell, fmt::Debug, future::Future, + mem, pin::Pin, rc::Rc, - task::{Context, Poll}, + sync::{Arc, Mutex, Weak}, }; /// A suspended `Future`, which can be used in the view. #[derive(Clone)] pub struct Suspend { - inner: Pin>>, + pub(crate) subscriber: SuspendSubscriber, + pub(crate) inner: Pin>>, +} + +#[derive(Debug, Clone)] +pub(crate) struct SuspendSubscriber { + inner: Arc, +} + +#[derive(Debug)] +struct SuspendSubscriberInner { + outer_subscriber: Option, + sources: Mutex>, + cancel: Mutex>>, +} + +impl SuspendSubscriber { + pub fn new(cancel: oneshot::Sender<()>) -> Self { + let outer_subscriber = Observer::get(); + Self { + inner: Arc::new(SuspendSubscriberInner { + outer_subscriber, + sources: Default::default(), + cancel: Mutex::new(Some(cancel)), + }), + } + } + + /// Re-links all reactive sources from this to another subscriber. + /// + /// This is used to collect reactive dependencies during the rendering phase, and only later + /// connect them to any outer effect, to prevent the completion of async resources from + /// triggering the render effect to run a second time. + pub fn forward(&self) { + if let Some(to) = &self.inner.outer_subscriber { + let sources = + mem::take(&mut *self.inner.sources.lock().or_poisoned()); + for source in sources { + source.add_subscriber(to.clone()); + to.add_source(source); + } + } + } +} + +impl ReactiveNode for SuspendSubscriberInner { + fn mark_dirty(&self) { + if let Some(tx) = self.cancel.lock().or_poisoned().take() { + _ = tx.send(()); + } + } + + fn mark_check(&self) {} + + fn mark_subscribers_check(&self) {} + + fn update_if_necessary(&self) -> bool { + false + } +} + +impl Subscriber for SuspendSubscriberInner { + fn add_source(&self, source: AnySource) { + self.sources.lock().or_poisoned().push(source); + } + + fn clear_sources(&self, subscriber: &AnySubscriber) { + for source in mem::take(&mut *self.sources.lock().or_poisoned()) { + source.remove_subscriber(subscriber); + } + } +} + +impl ToAnySubscriber for SuspendSubscriber { + fn to_any_subscriber(&self) -> AnySubscriber { + AnySubscriber( + Arc::as_ptr(&self.inner) as usize, + Arc::downgrade(&self.inner) as Weak, + ) + } } impl Suspend { /// Creates a new suspended view. pub fn new(fut: Fut) -> Self { - Self { - inner: Box::pin(ScopedFuture::new(fut)), - } - } -} - -impl Future for Suspend -where - Fut: Future, -{ - type Output = Fut::Output; - - fn poll( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll { - self.inner.as_mut().poll(cx) - } -} - -impl From> for Suspend { - fn from(inner: ScopedFuture) -> Self { - Self { - inner: Box::pin(inner), - } + let (tx, rx) = oneshot::channel(); + let subscriber = SuspendSubscriber::new(tx); + let any_subscriber = subscriber.to_any_subscriber(); + let inner = + any_subscriber.with_observer(|| Box::pin(ScopedFuture::new(fut))); + Self { subscriber, inner } } } @@ -106,10 +172,12 @@ where // TODO cancelation if it fires multiple times fn build(self) -> Self::State { + let Self { subscriber, inner } = self; + // poll the future once immediately // if it's already available, start in the ready state // otherwise, start with the fallback - let mut fut = Box::pin(self); + let mut fut = Box::pin(inner); let initial = fut.as_mut().now_or_never(); let initially_pending = initial.is_none(); let inner = Rc::new(RefCell::new(initial.build())); @@ -127,6 +195,8 @@ where let value = fut.as_mut().await; drop(id); Some(value).rebuild(&mut *state.borrow_mut()); + + subscriber.forward(); } }); } @@ -135,8 +205,10 @@ where } fn rebuild(self, state: &mut Self::State) { + let Self { subscriber, inner } = self; + // get a unique ID if there's a SuspenseContext - let fut = self; + let fut = inner; let id = use_context::().map(|sc| sc.task_id()); // spawn the future, and rebuild the state when it resolves @@ -150,6 +222,8 @@ where // has no parent any_spawner::Executor::tick().await; Some(value).rebuild(&mut *state.borrow_mut()); + + subscriber.forward(); } }); } @@ -208,7 +282,7 @@ where // TODO wrap this with a Suspense as needed // currently this is just used for Routes, which creates a Suspend but never actually needs // it (because we don't lazy-load routes on the server) - if let Some(inner) = self.now_or_never() { + if let Some(inner) = self.inner.now_or_never() { inner.to_html_with_buf(buf, position, escape, mark_branches); } } @@ -222,7 +296,7 @@ where ) where Self: Sized, { - let mut fut = Box::pin(self); + let mut fut = Box::pin(self.inner); match fut.as_mut().now_or_never() { Some(inner) => inner.to_html_async_with_buf::( buf, @@ -287,10 +361,12 @@ where cursor: &Cursor, position: &PositionState, ) -> Self::State { + let Self { subscriber, inner } = self; + // poll the future once immediately // if it's already available, start in the ready state // otherwise, start with the fallback - let mut fut = Box::pin(self); + let mut fut = Box::pin(inner); let initial = fut.as_mut().now_or_never(); let initially_pending = initial.is_none(); let inner = Rc::new(RefCell::new( @@ -310,6 +386,8 @@ where let value = fut.as_mut().await; drop(id); Some(value).rebuild(&mut *state.borrow_mut()); + + subscriber.forward(); } }); } @@ -318,7 +396,7 @@ where } async fn resolve(self) -> Self::AsyncOutput { - Some(self.await) + Some(self.inner.await) } fn dry_resolve(&mut self) { From 346efd66f54e8e7b231f3618a4511003c376feb2 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Mon, 9 Sep 2024 21:19:55 -0400 Subject: [PATCH 06/13] chore: remove unused cancellation logic for now --- tachys/src/reactive_graph/suspense.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tachys/src/reactive_graph/suspense.rs b/tachys/src/reactive_graph/suspense.rs index 5acb3f341..c9167bdb0 100644 --- a/tachys/src/reactive_graph/suspense.rs +++ b/tachys/src/reactive_graph/suspense.rs @@ -9,7 +9,7 @@ use crate::{ }, }; use any_spawner::Executor; -use futures::{channel::oneshot, select, FutureExt}; +use futures::{select, FutureExt}; use or_poisoned::OrPoisoned; use reactive_graph::{ computed::{ @@ -48,17 +48,15 @@ pub(crate) struct SuspendSubscriber { struct SuspendSubscriberInner { outer_subscriber: Option, sources: Mutex>, - cancel: Mutex>>, } impl SuspendSubscriber { - pub fn new(cancel: oneshot::Sender<()>) -> Self { + pub fn new() -> Self { let outer_subscriber = Observer::get(); Self { inner: Arc::new(SuspendSubscriberInner { outer_subscriber, sources: Default::default(), - cancel: Mutex::new(Some(cancel)), }), } } @@ -81,11 +79,7 @@ impl SuspendSubscriber { } impl ReactiveNode for SuspendSubscriberInner { - fn mark_dirty(&self) { - if let Some(tx) = self.cancel.lock().or_poisoned().take() { - _ = tx.send(()); - } - } + fn mark_dirty(&self) {} fn mark_check(&self) {} @@ -120,8 +114,7 @@ impl ToAnySubscriber for SuspendSubscriber { impl Suspend { /// Creates a new suspended view. pub fn new(fut: Fut) -> Self { - let (tx, rx) = oneshot::channel(); - let subscriber = SuspendSubscriber::new(tx); + let subscriber = SuspendSubscriber::new(); let any_subscriber = subscriber.to_any_subscriber(); let inner = any_subscriber.with_observer(|| Box::pin(ScopedFuture::new(fut))); From 6cb10401df17cf051884ec853b6e771bd56cf715 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Mon, 9 Sep 2024 21:20:14 -0400 Subject: [PATCH 07/13] chore(ci): update list of core crates --- .github/workflows/get-leptos-changed.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/get-leptos-changed.yml b/.github/workflows/get-leptos-changed.yml index 947e8fd95..fba4bcf03 100644 --- a/.github/workflows/get-leptos-changed.yml +++ b/.github/workflows/get-leptos-changed.yml @@ -43,11 +43,14 @@ jobs: oco/** or_poisoned/** reactive_graph/** + reactive_stores/** + reactive_stores_macro/** router/** router_macro/** server_fn/** server_fn/server_fn_macro_default/** server_fn_macro/** + tachys/** - name: List source files that changed run: echo '${{ steps.changed-source.outputs.all_changed_files }}' From efc6fc017d65f4b6c8baf6cd9b211603ba6cd16e Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Tue, 10 Sep 2024 06:59:47 -0400 Subject: [PATCH 08/13] fix: forward subscribers for already-resolved Suspend during hydration --- tachys/src/reactive_graph/suspense.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tachys/src/reactive_graph/suspense.rs b/tachys/src/reactive_graph/suspense.rs index c9167bdb0..ffcf912f7 100644 --- a/tachys/src/reactive_graph/suspense.rs +++ b/tachys/src/reactive_graph/suspense.rs @@ -383,6 +383,8 @@ where subscriber.forward(); } }); + } else { + subscriber.forward(); } SuspendState { inner } From 43f2ad7043701077f181233ff078ceba6240e630 Mon Sep 17 00:00:00 2001 From: Baptiste Date: Thu, 12 Sep 2024 02:01:06 +0200 Subject: [PATCH 09/13] chore: add #[track_caller] to `to_any_source` (#2963) --- reactive_graph/src/signal/subscriber_traits.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/reactive_graph/src/signal/subscriber_traits.rs b/reactive_graph/src/signal/subscriber_traits.rs index 4e7819b3c..5d88af9e8 100644 --- a/reactive_graph/src/signal/subscriber_traits.rs +++ b/reactive_graph/src/signal/subscriber_traits.rs @@ -97,6 +97,7 @@ impl ToAnySource for T where T::Output: Borrow>>, { + #[track_caller] fn to_any_source(&self) -> AnySource { self.as_subscriber_set() .map(|subs| { From 4fa6660a3fcb644e5fad53c10579d1342aab15f5 Mon Sep 17 00:00:00 2001 From: Chris <89366859+chrisp60@users.noreply.github.com> Date: Wed, 11 Sep 2024 21:02:02 -0400 Subject: [PATCH 10/13] doc: `router::{Wildcard, Static, Param}Segment` (#2949) --- .../src/matching/horizontal/param_segments.rs | 67 +++++++++++++++++++ .../src/matching/horizontal/static_segment.rs | 31 +++++++++ 2 files changed, 98 insertions(+) diff --git a/router/src/matching/horizontal/param_segments.rs b/router/src/matching/horizontal/param_segments.rs index 94645fa15..c00947f16 100644 --- a/router/src/matching/horizontal/param_segments.rs +++ b/router/src/matching/horizontal/param_segments.rs @@ -2,6 +2,33 @@ use super::{PartialPathMatch, PathSegment, PossibleRouteMatch}; use core::iter; use std::borrow::Cow; +/// A segment that captures a value from the url and maps it to a key. +/// +/// # Examples +/// ```rust +/// # (|| -> Option<()> { // Option does not impl Terminate, so no main +/// use leptos::prelude::*; +/// use leptos_router::{path, ParamSegment, PossibleRouteMatch}; +/// +/// let path = &"/hello"; +/// +/// // Manual definition +/// let manual = (ParamSegment("message"),); +/// let (key, value) = manual.test(path)?.params().last()?; +/// +/// assert_eq!(key, "message"); +/// assert_eq!(value, "hello"); +/// +/// // Macro definition +/// let using_macro = path!("/:message"); +/// let (key, value) = using_macro.test(path)?.params().last()?; +/// +/// assert_eq!(key, "message"); +/// assert_eq!(value, "hello"); +/// +/// # Some(()) +/// # })().unwrap(); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct ParamSegment(pub &'static str); @@ -51,6 +78,46 @@ impl PossibleRouteMatch for ParamSegment { } } +/// A segment that captures all remaining values from the url and maps it to a key. +/// +/// A [`WildcardSegment`] __must__ be the last segment of your path definition. +/// +/// ```rust +/// # (|| -> Option<()> { // Option does not impl Terminate, so no main +/// use leptos::prelude::*; +/// use leptos_router::{ +/// path, ParamSegment, PossibleRouteMatch, StaticSegment, WildcardSegment, +/// }; +/// +/// let path = &"/echo/send/sync/and/static"; +/// +/// // Manual definition +/// let manual = (StaticSegment("echo"), WildcardSegment("kitchen_sink")); +/// let (key, value) = manual.test(path)?.params().last()?; +/// +/// assert_eq!(key, "kitchen_sink"); +/// assert_eq!(value, "send/sync/and/static"); +/// +/// // Macro definition +/// let using_macro = path!("/echo/*else"); +/// let (key, value) = using_macro.test(path)?.params().last()?; +/// +/// assert_eq!(key, "else"); +/// assert_eq!(value, "send/sync/and/static"); +/// +/// // This fails to compile because the macro will catch the bad ordering +/// // let bad = path!("/echo/*foo/bar/:baz"); +/// +/// // This compiles but may not work as you expect at runtime. +/// ( +/// StaticSegment("echo"), +/// WildcardSegment("foo"), +/// ParamSegment("baz"), +/// ); +/// +/// # Some(()) +/// # })().unwrap(); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct WildcardSegment(pub &'static str); diff --git a/router/src/matching/horizontal/static_segment.rs b/router/src/matching/horizontal/static_segment.rs index fa7cdf700..6c7dc0b97 100644 --- a/router/src/matching/horizontal/static_segment.rs +++ b/router/src/matching/horizontal/static_segment.rs @@ -25,6 +25,37 @@ impl AsPath for &'static str { } } +/// A segment that is expected to be static. Not requiring mapping into params. +/// +/// Should work exactly as you would expect. +/// +/// # Examples +/// ```rust +/// # (|| -> Option<()> { // Option does not impl Terminate, so no main +/// use leptos::prelude::*; +/// use leptos_router::{path, PossibleRouteMatch, StaticSegment}; +/// +/// let path = &"/users"; +/// +/// // Manual definition +/// let manual = (StaticSegment("users"),); +/// let matched = manual.test(path)?; +/// assert_eq!(matched.matched(), "/users"); +/// +/// // Params are empty as we had no `ParamSegement`s or `WildcardSegment`s +/// // If you did have additional dynamic segments, this would not be empty. +/// assert_eq!(matched.params().count(), 0); +/// +/// // Macro definition +/// let using_macro = path!("/users"); +/// let matched = manual.test(path)?; +/// assert_eq!(matched.matched(), "/users"); +/// +/// assert_eq!(matched.params().count(), 0); +/// +/// # Some(()) +/// # })().unwrap(); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct StaticSegment(pub T); From a083b57260db4fa6588ff392bf24b4422047c0f6 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Thu, 12 Sep 2024 09:22:46 -0400 Subject: [PATCH 11/13] fix: do not panic in automatic Track implementation if source is disposed (#2964) --- leptos_server/src/local_resource.rs | 15 ++++++++++++++- reactive_graph/src/computed/arc_memo.rs | 12 +++++++++++- .../computed/async_derived/arc_async_derived.rs | 10 +++++++++- .../src/computed/async_derived/async_derived.rs | 13 ++++++++++++- reactive_graph/src/graph/source.rs | 11 +++++++++-- reactive_graph/src/signal/subscriber_traits.rs | 4 ++-- reactive_graph/src/traits.rs | 4 ++++ 7 files changed, 61 insertions(+), 8 deletions(-) diff --git a/leptos_server/src/local_resource.rs b/leptos_server/src/local_resource.rs index 9b1a13946..039194e90 100644 --- a/leptos_server/src/local_resource.rs +++ b/leptos_server/src/local_resource.rs @@ -9,7 +9,7 @@ use reactive_graph::{ }, owner::use_context, signal::guards::{AsyncPlain, ReadGuard}, - traits::{DefinedAt, ReadUntracked}, + traits::{DefinedAt, IsDisposed, ReadUntracked}, }; use send_wrapper::SendWrapper; use std::{ @@ -121,6 +121,13 @@ where } } +impl IsDisposed for ArcLocalResource { + #[inline(always)] + fn is_disposed(&self) -> bool { + false + } +} + impl ToAnySource for ArcLocalResource { fn to_any_source(&self) -> AnySource { self.data.to_any_source() @@ -292,6 +299,12 @@ where } } +impl IsDisposed for LocalResource { + fn is_disposed(&self) -> bool { + self.data.is_disposed() + } +} + impl ToAnySource for LocalResource where T: Send + Sync + 'static, diff --git a/reactive_graph/src/computed/arc_memo.rs b/reactive_graph/src/computed/arc_memo.rs index 32036ae49..b9c43d708 100644 --- a/reactive_graph/src/computed/arc_memo.rs +++ b/reactive_graph/src/computed/arc_memo.rs @@ -9,7 +9,7 @@ use crate::{ guards::{Mapped, Plain, ReadGuard}, ArcReadSignal, ArcRwSignal, }, - traits::{DefinedAt, Get, ReadUntracked}, + traits::{DefinedAt, Get, IsDisposed, ReadUntracked}, }; use core::fmt::Debug; use or_poisoned::OrPoisoned; @@ -260,6 +260,16 @@ where } } +impl IsDisposed for ArcMemo +where + S: Storage, +{ + #[inline(always)] + fn is_disposed(&self) -> bool { + false + } +} + impl ToAnySource for ArcMemo where S: Storage, diff --git a/reactive_graph/src/computed/async_derived/arc_async_derived.rs b/reactive_graph/src/computed/async_derived/arc_async_derived.rs index c2ae334b5..c58b52ea9 100644 --- a/reactive_graph/src/computed/async_derived/arc_async_derived.rs +++ b/reactive_graph/src/computed/async_derived/arc_async_derived.rs @@ -18,7 +18,8 @@ use crate::{ ArcTrigger, }, traits::{ - DefinedAt, Notify, ReadUntracked, Track, UntrackableGuard, Writeable, + DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard, + Writeable, }, transition::AsyncTransition, }; @@ -600,6 +601,13 @@ impl Writeable for ArcAsyncDerived { } } +impl IsDisposed for ArcAsyncDerived { + #[inline(always)] + fn is_disposed(&self) -> bool { + false + } +} + impl ToAnySource for ArcAsyncDerived { fn to_any_source(&self) -> AnySource { AnySource( diff --git a/reactive_graph/src/computed/async_derived/async_derived.rs b/reactive_graph/src/computed/async_derived/async_derived.rs index 3f986e718..611f57781 100644 --- a/reactive_graph/src/computed/async_derived/async_derived.rs +++ b/reactive_graph/src/computed/async_derived/async_derived.rs @@ -7,7 +7,8 @@ use crate::{ owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage}, signal::guards::{AsyncPlain, ReadGuard, WriteGuard}, traits::{ - DefinedAt, Dispose, Notify, ReadUntracked, UntrackableGuard, Writeable, + DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked, + UntrackableGuard, Writeable, }, unwrap_signal, }; @@ -322,6 +323,16 @@ where } } +impl IsDisposed for AsyncDerived +where + T: 'static, + S: Storage>, +{ + fn is_disposed(&self) -> bool { + self.inner.is_disposed() + } +} + impl ToAnySource for AsyncDerived where T: 'static, diff --git a/reactive_graph/src/graph/source.rs b/reactive_graph/src/graph/source.rs index 0558779b6..07d1fce2e 100644 --- a/reactive_graph/src/graph/source.rs +++ b/reactive_graph/src/graph/source.rs @@ -1,10 +1,10 @@ use super::{node::ReactiveNode, AnySubscriber}; -use crate::traits::DefinedAt; +use crate::traits::{DefinedAt, IsDisposed}; use core::{fmt::Debug, hash::Hash}; use std::{panic::Location, sync::Weak}; /// Abstracts over the type of any reactive source. -pub trait ToAnySource { +pub trait ToAnySource: IsDisposed { /// Converts this type to its type-erased equivalent. fn to_any_source(&self) -> AnySource; } @@ -62,6 +62,13 @@ impl PartialEq for AnySource { impl Eq for AnySource {} +impl IsDisposed for AnySource { + #[inline(always)] + fn is_disposed(&self) -> bool { + false + } +} + impl ToAnySource for AnySource { fn to_any_source(&self) -> AnySource { self.clone() diff --git a/reactive_graph/src/signal/subscriber_traits.rs b/reactive_graph/src/signal/subscriber_traits.rs index 5d88af9e8..5fb15ba5f 100644 --- a/reactive_graph/src/signal/subscriber_traits.rs +++ b/reactive_graph/src/signal/subscriber_traits.rs @@ -13,7 +13,7 @@ use crate::{ AnySource, AnySubscriber, ReactiveNode, Source, SubscriberSet, ToAnySource, }, - traits::DefinedAt, + traits::{DefinedAt, IsDisposed}, unwrap_signal, }; use or_poisoned::OrPoisoned; @@ -93,7 +93,7 @@ impl Source for T { } } -impl ToAnySource for T +impl ToAnySource for T where T::Output: Borrow>>, { diff --git a/reactive_graph/src/traits.rs b/reactive_graph/src/traits.rs index a886844e4..4bea8b382 100644 --- a/reactive_graph/src/traits.rs +++ b/reactive_graph/src/traits.rs @@ -107,6 +107,10 @@ pub trait Track { impl Track for T { #[track_caller] fn track(&self) { + if self.is_disposed() { + return; + } + if let Some(subscriber) = Observer::get() { subscriber.add_source(self.to_any_source()); self.add_subscriber(subscriber); From 96a1f80daf9f166ff1d8a8d36720235821a22b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Mond=C3=A9jar=20Rubio?= Date: Thu, 12 Sep 2024 22:43:32 +0200 Subject: [PATCH 12/13] tests: fix `Effect` doctests not being executed, and test-related issues (#2886) --- leptos/src/suspense_component.rs | 6 +- reactive_graph/src/computed.rs | 8 +-- reactive_graph/src/computed/selector.rs | 12 ++-- reactive_graph/src/effect/effect.rs | 24 ++++++-- reactive_graph/src/effect/render_effect.rs | 66 ++++++++++++---------- 5 files changed, 69 insertions(+), 47 deletions(-) diff --git a/leptos/src/suspense_component.rs b/leptos/src/suspense_component.rs index 6acd6ab18..5b1080b72 100644 --- a/leptos/src/suspense_component.rs +++ b/leptos/src/suspense_component.rs @@ -13,7 +13,7 @@ use reactive_graph::{ effect::RenderEffect, owner::{provide_context, use_context, Owner}, signal::ArcRwSignal, - traits::{Get, Read, Track, With}, + traits::{Dispose, Get, Read, Track, With}, }; use slotmap::{DefaultKey, SlotMap}; use tachys::{ @@ -286,7 +286,7 @@ where self.children.dry_resolve(); // check the set of tasks to see if it is empty, now or later - let eff = reactive_graph::effect::RenderEffect::new_isomorphic({ + let eff = reactive_graph::effect::Effect::new_isomorphic({ move |_| { tasks.track(); if tasks.read().is_empty() { @@ -338,7 +338,7 @@ where } children = children => { // clean up the (now useless) effect - drop(eff); + eff.dispose(); Some(OwnedView::new_with_owner(children, owner)) } diff --git a/reactive_graph/src/computed.rs b/reactive_graph/src/computed.rs index 56cf0ee8e..1c905febe 100644 --- a/reactive_graph/src/computed.rs +++ b/reactive_graph/src/computed.rs @@ -163,10 +163,10 @@ where #[deprecated = "This function is being removed to conform to Rust idioms. \ Please use `Selector::new()` instead."] pub fn create_selector( - source: impl Fn() -> T + Clone + 'static, + source: impl Fn() -> T + Clone + Send + Sync + 'static, ) -> Selector where - T: PartialEq + Eq + Clone + std::hash::Hash + 'static, + T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static, { Selector::new(source) } @@ -178,11 +178,11 @@ where #[deprecated = "This function is being removed to conform to Rust idioms. \ Please use `Selector::new_with_fn()` instead."] pub fn create_selector_with_fn( - source: impl Fn() -> T + Clone + 'static, + source: impl Fn() -> T + Clone + Send + Sync + 'static, f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static, ) -> Selector where - T: PartialEq + Eq + Clone + std::hash::Hash + 'static, + T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static, { Selector::new_with_fn(source, f) } diff --git a/reactive_graph/src/computed/selector.rs b/reactive_graph/src/computed/selector.rs index d359abfe8..9db0cdfe8 100644 --- a/reactive_graph/src/computed/selector.rs +++ b/reactive_graph/src/computed/selector.rs @@ -30,7 +30,7 @@ use std::{ /// let a = RwSignal::new(0); /// let is_selected = Selector::new(move || a.get()); /// let total_notifications = StoredValue::new(0); -/// Effect::new({ +/// Effect::new_isomorphic({ /// let is_selected = is_selected.clone(); /// move |_| { /// if is_selected.selected(5) { @@ -55,7 +55,7 @@ use std::{ /// /// # any_spawner::Executor::tick().await; /// assert_eq!(is_selected.selected(5), false); -/// # }); +/// # }).await; /// # }); /// ``` #[derive(Clone)] @@ -74,17 +74,17 @@ where impl Selector where - T: PartialEq + Eq + Clone + Hash + 'static, + T: PartialEq + Send + Sync + Eq + Clone + Hash + 'static, { /// Creates a new selector that compares values using [`PartialEq`]. - pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self { + pub fn new(source: impl Fn() -> T + Send + Sync + Clone + 'static) -> Self { Self::new_with_fn(source, PartialEq::eq) } /// Creates a new selector that compares values by returning `true` from a comparator function /// if the values are the same. pub fn new_with_fn( - source: impl Fn() -> T + Clone + 'static, + source: impl Fn() -> T + Clone + Send + Sync + 'static, f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static, ) -> Self { let subs: Arc>>> = @@ -92,7 +92,7 @@ where let v: Arc>> = Default::default(); let f = Arc::new(f) as Arc bool + Send + Sync>; - let effect = Arc::new(RenderEffect::new({ + let effect = Arc::new(RenderEffect::new_isomorphic({ let subs = Arc::clone(&subs); let f = Arc::clone(&f); let v = Arc::clone(&v); diff --git a/reactive_graph/src/effect/effect.rs b/reactive_graph/src/effect/effect.rs index 0aa55a3af..d98567730 100644 --- a/reactive_graph/src/effect/effect.rs +++ b/reactive_graph/src/effect/effect.rs @@ -43,6 +43,7 @@ use std::{ /// # use reactive_graph::owner::StoredValue; /// # tokio_test::block_on(async move { /// # tokio::task::LocalSet::new().run_until(async move { +/// # any_spawner::Executor::init_tokio(); /// let a = RwSignal::new(0); /// let b = RwSignal::new(0); /// @@ -52,7 +53,9 @@ use std::{ /// println!("Value: {}", a.get()); /// }); /// +/// # assert_eq!(a.get(), 0); /// a.set(1); +/// # assert_eq!(a.get(), 1); /// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1" /// /// // ❌ don't use effects to synchronize state within the reactive system @@ -61,7 +64,7 @@ use std::{ /// // and easily lead to problems like infinite loops /// b.set(a.get() + 1); /// }); -/// # }); +/// # }).await; /// # }); /// ``` /// ## Web-Specific Notes @@ -182,6 +185,7 @@ impl Effect { /// # use reactive_graph::signal::signal; /// # tokio_test::block_on(async move { /// # tokio::task::LocalSet::new().run_until(async move { + /// # any_spawner::Executor::init_tokio(); /// # /// let (num, set_num) = signal(0); /// @@ -192,13 +196,16 @@ impl Effect { /// }, /// false, /// ); + /// # assert_eq!(num.get(), 0); /// /// set_num.set(1); // > "Number: 1; Prev: Some(0)" + /// # assert_eq!(num.get(), 1); /// /// effect.stop(); // stop watching /// /// set_num.set(2); // (nothing happens) - /// # }); + /// # assert_eq!(num.get(), 2); + /// # }).await; /// # }); /// ``` /// @@ -210,6 +217,7 @@ impl Effect { /// # use reactive_graph::signal::signal; /// # tokio_test::block_on(async move { /// # tokio::task::LocalSet::new().run_until(async move { + /// # any_spawner::Executor::init_tokio(); /// # /// let (num, set_num) = signal(0); /// let (cb_num, set_cb_num) = signal(0); @@ -222,12 +230,17 @@ impl Effect { /// false, /// ); /// + /// # assert_eq!(num.get(), 0); /// set_num.set(1); // > "Number: 1; Cb: 0" + /// # assert_eq!(num.get(), 1); /// + /// # assert_eq!(cb_num.get(), 0); /// set_cb_num.set(1); // (nothing happens) + /// # assert_eq!(cb_num.get(), 1); /// /// set_num.set(2); // > "Number: 2; Cb: 1" - /// # }); + /// # assert_eq!(num.get(), 2); + /// # }).await; /// # }); /// ``` /// @@ -243,6 +256,7 @@ impl Effect { /// # use reactive_graph::signal::signal; /// # tokio_test::block_on(async move { /// # tokio::task::LocalSet::new().run_until(async move { + /// # any_spawner::Executor::init_tokio(); /// # /// let (num, set_num) = signal(0); /// @@ -254,8 +268,10 @@ impl Effect { /// true, /// ); // > "Number: 0; Prev: None" /// + /// # assert_eq!(num.get(), 0); /// set_num.set(1); // > "Number: 1; Prev: Some(0)" - /// # }); + /// # assert_eq!(num.get(), 1); + /// # }).await; /// # }); /// ``` pub fn watch( diff --git a/reactive_graph/src/effect/render_effect.rs b/reactive_graph/src/effect/render_effect.rs index ced3b4700..753bbf856 100644 --- a/reactive_graph/src/effect/render_effect.rs +++ b/reactive_graph/src/effect/render_effect.rs @@ -135,44 +135,50 @@ where { /// Creates a render effect that will run whether the `effects` feature is enabled or not. pub fn new_isomorphic( - mut fun: impl FnMut(Option) -> T + Send + 'static, + fun: impl FnMut(Option) -> T + Send + Sync + 'static, ) -> Self { - let (mut observer, mut rx) = channel(); - observer.notify(); + fn erased( + mut fun: Box) -> T + Send + Sync + 'static>, + ) -> RenderEffect { + let (observer, mut rx) = channel(); + let value = Arc::new(RwLock::new(None::)); + let owner = Owner::new(); + let inner = Arc::new(RwLock::new(EffectInner { + dirty: false, + observer, + sources: SourceSet::new(), + })); - let value = Arc::new(RwLock::new(None::)); - let owner = Owner::new(); - let inner = Arc::new(RwLock::new(EffectInner { - dirty: false, - observer, - sources: SourceSet::new(), - })); - let mut first_run = true; + let initial_value = owner + .with(|| inner.to_any_subscriber().with_observer(|| fun(None))); + *value.write().or_poisoned() = Some(initial_value); - Executor::spawn({ - let value = Arc::clone(&value); - let subscriber = inner.to_any_subscriber(); + Executor::spawn({ + let value = Arc::clone(&value); + let subscriber = inner.to_any_subscriber(); - async move { - while rx.next().await.is_some() { - if first_run - || subscriber + async move { + while rx.next().await.is_some() { + if subscriber .with_observer(|| subscriber.update_if_necessary()) - { - first_run = false; - subscriber.clear_sources(&subscriber); + { + subscriber.clear_sources(&subscriber); - let old_value = - mem::take(&mut *value.write().or_poisoned()); - let new_value = owner.with_cleanup(|| { - subscriber.with_observer(|| fun(old_value)) - }); - *value.write().or_poisoned() = Some(new_value); + let old_value = + mem::take(&mut *value.write().or_poisoned()); + let new_value = owner.with_cleanup(|| { + subscriber.with_observer(|| fun(old_value)) + }); + *value.write().or_poisoned() = Some(new_value); + } } } - } - }); - RenderEffect { value, inner } + }); + + RenderEffect { value, inner } + } + + erased(Box::new(fun)) } } From d7881ccfb543ed998618b799ec6e89d6eaf70568 Mon Sep 17 00:00:00 2001 From: luoxiaozero <48741584+luoxiaozero@users.noreply.github.com> Date: Fri, 13 Sep 2024 21:35:59 +0800 Subject: [PATCH 13/13] fix: allow component to use span prop when tracing feature is enabled (#2969) --- leptos_macro/src/component.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs index cbc473f53..19c296275 100644 --- a/leptos_macro/src/component.rs +++ b/leptos_macro/src/component.rs @@ -204,11 +204,11 @@ impl ToTokens for Model { )] }, quote! { - let span = ::leptos::tracing::Span::current(); + let __span = ::leptos::tracing::Span::current(); }, quote! { #[cfg(debug_assertions)] - let _guard = span.entered(); + let _guard = __span.entered(); }, if no_props || !cfg!(feature = "trace-component-props") { quote!()