Merge branch 'main' into more-stores

This commit is contained in:
Greg Johnston 2024-09-13 17:31:54 -04:00 committed by GitHub
commit dc9fbb0585
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 578 additions and 214 deletions

View file

@ -43,11 +43,14 @@ jobs:
oco/** oco/**
or_poisoned/** or_poisoned/**
reactive_graph/** reactive_graph/**
reactive_stores/**
reactive_stores_macro/**
router/** router/**
router_macro/** router_macro/**
server_fn/** server_fn/**
server_fn/server_fn_macro_default/** server_fn/server_fn_macro_default/**
server_fn_macro/** server_fn_macro/**
tachys/**
- name: List source files that changed - name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}' run: echo '${{ steps.changed-source.outputs.all_changed_files }}'

View file

@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"] exclude = ["benchmarks", "examples", "projects"]
[workspace.package] [workspace.package]
version = "0.7.0-beta4" version = "0.7.0-beta5"
edition = "2021" edition = "2021"
rust-version = "1.76" rust-version = "1.76"
[workspace.dependencies] [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" } any_spawner = { path = "./any_spawner/", version = "0.1.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", 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" } either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta4" } hydration_context = { path = "./hydration_context", version = "0.2.0-beta5" }
leptos = { path = "./leptos", version = "0.7.0-beta4" } leptos = { path = "./leptos", version = "0.7.0-beta5" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta4" } leptos_config = { path = "./leptos_config", version = "0.7.0-beta5" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta4" } leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta5" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta4" } leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta4" } leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta5" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta4" } leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta5" }
leptos_router = { path = "./router", version = "0.7.0-beta4" } leptos_router = { path = "./router", version = "0.7.0-beta5" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta4" } leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta5" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta4" } leptos_server = { path = "./leptos_server", version = "0.7.0-beta5" }
leptos_meta = { path = "./meta", version = "0.7.0-beta4" } leptos_meta = { path = "./meta", version = "0.7.0-beta5" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta4" } next_tuple = { path = "./next_tuple", version = "0.1.0-beta5" }
oco_ref = { path = "./oco", version = "0.2.0" } oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" } or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta4" } reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta5" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta4" } reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta5" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta4" } reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta5" }
server_fn = { path = "./server_fn", version = "0.7.0-beta4" } server_fn = { path = "./server_fn", version = "0.7.0-beta5" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta4" } 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-beta4" } server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta5" }
tachys = { path = "./tachys", version = "0.1.0-beta4" } tachys = { path = "./tachys", version = "0.1.0-beta5" }
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1

View file

@ -1,6 +1,6 @@
[package] [package]
name = "throw_error" name = "throw_error"
version = "0.2.0-beta4" version = "0.2.0-beta5"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"
readme = "../README.md" readme = "../README.md"

View file

@ -32,7 +32,6 @@ pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc. // Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context(); provide_meta_context();
let fallback = || view! { "Page not found." }.into_view(); let fallback = || view! { "Page not found." }.into_view();
let ssr = SsrMode::Async;
view! { view! {
<Stylesheet id="leptos" href="/pkg/axum_js_ssr.css"/> <Stylesheet id="leptos" href="/pkg/axum_js_ssr.css"/>
@ -79,19 +78,19 @@ pub fn App() -> impl IntoView {
<h1>"Leptos JavaScript Integration Demo with SSR in Axum"</h1> <h1>"Leptos JavaScript Integration Demo with SSR in Axum"</h1>
<FlatRoutes fallback> <FlatRoutes fallback>
<Route path=path!("") view=HomePage/> <Route path=path!("") view=HomePage/>
<Route path=path!("naive") view=Naive ssr/> <Route path=path!("naive") view=Naive ssr=SsrMode::Async/>
<Route path=path!("naive-alt") view=|| view! { <NaiveEvent/> } ssr/> <Route path=path!("naive-alt") view=|| view! { <NaiveEvent/> } ssr=SsrMode::Async/>
<Route path=path!("naive-hook") view=|| view! { <NaiveEvent hook=true/> } ssr/> <Route path=path!("naive-hook") view=|| view! { <NaiveEvent hook=true/> } ssr=SsrMode::Async/>
<Route path=path!("naive-fallback") view=|| view! { <Route path=path!("naive-fallback") view=|| view! {
<NaiveEvent hook=true fallback=true/> <NaiveEvent hook=true fallback=true/>
} ssr/> } ssr=SsrMode::Async/>
<Route path=path!("signal-effect-script") view=CodeDemoSignalEffect ssr/> <Route path=path!("signal-effect-script") view=CodeDemoSignalEffect ssr=SsrMode::Async/>
<Route path=path!("custom-event") view=CustomEvent ssr/> <Route path=path!("custom-event") view=CustomEvent ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-naive") view=WasmBindgenNaive ssr/> <Route path=path!("wasm-bindgen-naive") view=WasmBindgenNaive ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-event") view=WasmBindgenJSHookReadyEvent ssr/> <Route path=path!("wasm-bindgen-event") view=WasmBindgenJSHookReadyEvent ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-effect") view=WasmBindgenEffect ssr/> <Route path=path!("wasm-bindgen-effect") view=WasmBindgenEffect ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-direct") view=WasmBindgenDirect ssr/> <Route path=path!("wasm-bindgen-direct") view=WasmBindgenDirect ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-direct-fixed") view=WasmBindgenDirectFixed ssr/> <Route path=path!("wasm-bindgen-direct-fixed") view=WasmBindgenDirectFixed ssr=SsrMode::Async/>
</FlatRoutes> </FlatRoutes>
</article> </article>
</main> </main>

View file

@ -1,6 +1,6 @@
[package] [package]
name = "hydration_context" name = "hydration_context"
version = "0.2.0-beta4" version = "0.2.0-beta5"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"
readme = "../README.md" readme = "../README.md"

View file

@ -13,7 +13,7 @@ use reactive_graph::{
effect::RenderEffect, effect::RenderEffect,
owner::{provide_context, use_context, Owner}, owner::{provide_context, use_context, Owner},
signal::ArcRwSignal, signal::ArcRwSignal,
traits::{Get, Read, Track, With}, traits::{Dispose, Get, Read, Track, With},
}; };
use slotmap::{DefaultKey, SlotMap}; use slotmap::{DefaultKey, SlotMap};
use tachys::{ use tachys::{
@ -286,7 +286,7 @@ where
self.children.dry_resolve(); self.children.dry_resolve();
// check the set of tasks to see if it is empty, now or later // 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 |_| { move |_| {
tasks.track(); tasks.track();
if tasks.read().is_empty() { if tasks.read().is_empty() {
@ -338,7 +338,7 @@ where
} }
children = children => { children = children => {
// clean up the (now useless) effect // clean up the (now useless) effect
drop(eff); eff.dispose();
Some(OwnedView::new_with_owner(children, owner)) Some(OwnedView::new_with_owner(children, owner))
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "leptos_macro" name = "leptos_macro"
version = "0.7.0-beta4" version = "0.7.0-beta5"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"
repository = "https://github.com/leptos-rs/leptos" repository = "https://github.com/leptos-rs/leptos"

View file

@ -204,11 +204,11 @@ impl ToTokens for Model {
)] )]
}, },
quote! { quote! {
let span = ::leptos::tracing::Span::current(); let __span = ::leptos::tracing::Span::current();
}, },
quote! { quote! {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let _guard = span.entered(); let _guard = __span.entered();
}, },
if no_props || !cfg!(feature = "trace-component-props") { if no_props || !cfg!(feature = "trace-component-props") {
quote!() quote!()

View file

@ -9,7 +9,7 @@ use reactive_graph::{
}, },
owner::use_context, owner::use_context,
signal::guards::{AsyncPlain, ReadGuard}, signal::guards::{AsyncPlain, ReadGuard},
traits::{DefinedAt, ReadUntracked}, traits::{DefinedAt, IsDisposed, ReadUntracked},
}; };
use send_wrapper::SendWrapper; use send_wrapper::SendWrapper;
use std::{ use std::{
@ -121,6 +121,13 @@ where
} }
} }
impl<T: 'static> IsDisposed for ArcLocalResource<T> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T: 'static> ToAnySource for ArcLocalResource<T> { impl<T: 'static> ToAnySource for ArcLocalResource<T> {
fn to_any_source(&self) -> AnySource { fn to_any_source(&self) -> AnySource {
self.data.to_any_source() self.data.to_any_source()
@ -292,6 +299,12 @@ where
} }
} }
impl<T: 'static> IsDisposed for LocalResource<T> {
fn is_disposed(&self) -> bool {
self.data.is_disposed()
}
}
impl<T: 'static> ToAnySource for LocalResource<T> impl<T: 'static> ToAnySource for LocalResource<T>
where where
T: Send + Sync + 'static, T: Send + Sync + 'static,

View file

@ -81,13 +81,15 @@ where
let is_ready = initial.is_some(); let is_ready = initial.is_some();
let refetch = ArcRwSignal::new(0); 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 fun = {
let source = source.clone(); let source = source.clone();
let refetch = refetch.clone();
move || { move || {
refetch.track(); let (_, source) = source.get();
fetcher(source.get()) fetcher(source)
} }
}; };

View file

@ -1,6 +1,6 @@
[package] [package]
name = "leptos_meta" name = "leptos_meta"
version = "0.7.0-beta4" version = "0.7.0-beta5"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"
repository = "https://github.com/leptos-rs/leptos" repository = "https://github.com/leptos-rs/leptos"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "next_tuple" name = "next_tuple"
version = "0.1.0-beta4" version = "0.1.0-beta5"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"
readme = "../README.md" readme = "../README.md"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "reactive_graph" name = "reactive_graph"
version = "0.1.0-beta4" version = "0.1.0-beta5"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"
readme = "../README.md" readme = "../README.md"

View file

@ -163,10 +163,10 @@ where
#[deprecated = "This function is being removed to conform to Rust idioms. \ #[deprecated = "This function is being removed to conform to Rust idioms. \
Please use `Selector::new()` instead."] Please use `Selector::new()` instead."]
pub fn create_selector<T>( pub fn create_selector<T>(
source: impl Fn() -> T + Clone + 'static, source: impl Fn() -> T + Clone + Send + Sync + 'static,
) -> Selector<T> ) -> Selector<T>
where where
T: PartialEq + Eq + Clone + std::hash::Hash + 'static, T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static,
{ {
Selector::new(source) Selector::new(source)
} }
@ -178,11 +178,11 @@ where
#[deprecated = "This function is being removed to conform to Rust idioms. \ #[deprecated = "This function is being removed to conform to Rust idioms. \
Please use `Selector::new_with_fn()` instead."] Please use `Selector::new_with_fn()` instead."]
pub fn create_selector_with_fn<T>( pub fn create_selector_with_fn<T>(
source: impl Fn() -> T + Clone + 'static, source: impl Fn() -> T + Clone + Send + Sync + 'static,
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static, f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
) -> Selector<T> ) -> Selector<T>
where 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) Selector::new_with_fn(source, f)
} }

View file

@ -9,7 +9,7 @@ use crate::{
guards::{Mapped, Plain, ReadGuard}, guards::{Mapped, Plain, ReadGuard},
ArcReadSignal, ArcRwSignal, ArcReadSignal, ArcRwSignal,
}, },
traits::{DefinedAt, Get, ReadUntracked}, traits::{DefinedAt, Get, IsDisposed, ReadUntracked},
}; };
use core::fmt::Debug; use core::fmt::Debug;
use or_poisoned::OrPoisoned; use or_poisoned::OrPoisoned;
@ -260,6 +260,16 @@ where
} }
} }
impl<T: 'static, S> IsDisposed for ArcMemo<T, S>
where
S: Storage<T>,
{
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T: 'static, S> ToAnySource for ArcMemo<T, S> impl<T: 'static, S> ToAnySource for ArcMemo<T, S>
where where
S: Storage<T>, S: Storage<T>,

View file

@ -18,7 +18,8 @@ use crate::{
ArcTrigger, ArcTrigger,
}, },
traits::{ traits::{
DefinedAt, ReadUntracked, Track, Trigger, UntrackableGuard, Writeable, DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
}, },
transition::AsyncTransition, transition::AsyncTransition,
}; };
@ -580,8 +581,8 @@ impl<T: 'static> ReadUntracked for ArcAsyncDerived<T> {
} }
} }
impl<T: 'static> Trigger for ArcAsyncDerived<T> { impl<T: 'static> Notify for ArcAsyncDerived<T> {
fn trigger(&self) { fn notify(&self) {
Self::notify_subs(&self.wakers, &self.inner, &self.loading, None); Self::notify_subs(&self.wakers, &self.inner, &self.loading, None);
} }
} }
@ -600,6 +601,13 @@ impl<T: 'static> Writeable for ArcAsyncDerived<T> {
} }
} }
impl<T: 'static> IsDisposed for ArcAsyncDerived<T> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T: 'static> ToAnySource for ArcAsyncDerived<T> { impl<T: 'static> ToAnySource for ArcAsyncDerived<T> {
fn to_any_source(&self) -> AnySource { fn to_any_source(&self) -> AnySource {
AnySource( AnySource(

View file

@ -7,7 +7,8 @@ use crate::{
owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage}, owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage},
signal::guards::{AsyncPlain, ReadGuard, WriteGuard}, signal::guards::{AsyncPlain, ReadGuard, WriteGuard},
traits::{ traits::{
DefinedAt, Dispose, ReadUntracked, Trigger, UntrackableGuard, Writeable, DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
UntrackableGuard, Writeable,
}, },
unwrap_signal, unwrap_signal,
}; };
@ -291,13 +292,13 @@ where
} }
} }
impl<T, S> Trigger for AsyncDerived<T, S> impl<T, S> Notify for AsyncDerived<T, S>
where where
T: 'static, T: 'static,
S: Storage<ArcAsyncDerived<T>>, S: Storage<ArcAsyncDerived<T>>,
{ {
fn trigger(&self) { fn notify(&self) {
self.inner.try_with_value(|inner| inner.trigger()); self.inner.try_with_value(|inner| inner.notify());
} }
} }
@ -322,6 +323,16 @@ where
} }
} }
impl<T, S> IsDisposed for AsyncDerived<T, S>
where
T: 'static,
S: Storage<ArcAsyncDerived<T>>,
{
fn is_disposed(&self) -> bool {
self.inner.is_disposed()
}
}
impl<T, S> ToAnySource for AsyncDerived<T, S> impl<T, S> ToAnySource for AsyncDerived<T, S>
where where
T: 'static, T: 'static,

View file

@ -30,7 +30,7 @@ use std::{
/// let a = RwSignal::new(0); /// let a = RwSignal::new(0);
/// let is_selected = Selector::new(move || a.get()); /// let is_selected = Selector::new(move || a.get());
/// let total_notifications = StoredValue::new(0); /// let total_notifications = StoredValue::new(0);
/// Effect::new({ /// Effect::new_isomorphic({
/// let is_selected = is_selected.clone(); /// let is_selected = is_selected.clone();
/// move |_| { /// move |_| {
/// if is_selected.selected(5) { /// if is_selected.selected(5) {
@ -55,7 +55,7 @@ use std::{
/// ///
/// # any_spawner::Executor::tick().await; /// # any_spawner::Executor::tick().await;
/// assert_eq!(is_selected.selected(5), false); /// assert_eq!(is_selected.selected(5), false);
/// # }); /// # }).await;
/// # }); /// # });
/// ``` /// ```
#[derive(Clone)] #[derive(Clone)]
@ -74,17 +74,17 @@ where
impl<T> Selector<T> impl<T> Selector<T>
where where
T: PartialEq + Eq + Clone + Hash + 'static, T: PartialEq + Send + Sync + Eq + Clone + Hash + 'static,
{ {
/// Creates a new selector that compares values using [`PartialEq`]. /// 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) Self::new_with_fn(source, PartialEq::eq)
} }
/// Creates a new selector that compares values by returning `true` from a comparator function /// Creates a new selector that compares values by returning `true` from a comparator function
/// if the values are the same. /// if the values are the same.
pub fn new_with_fn( 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, f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
) -> Self { ) -> Self {
let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> = let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
@ -92,7 +92,7 @@ where
let v: Arc<RwLock<Option<T>>> = Default::default(); let v: Arc<RwLock<Option<T>>> = Default::default();
let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>; let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>;
let effect = Arc::new(RenderEffect::new({ let effect = Arc::new(RenderEffect::new_isomorphic({
let subs = Arc::clone(&subs); let subs = Arc::clone(&subs);
let f = Arc::clone(&f); let f = Arc::clone(&f);
let v = Arc::clone(&v); let v = Arc::clone(&v);

View file

@ -43,6 +43,7 @@ use std::{
/// # use reactive_graph::owner::StoredValue; /// # use reactive_graph::owner::StoredValue;
/// # tokio_test::block_on(async move { /// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move { /// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// let a = RwSignal::new(0); /// let a = RwSignal::new(0);
/// let b = RwSignal::new(0); /// let b = RwSignal::new(0);
/// ///
@ -52,7 +53,9 @@ use std::{
/// println!("Value: {}", a.get()); /// println!("Value: {}", a.get());
/// }); /// });
/// ///
/// # assert_eq!(a.get(), 0);
/// a.set(1); /// a.set(1);
/// # assert_eq!(a.get(), 1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 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 /// // ❌ 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 /// // and easily lead to problems like infinite loops
/// b.set(a.get() + 1); /// b.set(a.get() + 1);
/// }); /// });
/// # }); /// # }).await;
/// # }); /// # });
/// ``` /// ```
/// ## Web-Specific Notes /// ## Web-Specific Notes
@ -182,6 +185,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal; /// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move { /// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move { /// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// # /// #
/// let (num, set_num) = signal(0); /// let (num, set_num) = signal(0);
/// ///
@ -192,13 +196,16 @@ impl Effect<LocalStorage> {
/// }, /// },
/// false, /// false,
/// ); /// );
/// # assert_eq!(num.get(), 0);
/// ///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)" /// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # assert_eq!(num.get(), 1);
/// ///
/// effect.stop(); // stop watching /// effect.stop(); // stop watching
/// ///
/// set_num.set(2); // (nothing happens) /// set_num.set(2); // (nothing happens)
/// # }); /// # assert_eq!(num.get(), 2);
/// # }).await;
/// # }); /// # });
/// ``` /// ```
/// ///
@ -210,6 +217,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal; /// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move { /// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move { /// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// # /// #
/// let (num, set_num) = signal(0); /// let (num, set_num) = signal(0);
/// let (cb_num, set_cb_num) = signal(0); /// let (cb_num, set_cb_num) = signal(0);
@ -222,12 +230,17 @@ impl Effect<LocalStorage> {
/// false, /// false,
/// ); /// );
/// ///
/// # assert_eq!(num.get(), 0);
/// set_num.set(1); // > "Number: 1; Cb: 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) /// set_cb_num.set(1); // (nothing happens)
/// # assert_eq!(cb_num.get(), 1);
/// ///
/// set_num.set(2); // > "Number: 2; Cb: 1" /// set_num.set(2); // > "Number: 2; Cb: 1"
/// # }); /// # assert_eq!(num.get(), 2);
/// # }).await;
/// # }); /// # });
/// ``` /// ```
/// ///
@ -243,6 +256,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal; /// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move { /// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move { /// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// # /// #
/// let (num, set_num) = signal(0); /// let (num, set_num) = signal(0);
/// ///
@ -254,8 +268,10 @@ impl Effect<LocalStorage> {
/// true, /// true,
/// ); // > "Number: 0; Prev: None" /// ); // > "Number: 0; Prev: None"
/// ///
/// # assert_eq!(num.get(), 0);
/// set_num.set(1); // > "Number: 1; Prev: Some(0)" /// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # }); /// # assert_eq!(num.get(), 1);
/// # }).await;
/// # }); /// # });
/// ``` /// ```
pub fn watch<D, T>( pub fn watch<D, T>(

View file

@ -135,44 +135,50 @@ where
{ {
/// Creates a render effect that will run whether the `effects` feature is enabled or not. /// Creates a render effect that will run whether the `effects` feature is enabled or not.
pub fn new_isomorphic( pub fn new_isomorphic(
mut fun: impl FnMut(Option<T>) -> T + Send + 'static, fun: impl FnMut(Option<T>) -> T + Send + Sync + 'static,
) -> Self { ) -> Self {
let (mut observer, mut rx) = channel(); fn erased<T: Send + Sync + 'static>(
observer.notify(); mut fun: Box<dyn FnMut(Option<T>) -> T + Send + Sync + 'static>,
) -> RenderEffect<T> {
let (observer, mut rx) = channel();
let value = Arc::new(RwLock::new(None::<T>));
let owner = Owner::new();
let inner = Arc::new(RwLock::new(EffectInner {
dirty: false,
observer,
sources: SourceSet::new(),
}));
let value = Arc::new(RwLock::new(None::<T>)); let initial_value = owner
let owner = Owner::new(); .with(|| inner.to_any_subscriber().with_observer(|| fun(None)));
let inner = Arc::new(RwLock::new(EffectInner { *value.write().or_poisoned() = Some(initial_value);
dirty: false,
observer,
sources: SourceSet::new(),
}));
let mut first_run = true;
Executor::spawn({ Executor::spawn({
let value = Arc::clone(&value); let value = Arc::clone(&value);
let subscriber = inner.to_any_subscriber(); let subscriber = inner.to_any_subscriber();
async move { async move {
while rx.next().await.is_some() { while rx.next().await.is_some() {
if first_run if subscriber
|| subscriber
.with_observer(|| subscriber.update_if_necessary()) .with_observer(|| subscriber.update_if_necessary())
{ {
first_run = false; subscriber.clear_sources(&subscriber);
subscriber.clear_sources(&subscriber);
let old_value = let old_value =
mem::take(&mut *value.write().or_poisoned()); mem::take(&mut *value.write().or_poisoned());
let new_value = owner.with_cleanup(|| { let new_value = owner.with_cleanup(|| {
subscriber.with_observer(|| fun(old_value)) subscriber.with_observer(|| fun(old_value))
}); });
*value.write().or_poisoned() = Some(new_value); *value.write().or_poisoned() = Some(new_value);
}
} }
} }
} });
});
RenderEffect { value, inner } RenderEffect { value, inner }
}
erased(Box::new(fun))
} }
} }

View file

@ -1,10 +1,10 @@
use super::{node::ReactiveNode, AnySubscriber}; use super::{node::ReactiveNode, AnySubscriber};
use crate::traits::DefinedAt; use crate::traits::{DefinedAt, IsDisposed};
use core::{fmt::Debug, hash::Hash}; use core::{fmt::Debug, hash::Hash};
use std::{panic::Location, sync::Weak}; use std::{panic::Location, sync::Weak};
/// Abstracts over the type of any reactive source. /// Abstracts over the type of any reactive source.
pub trait ToAnySource { pub trait ToAnySource: IsDisposed {
/// Converts this type to its type-erased equivalent. /// Converts this type to its type-erased equivalent.
fn to_any_source(&self) -> AnySource; fn to_any_source(&self) -> AnySource;
} }
@ -62,6 +62,13 @@ impl PartialEq for AnySource {
impl Eq for AnySource {} impl Eq for AnySource {}
impl IsDisposed for AnySource {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl ToAnySource for AnySource { impl ToAnySource for AnySource {
fn to_any_source(&self) -> AnySource { fn to_any_source(&self) -> AnySource {
self.clone() self.clone()

View file

@ -582,7 +582,7 @@ impl<T, S> Dispose for StoredValue<T, S> {
#[inline(always)] #[inline(always)]
#[track_caller] #[track_caller]
#[deprecated( #[deprecated(
since = "0.7.0-beta4", since = "0.7.0-beta5",
note = "This function is being removed to conform to Rust idioms. Please \ note = "This function is being removed to conform to Rust idioms. Please \
use `StoredValue::new()` or `StoredValue::new_local()` instead." use `StoredValue::new()` or `StoredValue::new_local()` instead."
)] )]

View file

@ -9,6 +9,7 @@ pub mod guards;
mod read; mod read;
mod rw; mod rw;
mod subscriber_traits; mod subscriber_traits;
mod trigger;
mod write; mod write;
use crate::owner::LocalStorage; use crate::owner::LocalStorage;
@ -18,6 +19,7 @@ pub use arc_trigger::*;
pub use arc_write::*; pub use arc_write::*;
pub use read::*; pub use read::*;
pub use rw::*; pub use rw::*;
pub use trigger::*;
pub use write::*; pub use write::*;
/// Creates a reference-counted signal. /// Creates a reference-counted signal.

View file

@ -5,7 +5,7 @@ use super::{
}; };
use crate::{ use crate::{
graph::{ReactiveNode, SubscriberSet}, graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Trigger}, prelude::{IsDisposed, Notify},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Writeable}, traits::{DefinedAt, ReadUntracked, UntrackableGuard, Writeable},
}; };
use core::fmt::{Debug, Formatter, Result}; use core::fmt::{Debug, Formatter, Result};
@ -247,8 +247,8 @@ impl<T: 'static> ReadUntracked for ArcRwSignal<T> {
} }
} }
impl<T> Trigger for ArcRwSignal<T> { impl<T> Notify for ArcRwSignal<T> {
fn trigger(&self) { fn notify(&self) {
self.mark_dirty(); self.mark_dirty();
} }
} }

View file

@ -1,7 +1,7 @@
use super::subscriber_traits::AsSubscriberSet; use super::subscriber_traits::AsSubscriberSet;
use crate::{ use crate::{
graph::{ReactiveNode, SubscriberSet}, graph::{ReactiveNode, SubscriberSet},
traits::{DefinedAt, IsDisposed, Trigger}, traits::{DefinedAt, IsDisposed, Notify},
}; };
use std::{ use std::{
fmt::{Debug, Formatter, Result}, fmt::{Debug, Formatter, Result},
@ -83,8 +83,8 @@ impl DefinedAt for ArcTrigger {
} }
} }
impl Trigger for ArcTrigger { impl Notify for ArcTrigger {
fn trigger(&self) { fn notify(&self) {
self.inner.mark_dirty(); self.inner.mark_dirty();
} }
} }

View file

@ -1,7 +1,7 @@
use super::guards::{UntrackedWriteGuard, WriteGuard}; use super::guards::{UntrackedWriteGuard, WriteGuard};
use crate::{ use crate::{
graph::{ReactiveNode, SubscriberSet}, graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Trigger}, prelude::{IsDisposed, Notify},
traits::{DefinedAt, UntrackableGuard, Writeable}, traits::{DefinedAt, UntrackableGuard, Writeable},
}; };
use core::fmt::{Debug, Formatter, Result}; use core::fmt::{Debug, Formatter, Result};
@ -116,8 +116,8 @@ impl<T> IsDisposed for ArcWriteSignal<T> {
} }
} }
impl<T> Trigger for ArcWriteSignal<T> { impl<T> Notify for ArcWriteSignal<T> {
fn trigger(&self) { fn notify(&self) {
self.inner.mark_dirty(); self.inner.mark_dirty();
} }
} }

View file

@ -2,7 +2,7 @@
use crate::{ use crate::{
computed::BlockingLock, computed::BlockingLock,
traits::{Trigger, UntrackableGuard}, traits::{Notify, UntrackableGuard},
}; };
use core::fmt::Debug; use core::fmt::Debug;
use guardian::{ArcRwLockReadGuardian, ArcRwLockWriteGuardian}; use guardian::{ArcRwLockReadGuardian, ArcRwLockWriteGuardian};
@ -259,7 +259,7 @@ where
#[derive(Debug)] #[derive(Debug)]
pub struct WriteGuard<S, G> pub struct WriteGuard<S, G>
where where
S: Trigger, S: Notify,
{ {
pub(crate) triggerable: Option<S>, pub(crate) triggerable: Option<S>,
pub(crate) guard: Option<G>, pub(crate) guard: Option<G>,
@ -267,7 +267,7 @@ where
impl<S, G> WriteGuard<S, G> impl<S, G> WriteGuard<S, G>
where where
S: Trigger, S: Notify,
{ {
/// Creates a new guard from the inner mutable guard type, and the signal that should be /// Creates a new guard from the inner mutable guard type, and the signal that should be
/// triggered on drop. /// triggered on drop.
@ -281,7 +281,7 @@ where
impl<S, G> UntrackableGuard for WriteGuard<S, G> impl<S, G> UntrackableGuard for WriteGuard<S, G>
where where
S: Trigger, S: Notify,
G: DerefMut, G: DerefMut,
{ {
/// Removes the triggerable type, so that it is no longer notifies when dropped. /// Removes the triggerable type, so that it is no longer notifies when dropped.
@ -292,7 +292,7 @@ where
impl<S, G> Deref for WriteGuard<S, G> impl<S, G> Deref for WriteGuard<S, G>
where where
S: Trigger, S: Notify,
G: Deref, G: Deref,
{ {
type Target = G::Target; type Target = G::Target;
@ -310,7 +310,7 @@ where
impl<S, G> DerefMut for WriteGuard<S, G> impl<S, G> DerefMut for WriteGuard<S, G>
where where
S: Trigger, S: Notify,
G: DerefMut, G: DerefMut,
{ {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
@ -354,7 +354,7 @@ impl<T> DerefMut for UntrackedWriteGuard<T> {
// Dropping the write guard will notify dependencies. // Dropping the write guard will notify dependencies.
impl<S, T> Drop for WriteGuard<S, T> impl<S, T> Drop for WriteGuard<S, T>
where where
S: Trigger, S: Notify,
{ {
fn drop(&mut self) { fn drop(&mut self) {
// first, drop the inner guard // first, drop the inner guard
@ -362,7 +362,7 @@ where
// then, notify about a change // then, notify about a change
if let Some(triggerable) = self.triggerable.as_ref() { if let Some(triggerable) = self.triggerable.as_ref() {
triggerable.trigger(); triggerable.notify();
} }
} }
} }

View file

@ -8,7 +8,7 @@ use crate::{
owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage}, owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage},
signal::guards::{UntrackedWriteGuard, WriteGuard}, signal::guards::{UntrackedWriteGuard, WriteGuard},
traits::{ traits::{
DefinedAt, Dispose, IsDisposed, ReadUntracked, Trigger, DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
UntrackableGuard, Writeable, UntrackableGuard, Writeable,
}, },
unwrap_signal, unwrap_signal,
@ -340,11 +340,11 @@ where
} }
} }
impl<T, S> Trigger for RwSignal<T, S> impl<T, S> Notify for RwSignal<T, S>
where where
S: Storage<ArcRwSignal<T>>, S: Storage<ArcRwSignal<T>>,
{ {
fn trigger(&self) { fn notify(&self) {
self.mark_dirty(); self.mark_dirty();
} }
} }

View file

@ -13,7 +13,7 @@ use crate::{
AnySource, AnySubscriber, ReactiveNode, Source, SubscriberSet, AnySource, AnySubscriber, ReactiveNode, Source, SubscriberSet,
ToAnySource, ToAnySource,
}, },
traits::DefinedAt, traits::{DefinedAt, IsDisposed},
unwrap_signal, unwrap_signal,
}; };
use or_poisoned::OrPoisoned; use or_poisoned::OrPoisoned;
@ -93,10 +93,11 @@ impl<T: AsSubscriberSet + DefinedAt> Source for T {
} }
} }
impl<T: AsSubscriberSet + DefinedAt> ToAnySource for T impl<T: AsSubscriberSet + DefinedAt + IsDisposed> ToAnySource for T
where where
T::Output: Borrow<Arc<RwLock<SubscriberSet>>>, T::Output: Borrow<Arc<RwLock<SubscriberSet>>>,
{ {
#[track_caller]
fn to_any_source(&self) -> AnySource { fn to_any_source(&self) -> AnySource {
self.as_subscriber_set() self.as_subscriber_set()
.map(|subs| { .map(|subs| {

View file

@ -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<ArcTrigger>,
}
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<RwLock<SubscriberSet>>;
#[inline(always)]
fn as_subscriber_set(&self) -> Option<Self::Output> {
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();
}
}
}

View file

@ -2,7 +2,7 @@ use super::{guards::WriteGuard, ArcWriteSignal};
use crate::{ use crate::{
owner::{Storage, StoredValue, SyncStorage}, owner::{Storage, StoredValue, SyncStorage},
traits::{ traits::{
DefinedAt, Dispose, IsDisposed, Trigger, UntrackableGuard, Writeable, DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Writeable,
}, },
}; };
use core::fmt::Debug; use core::fmt::Debug;
@ -116,14 +116,14 @@ impl<T, S> IsDisposed for WriteSignal<T, S> {
} }
} }
impl<T, S> Trigger for WriteSignal<T, S> impl<T, S> Notify for WriteSignal<T, S>
where where
T: 'static, T: 'static,
S: Storage<ArcWriteSignal<T>>, S: Storage<ArcWriteSignal<T>>,
{ {
fn trigger(&self) { fn notify(&self) {
if let Some(inner) = self.inner.try_get_value() { if let Some(inner) = self.inner.try_get_value() {
inner.trigger(); inner.notify();
} }
} }
} }

View file

@ -107,6 +107,10 @@ pub trait Track {
impl<T: Source + ToAnySource + DefinedAt> Track for T { impl<T: Source + ToAnySource + DefinedAt> Track for T {
#[track_caller] #[track_caller]
fn track(&self) { fn track(&self) {
if self.is_disposed() {
return;
}
if let Some(subscriber) = Observer::get() { if let Some(subscriber) = Observer::get() {
subscriber.add_source(self.to_any_source()); subscriber.add_source(self.to_any_source());
self.add_subscriber(subscriber); self.add_subscriber(subscriber);
@ -209,7 +213,7 @@ pub trait UntrackableGuard: DerefMut {
/// Gives mutable access to a signal's value through a guard type. When the guard is dropped, the /// Gives mutable access to a signal's value through a guard type. When the guard is dropped, the
/// signal's subscribers will be notified. /// 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. /// The type of the signal's value.
type Value: Sized + 'static; type Value: Sized + 'static;
@ -381,9 +385,9 @@ where
} }
/// Notifies subscribers of a change in this signal. /// Notifies subscribers of a change in this signal.
pub trait Trigger { pub trait Notify {
/// Notifies subscribers of a change in this signal. /// 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, /// Updates the value of a signal by applying a function that updates it in place,

View file

@ -1,6 +1,6 @@
[package] [package]
name = "reactive_stores" name = "reactive_stores"
version = "0.1.0-beta4" version = "0.1.0-beta5"
rust-version.workspace = true rust-version.workspace = true
edition.workspace = true edition.workspace = true

View file

@ -5,7 +5,7 @@ use crate::{
use reactive_graph::{ use reactive_graph::{
signal::ArcTrigger, signal::ArcTrigger,
traits::{ traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard, DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
}, },
}; };
use std::{ use std::{
@ -241,9 +241,9 @@ impl<T> DefinedAt for ArcField<T> {
} }
} }
impl<T> Trigger for ArcField<T> { impl<T> Notify for ArcField<T> {
fn trigger(&self) { fn notify(&self) {
self.trigger.trigger(); self.trigger.notify();
} }
} }

View file

@ -6,7 +6,7 @@ use crate::{
use reactive_graph::{ use reactive_graph::{
owner::{Storage, StoredValue, SyncStorage}, owner::{Storage, StoredValue, SyncStorage},
signal::ArcTrigger, signal::ArcTrigger,
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Trigger}, traits::{DefinedAt, IsDisposed, Notify, ReadUntracked, Track},
unwrap_signal, unwrap_signal,
}; };
use std::{fmt::Debug, hash::Hash, ops::IndexMut, panic::Location}; use std::{fmt::Debug, hash::Hash, ops::IndexMut, panic::Location};
@ -142,13 +142,13 @@ impl<T, S> DefinedAt for Field<T, S> {
} }
} }
impl<T, S> Trigger for Field<T, S> impl<T, S> Notify for Field<T, S>
where where
S: Storage<ArcField<T>>, S: Storage<ArcField<T>>,
{ {
fn trigger(&self) { fn notify(&self) {
if let Some(inner) = self.inner.try_get_value() { if let Some(inner) = self.inner.try_get_value() {
inner.trigger(); inner.notify();
} }
} }
} }

View file

@ -9,7 +9,7 @@ use reactive_graph::{
ArcTrigger, ArcTrigger,
}, },
traits::{ traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard, DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable, Writeable,
}, },
}; };
@ -141,15 +141,15 @@ where
} }
} }
impl<Inner, Prev> Trigger for AtIndex<Inner, Prev> impl<Inner, Prev> Notify for AtIndex<Inner, Prev>
where where
Inner: StoreField<Value = Prev>, Inner: StoreField<Value = Prev>,
Prev: IndexMut<usize> + 'static, Prev: IndexMut<usize> + 'static,
Prev::Output: Sized, Prev::Output: Sized,
{ {
fn trigger(&self) { fn notify(&self) {
let trigger = self.get_trigger(self.path().into_iter().collect()); let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.trigger(); trigger.notify();
} }
} }

View file

@ -5,7 +5,7 @@ use reactive_graph::{
guards::{Plain, ReadGuard}, guards::{Plain, ReadGuard},
ArcTrigger, ArcTrigger,
}, },
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Trigger}, traits::{DefinedAt, IsDisposed, Notify, ReadUntracked, Track},
}; };
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::{ use std::{
@ -218,10 +218,9 @@ impl<T: 'static> Track for ArcStore<T> {
} }
} }
impl<T: 'static> Trigger for ArcStore<T> { impl<T: 'static> Notify for ArcStore<T> {
fn trigger(&self) { fn notify(&self) {
self.get_trigger(self.path().into_iter().collect()) self.get_trigger(self.path().into_iter().collect()).notify();
.trigger();
} }
} }
@ -326,14 +325,14 @@ where
} }
} }
impl<T, S> Trigger for Store<T, S> impl<T, S> Notify for Store<T, S>
where where
T: 'static, T: 'static,
S: Storage<ArcStore<T>>, S: Storage<ArcStore<T>>,
{ {
fn trigger(&self) { fn notify(&self) {
if let Some(inner) = self.inner.try_get_value() { if let Some(inner) = self.inner.try_get_value() {
inner.trigger(); inner.notify();
} }
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{path::StorePath, StoreField}; use crate::{path::StorePath, StoreField};
use itertools::{EitherOrBoth, Itertools}; use itertools::{EitherOrBoth, Itertools};
use reactive_graph::traits::{Trigger, UntrackableGuard}; use reactive_graph::traits::{Notify, UntrackableGuard};
use std::{ use std::{
borrow::Cow, borrow::Cow,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
@ -33,7 +33,7 @@ where
writer.untrack(); writer.untrack();
let mut notify = |path: &StorePath| { let mut notify = |path: &StorePath| {
println!("notifying on {path:?}"); 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); writer.patch_field(new, &path, &mut notify);
} }

View file

@ -9,7 +9,7 @@ use reactive_graph::{
ArcTrigger, ArcTrigger,
}, },
traits::{ traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard, DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable, Writeable,
}, },
}; };
@ -135,14 +135,14 @@ where
} }
} }
impl<Inner, Prev, T> Trigger for Subfield<Inner, Prev, T> impl<Inner, Prev, T> Notify for Subfield<Inner, Prev, T>
where where
Inner: StoreField<Value = Prev>, Inner: StoreField<Value = Prev>,
Prev: 'static, Prev: 'static,
{ {
fn trigger(&self) { fn notify(&self) {
let trigger = self.get_trigger(self.path().into_iter().collect()); let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.trigger(); trigger.notify();
} }
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "reactive_stores_macro" name = "reactive_stores_macro"
version = "0.1.0-beta4" version = "0.1.0-beta5"
rust-version.workspace = true rust-version.workspace = true
edition.workspace = true edition.workspace = true

View file

@ -1,6 +1,6 @@
[package] [package]
name = "leptos_router" name = "leptos_router"
version = "0.7.0-beta4" version = "0.7.0-beta5"
authors = ["Greg Johnston", "Ben Wishovich"] authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT" license = "MIT"
readme = "../README.md" readme = "../README.md"

View file

@ -2,6 +2,33 @@ use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
use core::iter; use core::iter;
use std::borrow::Cow; 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)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ParamSegment(pub &'static str); 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)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct WildcardSegment(pub &'static str); pub struct WildcardSegment(pub &'static str);

View file

@ -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)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct StaticSegment<T: AsPath>(pub T); pub struct StaticSegment<T: AsPath>(pub T);

View file

@ -15,7 +15,7 @@ use reactive_graph::{
computed::{ArcMemo, ScopedFuture}, computed::{ArcMemo, ScopedFuture},
owner::{provide_context, use_context, Owner}, owner::{provide_context, use_context, Owner},
signal::{ArcRwSignal, ArcTrigger}, signal::{ArcRwSignal, ArcTrigger},
traits::{Get, GetUntracked, ReadUntracked, Set, Track, Trigger}, traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track},
wrappers::write::SignalSetter, wrappers::write::SignalSetter,
}; };
use send_wrapper::SendWrapper; use send_wrapper::SendWrapper;
@ -124,7 +124,7 @@ where
ScopedFuture::new(async move { ScopedFuture::new(async move {
let triggers = join_all(loaders).await; let triggers = join_all(loaders).await;
for trigger in triggers { for trigger in triggers {
trigger.trigger(); trigger.notify();
} }
matched_view.rebuild(&mut *view.borrow_mut()); matched_view.rebuild(&mut *view.borrow_mut());
}) })
@ -179,7 +179,7 @@ where
let triggers = join_all(loaders).await; let triggers = join_all(loaders).await;
// tell each one of the outlet triggers that it's ready // tell each one of the outlet triggers that it's ready
for trigger in triggers { for trigger in triggers {
trigger.trigger(); trigger.notify();
} }
if let Some(loc) = location { if let Some(loc) = location {
loc.ready_to_complete(); loc.ready_to_complete();

View file

@ -1,6 +1,6 @@
[package] [package]
name = "leptos_router_macro" name = "leptos_router_macro"
version = "0.7.0-beta4" version = "0.7.0-beta5"
authors = ["Greg Johnston", "Ben Wishovich"] authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT" license = "MIT"
readme = "../README.md" readme = "../README.md"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tachys" name = "tachys"
version = "0.1.0-beta4" version = "0.1.0-beta5"
authors = ["Greg Johnston"] authors = ["Greg Johnston"]
license = "MIT" license = "MIT"
readme = "../README.md" readme = "../README.md"

View file

@ -719,7 +719,7 @@ where
} }
fn to_html(self, style: &mut String) { 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); inner.to_html(style);
} else { } else {
panic!("You cannot use Suspend on an attribute outside Suspense"); panic!("You cannot use Suspend on an attribute outside Suspense");
@ -736,7 +736,8 @@ where
let state = Rc::clone(&state); let state = Rc::clone(&state);
async move { async move {
*state.borrow_mut() = *state.borrow_mut() =
Some(self.await.hydrate::<FROM_SERVER>(&el)); Some(self.inner.await.hydrate::<FROM_SERVER>(&el));
self.subscriber.forward();
} }
}); });
state state
@ -748,7 +749,8 @@ where
Executor::spawn_local({ Executor::spawn_local({
let state = Rc::clone(&state); let state = Rc::clone(&state);
async move { async move {
*state.borrow_mut() = Some(self.await.build(&el)); *state.borrow_mut() = Some(self.inner.await.build(&el));
self.subscriber.forward();
} }
}); });
state state
@ -758,11 +760,12 @@ where
Executor::spawn_local({ Executor::spawn_local({
let state = Rc::clone(state); let state = Rc::clone(state);
async move { async move {
let value = self.await; let value = self.inner.await;
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();
if let Some(state) = state.as_mut() { if let Some(state) = state.as_mut() {
value.rebuild(state); value.rebuild(state);
} }
self.subscriber.forward();
} }
}); });
} }
@ -778,6 +781,6 @@ where
fn dry_resolve(&mut self) {} fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput { async fn resolve(self) -> Self::AsyncOutput {
self.await self.inner.await
} }
} }

View file

@ -401,7 +401,8 @@ where
let state = Rc::clone(&state); let state = Rc::clone(&state);
async move { async move {
*state.borrow_mut() = *state.borrow_mut() =
Some(self.await.hydrate::<FROM_SERVER>(&key, &el)); Some(self.inner.await.hydrate::<FROM_SERVER>(&key, &el));
self.subscriber.forward();
} }
}); });
state state
@ -414,7 +415,8 @@ where
Executor::spawn_local({ Executor::spawn_local({
let state = Rc::clone(&state); let state = Rc::clone(&state);
async move { 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 state
@ -425,11 +427,12 @@ where
Executor::spawn_local({ Executor::spawn_local({
let state = Rc::clone(state); let state = Rc::clone(state);
async move { async move {
let value = self.await; let value = self.inner.await;
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();
if let Some(state) = state.as_mut() { if let Some(state) = state.as_mut() {
value.rebuild(&key, state); value.rebuild(&key, state);
} }
self.subscriber.forward();
} }
}); });
} }
@ -447,7 +450,7 @@ where
fn dry_resolve(&mut self) {} fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput { async fn resolve(self) -> Self::AsyncOutput {
self.await self.inner.await
} }
} }

View file

@ -447,7 +447,7 @@ where
type CloneableOwned = Self; type CloneableOwned = Self;
fn to_html(self, style: &mut String) { 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); inner.to_html(style);
} else { } else {
panic!("You cannot use Suspend on an attribute outside Suspense"); panic!("You cannot use Suspend on an attribute outside Suspense");
@ -464,7 +464,8 @@ where
let state = Rc::clone(&state); let state = Rc::clone(&state);
async move { async move {
*state.borrow_mut() = *state.borrow_mut() =
Some(self.await.hydrate::<FROM_SERVER>(&el)); Some(self.inner.await.hydrate::<FROM_SERVER>(&el));
self.subscriber.forward();
} }
}); });
state state
@ -476,7 +477,8 @@ where
Executor::spawn_local({ Executor::spawn_local({
let state = Rc::clone(&state); let state = Rc::clone(&state);
async move { async move {
*state.borrow_mut() = Some(self.await.build(&el)); *state.borrow_mut() = Some(self.inner.await.build(&el));
self.subscriber.forward();
} }
}); });
state state
@ -486,11 +488,12 @@ where
Executor::spawn_local({ Executor::spawn_local({
let state = Rc::clone(state); let state = Rc::clone(state);
async move { async move {
let value = self.await; let value = self.inner.await;
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();
if let Some(state) = state.as_mut() { if let Some(state) = state.as_mut() {
value.rebuild(state); value.rebuild(state);
} }
self.subscriber.forward();
} }
}); });
} }
@ -506,6 +509,6 @@ where
fn dry_resolve(&mut self) {} fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput { async fn resolve(self) -> Self::AsyncOutput {
self.await self.inner.await
} }
} }

View file

@ -10,56 +10,115 @@ use crate::{
}; };
use any_spawner::Executor; use any_spawner::Executor;
use futures::{select, FutureExt}; use futures::{select, FutureExt};
use or_poisoned::OrPoisoned;
use reactive_graph::{ use reactive_graph::{
computed::{ computed::{
suspense::{LocalResourceNotifier, SuspenseContext}, suspense::{LocalResourceNotifier, SuspenseContext},
ScopedFuture, ScopedFuture,
}, },
graph::{
AnySource, AnySubscriber, Observer, ReactiveNode, Source, Subscriber,
ToAnySubscriber, WithObserver,
},
owner::{provide_context, use_context}, owner::{provide_context, use_context},
}; };
use std::{ use std::{
cell::RefCell, cell::RefCell,
fmt::Debug, fmt::Debug,
future::Future, future::Future,
mem,
pin::Pin, pin::Pin,
rc::Rc, rc::Rc,
task::{Context, Poll}, sync::{Arc, Mutex, Weak},
}; };
/// A suspended `Future`, which can be used in the view. /// A suspended `Future`, which can be used in the view.
#[derive(Clone)] #[derive(Clone)]
pub struct Suspend<Fut> { pub struct Suspend<Fut> {
inner: Pin<Box<ScopedFuture<Fut>>>, pub(crate) subscriber: SuspendSubscriber,
pub(crate) inner: Pin<Box<ScopedFuture<Fut>>>,
}
#[derive(Debug, Clone)]
pub(crate) struct SuspendSubscriber {
inner: Arc<SuspendSubscriberInner>,
}
#[derive(Debug)]
struct SuspendSubscriberInner {
outer_subscriber: Option<AnySubscriber>,
sources: Mutex<Vec<AnySource>>,
}
impl SuspendSubscriber {
pub fn new() -> Self {
let outer_subscriber = Observer::get();
Self {
inner: Arc::new(SuspendSubscriberInner {
outer_subscriber,
sources: Default::default(),
}),
}
}
/// 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) {}
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<dyn Subscriber + Send + Sync>,
)
}
} }
impl<Fut> Suspend<Fut> { impl<Fut> Suspend<Fut> {
/// Creates a new suspended view. /// Creates a new suspended view.
pub fn new(fut: Fut) -> Self { pub fn new(fut: Fut) -> Self {
Self { let subscriber = SuspendSubscriber::new();
inner: Box::pin(ScopedFuture::new(fut)), let any_subscriber = subscriber.to_any_subscriber();
} let inner =
} any_subscriber.with_observer(|| Box::pin(ScopedFuture::new(fut)));
} Self { subscriber, inner }
impl<Fut> Future for Suspend<Fut>
where
Fut: Future,
{
type Output = Fut::Output;
fn poll(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Self::Output> {
self.inner.as_mut().poll(cx)
}
}
impl<Fut> From<ScopedFuture<Fut>> for Suspend<Fut> {
fn from(inner: ScopedFuture<Fut>) -> Self {
Self {
inner: Box::pin(inner),
}
} }
} }
@ -106,10 +165,12 @@ where
// TODO cancelation if it fires multiple times // TODO cancelation if it fires multiple times
fn build(self) -> Self::State { fn build(self) -> Self::State {
let Self { subscriber, inner } = self;
// poll the future once immediately // poll the future once immediately
// if it's already available, start in the ready state // if it's already available, start in the ready state
// otherwise, start with the fallback // 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 initial = fut.as_mut().now_or_never();
let initially_pending = initial.is_none(); let initially_pending = initial.is_none();
let inner = Rc::new(RefCell::new(initial.build())); let inner = Rc::new(RefCell::new(initial.build()));
@ -127,6 +188,8 @@ where
let value = fut.as_mut().await; let value = fut.as_mut().await;
drop(id); drop(id);
Some(value).rebuild(&mut *state.borrow_mut()); Some(value).rebuild(&mut *state.borrow_mut());
subscriber.forward();
} }
}); });
} }
@ -135,8 +198,10 @@ where
} }
fn rebuild(self, state: &mut Self::State) { fn rebuild(self, state: &mut Self::State) {
let Self { subscriber, inner } = self;
// get a unique ID if there's a SuspenseContext // get a unique ID if there's a SuspenseContext
let fut = self; let fut = inner;
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id()); let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
// spawn the future, and rebuild the state when it resolves // spawn the future, and rebuild the state when it resolves
@ -150,6 +215,8 @@ where
// has no parent // has no parent
any_spawner::Executor::tick().await; any_spawner::Executor::tick().await;
Some(value).rebuild(&mut *state.borrow_mut()); Some(value).rebuild(&mut *state.borrow_mut());
subscriber.forward();
} }
}); });
} }
@ -208,7 +275,7 @@ where
// TODO wrap this with a Suspense as needed // TODO wrap this with a Suspense as needed
// currently this is just used for Routes, which creates a Suspend but never actually needs // 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) // 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); inner.to_html_with_buf(buf, position, escape, mark_branches);
} }
} }
@ -222,7 +289,7 @@ where
) where ) where
Self: Sized, Self: Sized,
{ {
let mut fut = Box::pin(self); let mut fut = Box::pin(self.inner);
match fut.as_mut().now_or_never() { match fut.as_mut().now_or_never() {
Some(inner) => inner.to_html_async_with_buf::<OUT_OF_ORDER>( Some(inner) => inner.to_html_async_with_buf::<OUT_OF_ORDER>(
buf, buf,
@ -287,10 +354,12 @@ where
cursor: &Cursor<Rndr>, cursor: &Cursor<Rndr>,
position: &PositionState, position: &PositionState,
) -> Self::State { ) -> Self::State {
let Self { subscriber, inner } = self;
// poll the future once immediately // poll the future once immediately
// if it's already available, start in the ready state // if it's already available, start in the ready state
// otherwise, start with the fallback // 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 initial = fut.as_mut().now_or_never();
let initially_pending = initial.is_none(); let initially_pending = initial.is_none();
let inner = Rc::new(RefCell::new( let inner = Rc::new(RefCell::new(
@ -310,15 +379,19 @@ where
let value = fut.as_mut().await; let value = fut.as_mut().await;
drop(id); drop(id);
Some(value).rebuild(&mut *state.borrow_mut()); Some(value).rebuild(&mut *state.borrow_mut());
subscriber.forward();
} }
}); });
} else {
subscriber.forward();
} }
SuspendState { inner } SuspendState { inner }
} }
async fn resolve(self) -> Self::AsyncOutput { async fn resolve(self) -> Self::AsyncOutput {
Some(self.await) Some(self.inner.await)
} }
fn dry_resolve(&mut self) { fn dry_resolve(&mut self) {