diff --git a/leptos_server/src/resource.rs b/leptos_server/src/resource.rs index f047d5b95..b11b85216 100644 --- a/leptos_server/src/resource.rs +++ b/leptos_server/src/resource.rs @@ -83,7 +83,8 @@ where move || fetcher(source.get()) }; - let data = ArcAsyncDerived::new_with_initial(initial, fun); + let data = + ArcAsyncDerived::new_with_initial_without_spawning(initial, fun); if is_ready { source.with(|_| ()); source.add_subscriber(data.to_any_subscriber()); diff --git a/reactive_graph/src/computed/async_derived/arc_async_derived.rs b/reactive_graph/src/computed/async_derived/arc_async_derived.rs index bb628ac56..978687354 100644 --- a/reactive_graph/src/computed/async_derived/arc_async_derived.rs +++ b/reactive_graph/src/computed/async_derived/arc_async_derived.rs @@ -195,10 +195,10 @@ impl DefinedAt for ArcAsyncDerived { // whether `fun` returns a `Future` that is `Send`. Doing it as a function would, // as far as I can tell, require repeating most of the function body. macro_rules! spawn_derived { - ($spawner:expr, $initial:ident, $fun:ident, $should_spawn:literal) => {{ + ($spawner:expr, $initial:ident, $fun:ident, $should_spawn:literal, $force_spawn:literal) => {{ let (notifier, mut rx) = channel(); - let is_ready = $initial.is_some(); + let is_ready = $initial.is_some() && !$force_spawn; let owner = Owner::new(); let inner = Arc::new(RwLock::new(ArcAsyncDerivedInner { @@ -344,9 +344,8 @@ impl ArcAsyncDerived { Self::new_with_initial(None, fun) } - /// Creates a new async derived computation with an initial value. - /// - /// If the initial value is `Some(_)`, the task will not be run initially. + /// Creates a new async derived computation with an initial value as a fallback, and begins running the + /// `Future` eagerly to get the actual first value. #[track_caller] pub fn new_with_initial( initial_value: Option, @@ -357,7 +356,27 @@ impl ArcAsyncDerived { Fut: Future + Send + 'static, { let (this, _) = - spawn_derived!(Executor::spawn, initial_value, fun, true); + spawn_derived!(Executor::spawn, initial_value, fun, true, true); + this + } + + /// Creates a new async derived computation with an initial value, and does not spawn a task + /// initially. + /// + /// This is mostly used with manual dependency tracking, for primitives built on top of this + /// where you do not want to run the run the `Future` unnecessarily. + #[doc(hidden)] + #[track_caller] + pub fn new_with_initial_without_spawning( + initial_value: Option, + fun: impl Fn() -> Fut + Send + Sync + 'static, + ) -> Self + where + T: Send + Sync + 'static, + Fut: Future + Send + 'static, + { + let (this, _) = + spawn_derived!(Executor::spawn, initial_value, fun, true, false); this } @@ -375,10 +394,8 @@ impl ArcAsyncDerived { Self::new_unsync_with_initial(None, fun) } - /// Creates a new async derived computation with an initial value. Async work will be - /// guaranteed to run only on the current thread. - /// - /// If the initial value is `Some(_)`, the task will not be run initially. + /// Creates a new async derived computation with an initial value as a fallback, and begins running the + /// `Future` eagerly to get the actual first value. #[track_caller] pub fn new_unsync_with_initial( initial_value: Option, @@ -388,8 +405,13 @@ impl ArcAsyncDerived { T: 'static, Fut: Future + 'static, { - let (this, _) = - spawn_derived!(Executor::spawn_local, initial_value, fun, true); + let (this, _) = spawn_derived!( + Executor::spawn_local, + initial_value, + fun, + true, + true + ); this } @@ -402,7 +424,7 @@ impl ArcAsyncDerived { { let initial = None::; let (this, _) = - spawn_derived!(Executor::spawn_local, initial, fun, false); + spawn_derived!(Executor::spawn_local, initial, fun, false, false); this } diff --git a/reactive_graph/tests/async_derived.rs b/reactive_graph/tests/async_derived.rs index a5d5f4314..cad8a7e8d 100644 --- a/reactive_graph/tests/async_derived.rs +++ b/reactive_graph/tests/async_derived.rs @@ -90,3 +90,35 @@ async fn read_signal_traits_on_arena() { assert_eq!(value.with(|n| *n), None); assert_eq!(value.get(), None); } + +#[tokio::test] +async fn async_derived_with_initial() { + _ = Executor::init_tokio(); + + let signal1 = RwSignal::new(0); + let signal2 = RwSignal::new(0); + let derived = + ArcAsyncDerived::new_with_initial(Some(5), move || async move { + // reactive values can be tracked anywhere in the `async` block + let value1 = signal1.get(); + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + let value2 = signal2.get(); + + value1 + value2 + }); + + // the value can be accessed synchronously as `Option` + assert_eq!(derived.get(), Some(5)); + // we can also .await the value, i.e., convert it into a Future + assert_eq!(derived.clone().await, 0); + assert_eq!(derived.get(), Some(0)); + + signal1.set(1); + // while the new value is still pending, the signal holds the old value + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + assert_eq!(derived.get(), Some(0)); + + // setting multiple dependencies will hold until the latest change is ready + signal2.set(1); + assert_eq!(derived.await, 2); +}