Browse Source

skeleton of use_server_future with reactivity

Jonathan Kelley 1 year ago
parent
commit
0c71b95e82

+ 2 - 2
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<String>) -> 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()

+ 1 - 1
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!");

+ 59 - 0
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<i32>, 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<Element> {
+    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
+}

+ 1 - 1
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
     });

+ 1 - 1
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);
     }
 

+ 74 - 124
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<T, F>(_future: impl FnOnce() -> F) -> Option<UseServerFuture<T>>
+pub fn use_server_future<T, F>(_future: impl Fn() -> F) -> UseServerFuture<T>
 where
-    T: 'static + Serialize + DeserializeOwned + Debug,
+    T: Serialize + DeserializeOwned + 'static,
     F: Future<Output = T> + 'static,
 {
-    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();
-    //     })));
-    // }
+    let value: Signal<Option<T>> = 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::<T>();
+
+        #[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();
+    }
 
-    // if first_run {
-    //     #[cfg(feature = "ssr")]
-    //     {
-    //         tracing::trace!("Suspending first run of use_server_future");
-    //         cx.suspend();
-    //     }
-    //     None
-    // } else {
-    //     Some(state)
-    // }
+    todo!()
 }
 
-pub struct UseServerFuture<T> {
-    update: Arc<dyn Fn()>,
-    needs_regen: Cell<bool>,
-    task: Cell<Option<Task>>,
-    value: Rc<RefCell<Option<Box<T>>>>,
+pub struct UseServerFuture<T: 'static> {
+    value: Signal<Option<Signal<T>>>,
 }
 
-impl<T> UseServerFuture<T> {
-    /// 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);
-        }
-    }
-
-    /// 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<Task> {
-        self.task.get()
-    }
-
-    /// Get the current state of the future.
-    pub fn reloading(&self) -> bool {
-        self.task.get().is_some()
-    }
-}
+// impl<T> UseServerFuture<T> {
+//     /// 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);
+//         }
+//     }
+
+//     /// 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<Task> {
+//         self.task.get()
+//     }
+
+//     /// Get the current state of the future.
+//     pub fn reloading(&self) -> bool {
+//         self.task.get().is_some()
+//     }
+// }

+ 1 - 1
packages/hooks/src/use_coroutine.rs

@@ -127,7 +127,7 @@ impl<T> Coroutine<T> {
     /// 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();
     }
 }
 

+ 31 - 28
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<F>(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<Task>,
 }
 
+/// 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,
-}

+ 5 - 6
packages/hooks/src/use_memo.rs

@@ -34,7 +34,7 @@ pub fn use_memo<R: PartialEq>(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<R: PartialEq, S: Storage<SignalData<R>>>(
 ///
 /// ```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);
 ///

+ 27 - 22
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<T, F>(future: impl Fn() -> F + 'static) -> UseResource<T>
+/// 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<T, F>(future: impl Fn() -> F + 'static) -> AsyncMemo<T>
 where
     T: 'static,
     F: Future<Output = T> + '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<T: 'static> {
-    value: Signal<Option<T>>,
+pub struct AsyncMemo<T: 'static> {
+    value: Signal<Option<Signal<T>>>,
     task: Signal<Option<Task>>,
     state: Signal<UseResourceState<T>>,
 }
 
-impl<T> UseResource<T> {
+impl<T> AsyncMemo<T> {
     /// Restart the future with new dependencies.
     ///
     /// Will not cancel the previous future, but will ignore any values that it
@@ -81,14 +75,15 @@ impl<T> UseResource<T> {
 
     // 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<Option<T>> {
-        self.value
+    pub fn value(&self) -> Option<Signal<T>> {
+        self.value.cloned()
     }
 
     /// Get the ID of the future in Dioxus' internal scheduler
@@ -114,6 +109,16 @@ impl<T> UseResource<T> {
         //     (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<ReadOnlySignal<T>> {
+        let out = self.value();
+        if out.is_none() {
+            suspend();
+        }
+        out.map(|sig| sig.into())
+    }
 }
 
 pub enum UseResourceState<T: 'static> {

+ 2 - 11
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<Vec<Effect>>,
     pub(crate) effect_mapping: RwLock<FxHashMap<GenerationalBoxId, Effect>>,
 }
 
-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<Effect> {
         self.effects.read().last().copied()