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/**
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 }}'

View file

@ -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

View file

@ -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"

View file

@ -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! {
<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>
<FlatRoutes fallback>
<Route path=path!("") view=HomePage/>
<Route path=path!("naive") view=Naive ssr/>
<Route path=path!("naive-alt") view=|| view! { <NaiveEvent/> } ssr/>
<Route path=path!("naive-hook") view=|| view! { <NaiveEvent hook=true/> } ssr/>
<Route path=path!("naive") view=Naive ssr=SsrMode::Async/>
<Route path=path!("naive-alt") view=|| view! { <NaiveEvent/> } ssr=SsrMode::Async/>
<Route path=path!("naive-hook") view=|| view! { <NaiveEvent hook=true/> } ssr=SsrMode::Async/>
<Route path=path!("naive-fallback") view=|| view! {
<NaiveEvent hook=true fallback=true/>
} ssr/>
<Route path=path!("signal-effect-script") view=CodeDemoSignalEffect ssr/>
<Route path=path!("custom-event") view=CustomEvent ssr/>
<Route path=path!("wasm-bindgen-naive") view=WasmBindgenNaive ssr/>
<Route path=path!("wasm-bindgen-event") view=WasmBindgenJSHookReadyEvent ssr/>
<Route path=path!("wasm-bindgen-effect") view=WasmBindgenEffect ssr/>
<Route path=path!("wasm-bindgen-direct") view=WasmBindgenDirect ssr/>
<Route path=path!("wasm-bindgen-direct-fixed") view=WasmBindgenDirectFixed ssr/>
} ssr=SsrMode::Async/>
<Route path=path!("signal-effect-script") view=CodeDemoSignalEffect ssr=SsrMode::Async/>
<Route path=path!("custom-event") view=CustomEvent ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-naive") view=WasmBindgenNaive ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-event") view=WasmBindgenJSHookReadyEvent ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-effect") view=WasmBindgenEffect ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-direct") view=WasmBindgenDirect ssr=SsrMode::Async/>
<Route path=path!("wasm-bindgen-direct-fixed") view=WasmBindgenDirectFixed ssr=SsrMode::Async/>
</FlatRoutes>
</article>
</main>

View file

@ -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"

View file

@ -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))
}

View file

@ -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"

View file

@ -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!()

View file

@ -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<T: 'static> IsDisposed for ArcLocalResource<T> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T: 'static> ToAnySource for ArcLocalResource<T> {
fn to_any_source(&self) -> AnySource {
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>
where
T: Send + Sync + 'static,

View file

@ -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)
}
};

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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<T>(
source: impl Fn() -> T + Clone + 'static,
source: impl Fn() -> T + Clone + Send + Sync + 'static,
) -> Selector<T>
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<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,
) -> Selector<T>
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)
}

View file

@ -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<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>
where
S: Storage<T>,

View file

@ -18,7 +18,8 @@ use crate::{
ArcTrigger,
},
traits::{
DefinedAt, ReadUntracked, Track, Trigger, UntrackableGuard, Writeable,
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
},
transition::AsyncTransition,
};
@ -580,8 +581,8 @@ impl<T: 'static> ReadUntracked for ArcAsyncDerived<T> {
}
}
impl<T: 'static> Trigger for ArcAsyncDerived<T> {
fn trigger(&self) {
impl<T: 'static> Notify for ArcAsyncDerived<T> {
fn notify(&self) {
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> {
fn to_any_source(&self) -> AnySource {
AnySource(

View file

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

View file

@ -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<T> Selector<T>
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<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
@ -92,7 +92,7 @@ where
let v: Arc<RwLock<Option<T>>> = Default::default();
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 f = Arc::clone(&f);
let v = Arc::clone(&v);

View file

@ -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<LocalStorage> {
/// # 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<LocalStorage> {
/// },
/// 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<LocalStorage> {
/// # 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<LocalStorage> {
/// 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<LocalStorage> {
/// # 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<LocalStorage> {
/// 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<D, T>(

View file

@ -135,11 +135,12 @@ 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>) -> T + Send + 'static,
fun: impl FnMut(Option<T>) -> T + Send + Sync + 'static,
) -> Self {
let (mut observer, mut rx) = channel();
observer.notify();
fn erased<T: Send + Sync + 'static>(
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 {
@ -147,7 +148,10 @@ where
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);
@ -155,11 +159,9 @@ where
async move {
while rx.next().await.is_some() {
if first_run
|| subscriber
if subscriber
.with_observer(|| subscriber.update_if_necessary())
{
first_run = false;
subscriber.clear_sources(&subscriber);
let old_value =
@ -172,8 +174,12 @@ where
}
}
});
RenderEffect { value, inner }
}
erased(Box::new(fun))
}
}
impl<T> ToAnySubscriber for RenderEffect<T> {

View file

@ -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()

View file

@ -582,7 +582,7 @@ impl<T, S> Dispose for StoredValue<T, S> {
#[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."
)]

View file

@ -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.

View file

@ -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<T: 'static> ReadUntracked for ArcRwSignal<T> {
}
}
impl<T> Trigger for ArcRwSignal<T> {
fn trigger(&self) {
impl<T> Notify for ArcRwSignal<T> {
fn notify(&self) {
self.mark_dirty();
}
}

View file

@ -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();
}
}

View file

@ -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<T> IsDisposed for ArcWriteSignal<T> {
}
}
impl<T> Trigger for ArcWriteSignal<T> {
fn trigger(&self) {
impl<T> Notify for ArcWriteSignal<T> {
fn notify(&self) {
self.inner.mark_dirty();
}
}

View file

@ -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<S, G>
where
S: Trigger,
S: Notify,
{
pub(crate) triggerable: Option<S>,
pub(crate) guard: Option<G>,
@ -267,7 +267,7 @@ where
impl<S, G> WriteGuard<S, G>
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<S, G> UntrackableGuard for WriteGuard<S, G>
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<S, G> Deref for WriteGuard<S, G>
where
S: Trigger,
S: Notify,
G: Deref,
{
type Target = G::Target;
@ -310,7 +310,7 @@ where
impl<S, G> DerefMut for WriteGuard<S, G>
where
S: Trigger,
S: Notify,
G: DerefMut,
{
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.
impl<S, T> Drop for WriteGuard<S, T>
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();
}
}
}

View file

@ -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<T, S> Trigger for RwSignal<T, S>
impl<T, S> Notify for RwSignal<T, S>
where
S: Storage<ArcRwSignal<T>>,
{
fn trigger(&self) {
fn notify(&self) {
self.mark_dirty();
}
}

View file

@ -13,7 +13,7 @@ use crate::{
AnySource, AnySubscriber, ReactiveNode, Source, SubscriberSet,
ToAnySource,
},
traits::DefinedAt,
traits::{DefinedAt, IsDisposed},
unwrap_signal,
};
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
T::Output: Borrow<Arc<RwLock<SubscriberSet>>>,
{
#[track_caller]
fn to_any_source(&self) -> AnySource {
self.as_subscriber_set()
.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::{
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<T, S> IsDisposed for WriteSignal<T, S> {
}
}
impl<T, S> Trigger for WriteSignal<T, S>
impl<T, S> Notify for WriteSignal<T, S>
where
T: 'static,
S: Storage<ArcWriteSignal<T>>,
{
fn trigger(&self) {
fn notify(&self) {
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 {
#[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);
@ -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
/// 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 +385,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,

View file

@ -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

View file

@ -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::{
@ -241,9 +241,9 @@ impl<T> DefinedAt for ArcField<T> {
}
}
impl<T> Trigger for ArcField<T> {
fn trigger(&self) {
self.trigger.trigger();
impl<T> Notify for ArcField<T> {
fn notify(&self) {
self.trigger.notify();
}
}

View file

@ -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::{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
S: Storage<ArcField<T>>,
{
fn trigger(&self) {
fn notify(&self) {
if let Some(inner) = self.inner.try_get_value() {
inner.trigger();
inner.notify();
}
}
}

View file

@ -9,7 +9,7 @@ use reactive_graph::{
ArcTrigger,
},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard,
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
},
};
@ -141,15 +141,15 @@ where
}
}
impl<Inner, Prev> Trigger for AtIndex<Inner, Prev>
impl<Inner, Prev> Notify for AtIndex<Inner, Prev>
where
Inner: StoreField<Value = Prev>,
Prev: IndexMut<usize> + 'static,
Prev::Output: Sized,
{
fn trigger(&self) {
fn notify(&self) {
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},
ArcTrigger,
},
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Trigger},
traits::{DefinedAt, IsDisposed, Notify, ReadUntracked, Track},
};
use rustc_hash::FxHashMap;
use std::{
@ -218,10 +218,9 @@ impl<T: 'static> Track for ArcStore<T> {
}
}
impl<T: 'static> Trigger for ArcStore<T> {
fn trigger(&self) {
self.get_trigger(self.path().into_iter().collect())
.trigger();
impl<T: 'static> Notify for ArcStore<T> {
fn notify(&self) {
self.get_trigger(self.path().into_iter().collect()).notify();
}
}
@ -326,14 +325,14 @@ where
}
}
impl<T, S> Trigger for Store<T, S>
impl<T, S> Notify for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
{
fn trigger(&self) {
fn notify(&self) {
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 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);
}

View file

@ -9,7 +9,7 @@ use reactive_graph::{
ArcTrigger,
},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard,
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
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
Inner: StoreField<Value = Prev>,
Prev: 'static,
{
fn trigger(&self) {
fn notify(&self) {
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.trigger();
trigger.notify();
}
}

View file

@ -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

View file

@ -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"

View file

@ -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);

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)]
pub struct StaticSegment<T: AsPath>(pub T);

View file

@ -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();

View file

@ -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"

View file

@ -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"

View file

@ -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::<FROM_SERVER>(&el));
Some(self.inner.await.hydrate::<FROM_SERVER>(&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
}
}

View file

@ -401,7 +401,8 @@ where
let state = Rc::clone(&state);
async move {
*state.borrow_mut() =
Some(self.await.hydrate::<FROM_SERVER>(&key, &el));
Some(self.inner.await.hydrate::<FROM_SERVER>(&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
}
}

View file

@ -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::<FROM_SERVER>(&el));
Some(self.inner.await.hydrate::<FROM_SERVER>(&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
}
}

View file

@ -10,56 +10,115 @@ use crate::{
};
use any_spawner::Executor;
use futures::{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<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> {
/// Creates a new suspended view.
pub fn new(fut: Fut) -> Self {
Self {
inner: Box::pin(ScopedFuture::new(fut)),
}
}
}
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),
}
let subscriber = SuspendSubscriber::new();
let any_subscriber = subscriber.to_any_subscriber();
let inner =
any_subscriber.with_observer(|| Box::pin(ScopedFuture::new(fut)));
Self { subscriber, inner }
}
}
@ -106,10 +165,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 +188,8 @@ where
let value = fut.as_mut().await;
drop(id);
Some(value).rebuild(&mut *state.borrow_mut());
subscriber.forward();
}
});
}
@ -135,8 +198,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::<SuspenseContext>().map(|sc| sc.task_id());
// spawn the future, and rebuild the state when it resolves
@ -150,6 +215,8 @@ where
// has no parent
any_spawner::Executor::tick().await;
Some(value).rebuild(&mut *state.borrow_mut());
subscriber.forward();
}
});
}
@ -208,7 +275,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 +289,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::<OUT_OF_ORDER>(
buf,
@ -287,10 +354,12 @@ where
cursor: &Cursor<Rndr>,
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,15 +379,19 @@ where
let value = fut.as_mut().await;
drop(id);
Some(value).rebuild(&mut *state.borrow_mut());
subscriber.forward();
}
});
} else {
subscriber.forward();
}
SuspendState { inner }
}
async fn resolve(self) -> Self::AsyncOutput {
Some(self.await)
Some(self.inner.await)
}
fn dry_resolve(&mut self) {