From d67f0e34a46a31041491a8354c5c18d754214661 Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Wed, 20 Sep 2023 11:47:11 -0600 Subject: [PATCH 1/8] initial impl --- leptos_reactive/Cargo.toml | 1 + leptos_reactive/src/runtime.rs | 78 ++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/leptos_reactive/Cargo.toml b/leptos_reactive/Cargo.toml index 1c4e2f9ad3..72c106c17b 100644 --- a/leptos_reactive/Cargo.toml +++ b/leptos_reactive/Cargo.toml @@ -42,6 +42,7 @@ web-sys = { version = "0.3", optional = true, features = [ cfg-if = "1" indexmap = "2" self_cell = "1.0.0" +pin-project = "1.1.3" [dev-dependencies] criterion = { version = "0.5.1", features = ["html_reports"] } diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 630496201a..08fb8baa0a 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -13,8 +13,10 @@ use cfg_if::cfg_if; use core::hash::BuildHasherDefault; use futures::stream::FuturesUnordered; use indexmap::IndexSet; +use pin_project::pin_project; use rustc_hash::{FxHashMap, FxHasher}; use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap}; +use std::task::Poll; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, @@ -851,25 +853,40 @@ where /// /// ## Panics /// Panics if there is no current reactive runtime. -pub fn with_owner(owner: Owner, f: impl FnOnce() -> T + 'static) -> T -where - T: 'static, -{ +pub fn with_owner(owner: Owner, f: impl FnOnce() -> T) -> T { + try_with_owner(owner, f) + .expect("runtime/scope should be alive when with_owner runs") +} + +/// Runs the given code with the given reactive owner. +pub fn try_with_owner(owner: Owner, f: impl FnOnce() -> T) -> Option { with_runtime(|runtime| { - let prev_observer = runtime.observer.take(); - let prev_owner = runtime.owner.take(); + runtime + .nodes + .try_borrow() + .map(|nodes| { + if nodes.contains_key(owner.0) { + let prev_observer = runtime.observer.take(); + let prev_owner = runtime.owner.take(); - runtime.owner.set(Some(owner.0)); - runtime.observer.set(Some(owner.0)); + runtime.owner.set(Some(owner.0)); + runtime.observer.set(Some(owner.0)); - let v = f(); + let v = f(); - runtime.observer.set(prev_observer); - runtime.owner.set(prev_owner); + runtime.observer.set(prev_observer); + runtime.owner.set(prev_owner); - v + Some(v) + } else { + None + } + }) + .ok() + .flatten() }) - .expect("runtime should be alive when with_owner runs") + .ok() + .flatten() } impl RuntimeId { @@ -1469,3 +1486,38 @@ pub fn untrack(f: impl FnOnce() -> T) -> T { pub fn untrack_with_diagnostics(f: impl FnOnce() -> T) -> T { Runtime::current().untrack(f, true) } + +/// Allows running a future that has access to a given scope. +#[pin_project] +pub struct ScopedFuture { + owner: Owner, + #[pin] + future: Fut, +} + +impl Future for ScopedFuture { + type Output = Option; + + fn poll( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll { + // TODO: we need to think about how to make this + // not panic for scopes that have been cleaned up... + // or perhaps we can force the scope to not be cleaned + // up until all futures that have a handle to them are + // dropped... + + let this = self.project(); + + if let Some(poll) = try_with_owner(*this.owner, || this.future.poll(cx)) + { + match poll { + Poll::Ready(res) => Poll::Ready(Some(res)), + Poll::Pending => Poll::Pending, + } + } else { + Poll::Ready(None) + } + } +} From e1d0a9e718a61718ad33e348e6d2ab563b914f7d Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Wed, 20 Sep 2023 11:55:11 -0600 Subject: [PATCH 2/8] removed the accidental 'static bound on `ScopedFuture` --- leptos_reactive/src/runtime.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 08fb8baa0a..c01b8655b1 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -1489,7 +1489,7 @@ pub fn untrack_with_diagnostics(f: impl FnOnce() -> T) -> T { /// Allows running a future that has access to a given scope. #[pin_project] -pub struct ScopedFuture { +pub struct ScopedFuture { owner: Owner, #[pin] future: Fut, @@ -1521,3 +1521,5 @@ impl Future for ScopedFuture { } } } + +impl ScopedFuture {} From ddd740ddb3a8e6d201825d6a31a3355963c54e4a Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Wed, 20 Sep 2023 11:58:00 -0600 Subject: [PATCH 3/8] added `ScopedFuture::new()` and exposed it along with `try_with_owner() --- leptos_reactive/src/lib.rs | 3 ++- leptos_reactive/src/runtime.rs | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs index a7dc6bbf3b..ad0edd44c0 100644 --- a/leptos_reactive/src/lib.rs +++ b/leptos_reactive/src/lib.rs @@ -114,8 +114,9 @@ pub use resource::*; use runtime::*; pub use runtime::{ as_child_of_current_owner, batch, create_runtime, current_runtime, - on_cleanup, run_as_child, set_current_runtime, untrack, + on_cleanup, run_as_child, set_current_runtime, try_with_owner, untrack, untrack_with_diagnostics, with_current_owner, with_owner, Owner, RuntimeId, + ScopedFuture, }; pub use selector::*; pub use serialization::*; diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index c01b8655b1..2a1ed10699 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -1522,4 +1522,10 @@ impl Future for ScopedFuture { } } -impl ScopedFuture {} +impl ScopedFuture { + /// Creates a new future that will have access to the `[Owner]`'s + /// scope. + pub fn new(owner: Owner, fut: Fut) -> Self { + Self { owner, future: fut } + } +} From 25780331661a32d8674343902bdb004851194b00 Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Wed, 20 Sep 2023 12:27:21 -0600 Subject: [PATCH 4/8] added `spawn_local_*` methods --- leptos_reactive/src/lib.rs | 8 ++- leptos_reactive/src/runtime.rs | 100 ++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs index ad0edd44c0..83e827838f 100644 --- a/leptos_reactive/src/lib.rs +++ b/leptos_reactive/src/lib.rs @@ -114,9 +114,11 @@ pub use resource::*; use runtime::*; pub use runtime::{ as_child_of_current_owner, batch, create_runtime, current_runtime, - on_cleanup, run_as_child, set_current_runtime, try_with_owner, untrack, - untrack_with_diagnostics, with_current_owner, with_owner, Owner, RuntimeId, - ScopedFuture, + on_cleanup, run_as_child, set_current_runtime, + spawn_local_with_current_owner, spawn_local_with_owner, + try_spawn_local_with_current_owner, try_spawn_local_with_owner, + try_with_owner, untrack, untrack_with_diagnostics, with_current_owner, + with_owner, Owner, RuntimeId, ScopedFuture, }; pub use selector::*; pub use serialization::*; diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 2a1ed10699..002a4e7ad8 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -1524,8 +1524,106 @@ impl Future for ScopedFuture { impl ScopedFuture { /// Creates a new future that will have access to the `[Owner]`'s - /// scope. + /// scope context. pub fn new(owner: Owner, fut: Fut) -> Self { Self { owner, future: fut } } + + /// Runs the future in the current [`Owner`]'s scope context. + /// + /// # Panics + /// Panics if there is no current [`Owner`] context available. + #[track_caller] + pub fn new_current(fut: Fut) -> Self { + Self { + owner: Owner::current().expect( + "`ScopedFuture::new_current()` \ + to be called within an `Owner` \ + context", + ), + future: fut, + } + } +} + +/// Runs a future that has access to the provided [`Owner`]'s +/// scope context. +pub fn spawn_local_with_owner( + owner: Owner, + fut: impl Future + 'static, +) { + let scoped_future = ScopedFuture::new(owner, fut); + + crate::spawn_local(async move { + if scoped_future.await.is_none() { + // TODO: should we warn here? + // /* warning message */ + } + }); +} + +/// Runs a future that has access to the provided [`Owner`]'s +/// scope context. +/// +/// # Panics +/// Panics if there is no [`Owner`] context available. +#[track_caller] +pub fn spawn_local_with_current_owner(fut: impl Future + 'static) { + let scoped_future = ScopedFuture::new_current(fut); + + crate::spawn_local(async move { + if scoped_future.await.is_none() { + // TODO: should we warn here? + // /* warning message */ + } + }); +} + +/// Runs a future that has access to the provided [`Owner`]'s +/// scope context. +/// +/// Since futures run in the background, it is possible that +/// the scope has been cleaned up since the future started running. +/// If this happens, the future will not be completed. +/// +/// The `on_cancelled` callback can be used to notify you that the +/// future was cancelled. +pub fn try_spawn_local_with_owner( + owner: Owner, + fut: impl Future + 'static, + on_cancelled: impl FnOnce() + 'static, +) { + let scoped_future = ScopedFuture::new(owner, fut); + + crate::spawn_local(async move { + if scoped_future.await.is_none() { + on_cancelled(); + } + }); +} + +/// Runs a future that has access to the provided [`Owner`]'s +/// scope context. +/// +/// Since futures run in the background, it is possible that +/// the scope has been cleaned up since the future started running. +/// If this happens, the future will not be completed. +/// +/// The `on_cancelled` callback can be used to notify you that the +/// future was cancelled. +/// +/// # Panics +/// Panics if there is no [`Owner`] context available. +#[track_caller] +pub fn try_spawn_local_with_current_owner( + fut: impl Future + 'static, + on_cancelled: impl FnOnce() + 'static, +) { + let scoped_future = ScopedFuture::new_current(fut); + + crate::spawn_local(async move { + if scoped_future.await.is_none() { + on_cancelled(); + } + }); } From 59cdd47274f8a1f7838d114a7575bdb8acfa0a17 Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Wed, 20 Sep 2023 14:47:25 -0600 Subject: [PATCH 5/8] relaxed `pin-project` dependency version --- leptos_reactive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leptos_reactive/Cargo.toml b/leptos_reactive/Cargo.toml index 72c106c17b..6750f1f16d 100644 --- a/leptos_reactive/Cargo.toml +++ b/leptos_reactive/Cargo.toml @@ -42,7 +42,7 @@ web-sys = { version = "0.3", optional = true, features = [ cfg-if = "1" indexmap = "2" self_cell = "1.0.0" -pin-project = "1.1.3" +pin-project = "1" [dev-dependencies] criterion = { version = "0.5.1", features = ["html_reports"] } From 6ba8315f7d86010b65641f2f8d59f386249a2ed1 Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Wed, 20 Sep 2023 15:35:30 -0600 Subject: [PATCH 6/8] broke borrow cycle which caused borrow panic --- leptos_reactive/src/runtime.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 002a4e7ad8..7c1c0de369 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -864,8 +864,9 @@ pub fn try_with_owner(owner: Owner, f: impl FnOnce() -> T) -> Option { runtime .nodes .try_borrow() - .map(|nodes| { - if nodes.contains_key(owner.0) { + .map(|nodes| nodes.contains_key(owner.0)) + .map(|scope_exists| { + if scope_exists { let prev_observer = runtime.observer.take(); let prev_owner = runtime.owner.take(); From 168b83371fd40693f32347d1df4fe2dbfdf67c7d Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Wed, 20 Sep 2023 15:41:25 -0600 Subject: [PATCH 7/8] refactored away from if/else --- leptos_reactive/src/runtime.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 7c1c0de369..5ac3da1531 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -866,7 +866,7 @@ pub fn try_with_owner(owner: Owner, f: impl FnOnce() -> T) -> Option { .try_borrow() .map(|nodes| nodes.contains_key(owner.0)) .map(|scope_exists| { - if scope_exists { + scope_exists.then(|| { let prev_observer = runtime.observer.take(); let prev_owner = runtime.owner.take(); @@ -878,10 +878,8 @@ pub fn try_with_owner(owner: Owner, f: impl FnOnce() -> T) -> Option { runtime.observer.set(prev_observer); runtime.owner.set(prev_owner); - Some(v) - } else { - None - } + v + }) }) .ok() .flatten() From 76448379fffd80937f78749ef34e53747fd8a9ef Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Thu, 28 Sep 2023 12:42:12 -0400 Subject: [PATCH 8/8] cargo fmt --- leptos_reactive/src/runtime.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 37dc157bea..501057effd 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -16,7 +16,6 @@ use indexmap::IndexSet; use pin_project::pin_project; use rustc_hash::{FxHashMap, FxHasher}; use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap}; -use std::task::Poll; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, @@ -25,6 +24,7 @@ use std::{ marker::PhantomData, pin::Pin, rc::Rc, + task::Poll, }; pub(crate) type PinnedFuture = Pin>>; @@ -1536,9 +1536,8 @@ impl ScopedFuture { pub fn new_current(fut: Fut) -> Self { Self { owner: Owner::current().expect( - "`ScopedFuture::new_current()` \ - to be called within an `Owner` \ - context", + "`ScopedFuture::new_current()` to be called within an `Owner` \ + context", ), future: fut, }