From 0c71b95e8297477ec7a6473daa5d87513bc27c5b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 31 Jan 2024 19:11:02 -0800 Subject: [PATCH] skeleton of use_server_future with reactivity --- examples/dog_app.rs | 4 +- examples/eval.rs | 2 +- examples/server_future.rs | 59 ++++++ examples/signals.rs | 2 +- packages/core/src/tasks.rs | 2 +- packages/fullstack/src/hooks/server_future.rs | 190 +++++++----------- packages/hooks/src/use_coroutine.rs | 2 +- packages/hooks/src/use_future.rs | 59 +++--- packages/hooks/src/use_memo.rs | 11 +- packages/hooks/src/use_resource.rs | 49 +++-- packages/signals/src/effect.rs | 13 +- 11 files changed, 200 insertions(+), 193 deletions(-) create mode 100644 examples/server_future.rs diff --git a/examples/dog_app.rs b/examples/dog_app.rs index a9c21fdfb..f61b03333 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -7,7 +7,7 @@ fn main() { fn app() -> Element { let mut breed = use_signal(|| "deerhound".to_string()); - let breed_list = use_resource(move || async move { + let breed_list = use_async_memo(move || async move { let list = reqwest::get("https://dog.ceo/api/breeds/list/all") .await .unwrap() @@ -44,7 +44,7 @@ fn app() -> Element { #[component] fn BreedPic(breed: Signal) -> Element { - let fut = use_resource(move || async move { + let fut = use_async_memo(move || async move { reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) .await .unwrap() diff --git a/examples/eval.rs b/examples/eval.rs index f79fa679c..c22d64932 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -5,7 +5,7 @@ fn main() { } fn app() -> Element { - let future = use_resource(move || async move { + let future = use_async_memo(move || async move { let mut eval = eval( r#" dioxus.send("Hi from JS!"); diff --git a/examples/server_future.rs b/examples/server_future.rs new file mode 100644 index 000000000..fcfc91f85 --- /dev/null +++ b/examples/server_future.rs @@ -0,0 +1,59 @@ +use dioxus::prelude::*; + +fn main() { + launch(app); +} + +fn app() -> Element { + let val = use_server_future(fetch_users).suspend()?; + + rsx! { + h1 { "Users" } + + } +} + +#[component] +fn ClientComponent(name: Signal, id: i64) -> Element { + rsx! { + div { "Name: {name}, ID: {id}" } + button { + onclick: move |_| async move { + // Optimistically change the name on the client + name.set("new name".to_string()); + + // Change the name on the server + change_name(id, "new name".to_string()).await; + + // And then re-fetch the user list + revalidate(user_list); + }, + "Change name" + } + } +} + +#[derive(Table)] +struct Users { + name: String, + age: i32, +} + +#[server] +async fn fetch_users() -> Result { + let users = get_users().await?; + + Ok(rsx! { + for user in users { + ClientComponent { + name: user.name, + id: user.id, + } + } + }) +} + +#[server] +async fn change_name(id: i64, new_name: String) -> Result<()> { + // Send a request to the server to change the name +} diff --git a/examples/signals.rs b/examples/signals.rs index 0a2cda7e3..e666980ae 100644 --- a/examples/signals.rs +++ b/examples/signals.rs @@ -39,7 +39,7 @@ fn app() -> Element { }); // use_resource will spawn a future that resolves to a value - essentially an async memo - let _slow_count = use_resource(move || async move { + let _slow_count = use_async_memo(move || async move { tokio::time::sleep(Duration::from_millis(200)).await; count() * 2 }); diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs index 538832911..3e6003210 100644 --- a/packages/core/src/tasks.rs +++ b/packages/core/src/tasks.rs @@ -31,7 +31,7 @@ impl Task { /// Drop the task immediately. /// /// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you - pub fn stop(self) { + pub fn cancel(self) { remove_future(self); } diff --git a/packages/fullstack/src/hooks/server_future.rs b/packages/fullstack/src/hooks/server_future.rs index 0d1265f13..bfee62433 100644 --- a/packages/fullstack/src/hooks/server_future.rs +++ b/packages/fullstack/src/hooks/server_future.rs @@ -1,6 +1,5 @@ use dioxus_lib::prelude::*; use serde::{de::DeserializeOwned, Serialize}; -// use std::any::Any; use std::cell::Cell; use std::cell::Ref; use std::cell::RefCell; @@ -11,138 +10,89 @@ use std::sync::Arc; /// A future that resolves to a value. /// -/// This runs the future only once - though the future may be regenerated -/// through the [`UseServerFuture::restart`] method. /// -/// This is commonly used for components that cannot be rendered until some -/// asynchronous operation has completed. /// -/// Whenever the hooks dependencies change, the future will be re-evaluated. -/// If a future is pending when the dependencies change, the previous future -/// will be allowed to continue +/// ```rust +/// fn User(id: String) -> Element { +/// let data = use_sever_future(move || fetch_user(id)).suspend()?; /// -/// - dependencies: a tuple of references to values that are PartialEq + Clone +/// +/// } +/// +/// ``` #[must_use = "Consider using `cx.spawn` to run a future without reading its value"] -pub fn use_server_future(_future: impl FnOnce() -> F) -> Option> +pub fn use_server_future(_future: impl Fn() -> F) -> UseServerFuture where - T: 'static + Serialize + DeserializeOwned + Debug, + T: Serialize + DeserializeOwned + 'static, F: Future + 'static, { + let value: Signal> = use_signal(|| { + // Doesn't this need to be keyed by something? + // We should try and link these IDs across the server and client + // Just the file/line/col span should be fine (or byte index) + #[cfg(feature = "ssr")] + return crate::html_storage::deserialize::take_server_data::(); + + #[cfg(not(feature = "ssr"))] + return None; + }); + + // Run the callback regardless, giving us the future without actually polling it + // This is where use_server_future gets its reactivity from + // If the client is using signals to drive the future itself, (say, via args to the server_fn), then we need to know + // what signals are being used + use_future(move || async move { + // watch the reactive context + // if it changes, restart the future + // + // if let Err(err) = crate::prelude::server_context().push_html_data(&data) { + // tracing::error!("Failed to push HTML data: {}", err); + // }; + }); + + // if there's no value ready, mark this component as suspended and return early + if value.peek().is_none() { + suspend(); + } + todo!() - // let state = use_hook(move || UseServerFuture { - // update: schedule_update(), - // needs_regen: Cell::new(true), - // value: Default::default(), - // task: Cell::new(None), - // dependencies: Vec::new(), - // }); - - // let first_run = { state.value.borrow().as_ref().is_none() && state.task.get().is_none() }; - - // #[cfg(not(feature = "ssr"))] - // { - // if first_run { - // match crate::html_storage::deserialize::take_server_data() { - // Some(data) => { - // tracing::trace!("Loaded {data:?} from server"); - // *state.value.borrow_mut() = Some(Box::new(data)); - // state.needs_regen.set(false); - // return Some(state); - // } - // None => { - // tracing::trace!("Failed to load from server... running future"); - // } - // }; - // } - // } - - // if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { - // // We don't need regen anymore - // state.needs_regen.set(false); - - // // Create the new future - // let fut = future(dependencies.out()); - - // // Clone in our cells - // let value = state.value.clone(); - // let schedule_update = state.update.clone(); - - // // Cancel the current future - // if let Some(current) = state.task.take() { - // remove_future(current); - // } - - // state.task.set(Some(push_future(async move { - // let data; - // #[cfg(feature = "ssr")] - // { - // data = fut.await; - // if first_run { - // if let Err(err) = crate::prelude::server_context().push_html_data(&data) { - // tracing::error!("Failed to push HTML data: {}", err); - // }; - // } - // } - // #[cfg(not(feature = "ssr"))] - // { - // data = fut.await; - // } - // *value.borrow_mut() = Some(Box::new(data)); - - // schedule_update(); - // }))); - // } - - // if first_run { - // #[cfg(feature = "ssr")] - // { - // tracing::trace!("Suspending first run of use_server_future"); - // cx.suspend(); - // } - // None - // } else { - // Some(state) - // } } -pub struct UseServerFuture { - update: Arc, - needs_regen: Cell, - task: Cell>, - value: Rc>>>, +pub struct UseServerFuture { + value: Signal>>, } -impl UseServerFuture { - /// Restart the future with new dependencies. - /// - /// Will not cancel the previous future, but will ignore any values that it - /// generates. - pub fn restart(&self) { - self.needs_regen.set(true); - (self.update)(); - } +// impl UseServerFuture { +// /// Restart the future with new dependencies. +// /// +// /// Will not cancel the previous future, but will ignore any values that it +// /// generates. +// pub fn restart(&self) { +// self.needs_regen.set(true); +// (self.update)(); +// } - /// Forcefully cancel a future - pub fn cancel(&self) { - if let Some(task) = self.task.take() { - remove_future(task); - } - } +// /// Forcefully cancel a future +// pub fn cancel(&self) { +// if let Some(task) = self.task.take() { +// remove_future(task); +// } +// } - /// Return any value, even old values if the future has not yet resolved. - /// - /// If the future has never completed, the returned value will be `None`. - pub fn value(&self) -> Ref<'_, T> { - Ref::map(self.value.borrow(), |v| v.as_deref().unwrap()) - } +// /// Return any value, even old values if the future has not yet resolved. +// /// +// /// If the future has never completed, the returned value will be `None`. +// pub fn value(&self) -> Ref<'_, T> { +// Ref::map(self.value.borrow(), |v| v.as_deref().unwrap()) +// } - /// Get the ID of the future in Dioxus' internal scheduler - pub fn task(&self) -> Option { - self.task.get() - } +// /// Get the ID of the future in Dioxus' internal scheduler +// pub fn task(&self) -> Option { +// self.task.get() +// } - /// Get the current state of the future. - pub fn reloading(&self) -> bool { - self.task.get().is_some() - } -} +// /// Get the current state of the future. +// pub fn reloading(&self) -> bool { +// self.task.get().is_some() +// } +// } diff --git a/packages/hooks/src/use_coroutine.rs b/packages/hooks/src/use_coroutine.rs index b63ebed1a..06f342ceb 100644 --- a/packages/hooks/src/use_coroutine.rs +++ b/packages/hooks/src/use_coroutine.rs @@ -127,7 +127,7 @@ impl Coroutine { /// Forces the component to re-render, which will re-invoke the coroutine. pub fn restart(&mut self) { self.needs_regen.set(true); - self.task().stop(); + self.task().cancel(); } } diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index db6c204bf..92942b460 100644 --- a/packages/hooks/src/use_future.rs +++ b/packages/hooks/src/use_future.rs @@ -1,7 +1,7 @@ #![allow(missing_docs)] use crate::{use_callback, use_hook_did_run, use_signal, UseCallback}; use dioxus_core::{ - prelude::{flush_sync, spawn, use_drop, use_hook}, + prelude::{flush_sync, spawn, use_hook}, Task, }; use dioxus_signals::*; @@ -16,7 +16,7 @@ pub fn use_future(mut future: impl FnMut() -> F + 'static) -> UseFuture where F: Future + 'static, { - let mut complete = use_signal(|| UseFutureState::Pending); + let mut state = use_signal(|| UseFutureState::Pending); let mut callback = use_callback(move || { let fut = future(); @@ -26,9 +26,9 @@ where // The point here is to not run use_future on the server... which like, shouldn't we? flush_sync().await; - complete.set(UseFutureState::Pending); + state.set(UseFutureState::Pending); fut.await; - complete.set(UseFutureState::Complete); + state.set(UseFutureState::Complete); }) }); @@ -46,11 +46,9 @@ where false => task.peek().pause(), }); - use_drop(move || task.peek().stop()); - UseFuture { task, - state: complete, + state, callback, } } @@ -62,13 +60,30 @@ pub struct UseFuture { callback: UseCallback, } +/// A signal that represents the state of a future +// we might add more states (panicked, etc) +#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)] +pub enum UseFutureState { + /// The future is still running + Pending, + + /// The future has been forcefully stopped + Stopped, + + /// The future has been paused, tempoarily + Paused, + + /// The future has completed + Complete, +} + impl UseFuture { /// Restart the future with new dependencies. /// /// Will not cancel the previous future, but will ignore any values that it /// generates. pub fn restart(&mut self) { - self.task.write().stop(); + self.task.write().cancel(); let new_task = self.callback.call(); self.task.set(new_task); } @@ -76,7 +91,7 @@ impl UseFuture { /// Forcefully cancel a future pub fn cancel(&mut self) { self.state.set(UseFutureState::Stopped); - self.task.write().stop(); + self.task.write().cancel(); } /// Pause the future @@ -101,9 +116,14 @@ impl UseFuture { self.task.cloned() } - /// Get the current state of the future. + /// Is the future currently finished running? + /// + /// Reading this does not subscribe to the future's state pub fn finished(&self) -> bool { - matches!(self.state.peek().clone(), UseFutureState::Complete) + matches!( + self.state.peek().clone(), + UseFutureState::Complete | UseFutureState::Stopped + ) } /// Get the current state of the future. @@ -111,20 +131,3 @@ impl UseFuture { self.state.clone().into() } } - -/// A signal that represents the state of a future -// we might add more states (panicked, etc) -#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)] -pub enum UseFutureState { - /// The future is still running - Pending, - - /// The future has been forcefully stopped - Stopped, - - /// The future has been paused, tempoarily - Paused, - - /// The future has completed - Complete, -} diff --git a/packages/hooks/src/use_memo.rs b/packages/hooks/src/use_memo.rs index 55d83d1e3..ad3acf8df 100644 --- a/packages/hooks/src/use_memo.rs +++ b/packages/hooks/src/use_memo.rs @@ -34,7 +34,7 @@ pub fn use_memo(f: impl FnMut() -> R + 'static) -> ReadOnlySignal< /// use dioxus::prelude::*; /// use dioxus_signals::*; /// -/// fn App(cx: Scope) -> Element { +/// fn App() -> Element { /// let mut count = use_signal(cx, || 0); /// let double = use_memo(cx, move || count * 2); /// count += 1; @@ -56,10 +56,9 @@ pub fn use_maybe_sync_memo>>( /// /// ```rust /// use dioxus::prelude::*; -/// use dioxus_signals::*; /// -/// fn App(cx: Scope) -> Element { -/// let mut local_state = use_state(cx, || 0); +/// fn App() -> Element { +/// let mut local_state = use_state(|| 0); /// let double = use_memo_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2); /// local_state.set(1); /// @@ -85,8 +84,8 @@ where /// use dioxus::prelude::*; /// use dioxus_signals::*; /// -/// fn App(cx: Scope) -> Element { -/// let mut local_state = use_state(cx, || 0); +/// fn App() -> Element { +/// let mut local_state = use_state(|| 0); /// let double = use_memo_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2); /// local_state.set(1); /// diff --git a/packages/hooks/src/use_resource.rs b/packages/hooks/src/use_resource.rs index 3b2e778a3..d64b7e210 100644 --- a/packages/hooks/src/use_resource.rs +++ b/packages/hooks/src/use_resource.rs @@ -1,25 +1,19 @@ #![allow(missing_docs)] use crate::use_signal; -use dioxus_core::{prelude::spawn, Task}; +use dioxus_core::{ + prelude::{spawn, suspend}, + Task, +}; use dioxus_signals::*; use futures_util::{future, pin_mut, FutureExt}; use std::future::Future; -/// A future that resolves to a value. +/// A memo that resolve to a value asynchronously. /// -/// This runs the future only once - though the future may be regenerated -/// through the [`UseFuture::restart`] method. -/// -/// This is commonly used for components that cannot be rendered until some -/// asynchronous operation has completed. -/// -/// Whenever the hooks dependencies change, the future will be re-evaluated. -/// If a future is pending when the dependencies change, the previous future -/// will be canceled before the new one is started. -/// -/// - dependencies: a tuple of references to values that are PartialEq + Clone -pub fn use_resource(future: impl Fn() -> F + 'static) -> UseResource +/// Regular memos are synchronous and resolve immediately. However, you might want to resolve a memo +#[must_use = "Consider using `cx.spawn` to run a future without reading its value"] +pub fn use_async_memo(future: impl Fn() -> F + 'static) -> AsyncMemo where T: 'static, F: Future + 'static, @@ -46,23 +40,23 @@ where .await; // Set the value - value.set(Some(res)); + value.set(Some(Signal::new(res))); }); Some(task) }); - UseResource { task, value, state } + AsyncMemo { task, value, state } } #[allow(unused)] -pub struct UseResource { - value: Signal>, +pub struct AsyncMemo { + value: Signal>>, task: Signal>, state: Signal>, } -impl UseResource { +impl AsyncMemo { /// Restart the future with new dependencies. /// /// Will not cancel the previous future, but will ignore any values that it @@ -81,14 +75,15 @@ impl UseResource { // Manually set the value in the future slot without starting the future over pub fn set(&mut self, new_value: T) { - self.value.set(Some(new_value)); + todo!() + // self.value.set(Some(new_value)); } /// Return any value, even old values if the future has not yet resolved. /// /// If the future has never completed, the returned value will be `None`. - pub fn value(&self) -> Signal> { - self.value + pub fn value(&self) -> Option> { + self.value.cloned() } /// Get the ID of the future in Dioxus' internal scheduler @@ -114,6 +109,16 @@ impl UseResource { // (Some(_), None) => UseResourceState::Pending, // } } + + /// Wait for this async memo to resolve, returning the inner signal value + /// If the value is pending, returns none and suspends the current component + pub fn suspend(&self) -> Option> { + let out = self.value(); + if out.is_none() { + suspend(); + } + out.map(|sig| sig.into()) + } } pub enum UseResourceState { diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index 79e5a871a..d459ca75f 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -1,4 +1,5 @@ use crate::write::*; +use crate::CopyValue; use core::{self, fmt::Debug}; use dioxus_core::prelude::*; use futures_channel::mpsc::UnboundedSender; @@ -8,26 +9,16 @@ use parking_lot::RwLock; use rustc_hash::FxHashMap; use std::fmt::{self, Formatter}; -use crate::CopyValue; - thread_local! { pub(crate)static EFFECT_STACK: EffectStack = EffectStack::default(); } +#[derive(Default)] pub(crate) struct EffectStack { pub(crate) effects: RwLock>, pub(crate) effect_mapping: RwLock>, } -impl Default for EffectStack { - fn default() -> Self { - Self { - effects: RwLock::new(Vec::new()), - effect_mapping: RwLock::new(FxHashMap::default()), - } - } -} - impl EffectStack { pub(crate) fn current(&self) -> Option { self.effects.read().last().copied()