Wait for the suspense boundary above the router to resolve before sending the first streaming chunk (#3891)
* Change the suspense streaming context after suspense is resolved in the router * Wait for the suspense context before streaming the first chunk in ssr * forward more response parts * warn * extract the hydration format into a crate * Fix hydration when no data is sent * forward web and server features * Fix suspense that starts on the client * fix use_server_future on the client * Add documentation for fullstack hooks and protocol * downgrade rust edition * fix clippy * fix tests * fix clippy * Fix pure web compile * undo changes to the fullstack router example * fix formatting
This commit is contained in:
1 parent
4f74ef81d1
commit
84221f55a0
45 files changed
+988
-533
No files matched your search
@@ -0,0 +1,4 @@
|
||||
mod server_cached;
|
||||
pub use server_cached::*;
|
||||
mod server_future;
|
||||
pub use server_future::*;
|
||||
@@ -0,0 +1,56 @@
|
||||
use dioxus_core::prelude::use_hook;
|
||||
use dioxus_fullstack_protocol::SerializeContextEntry;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
/// This allows you to send data from the server to the client. The data is serialized into the HTML on the server and hydrated on the client.
|
||||
///
|
||||
/// When you run this function on the client, you need to be careful to insure the order you run it initially is the same order you run it on the server.
|
||||
///
|
||||
/// If Dioxus fullstack cannot find the data on the client, it will run the closure again to get the data.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use dioxus_lib::prelude::*;
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// fn app() -> Element {
|
||||
/// let state1 = use_server_cached(|| {
|
||||
/// 1234
|
||||
/// });
|
||||
///
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||
server_fn: impl Fn() -> O,
|
||||
) -> O {
|
||||
let location = std::panic::Location::caller();
|
||||
use_hook(|| server_cached(server_fn, location))
|
||||
}
|
||||
|
||||
pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||
value: impl FnOnce() -> O,
|
||||
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
||||
) -> O {
|
||||
let serialize = dioxus_fullstack_protocol::serialize_context();
|
||||
#[allow(unused)]
|
||||
let entry: SerializeContextEntry<O> = serialize.create_entry();
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
let data = value();
|
||||
entry.insert(&data, location);
|
||||
data
|
||||
}
|
||||
#[cfg(all(not(feature = "server"), feature = "web"))]
|
||||
{
|
||||
match entry.get() {
|
||||
Ok(value) => value,
|
||||
Err(_) => value(),
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(feature = "server", feature = "web")))]
|
||||
{
|
||||
value()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use dioxus_core::prelude::{suspend, use_hook, RenderError};
|
||||
use dioxus_hooks::*;
|
||||
use dioxus_signals::Readable;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::future::Future;
|
||||
|
||||
/// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
||||
///
|
||||
///
|
||||
/// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
||||
///
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// // ❌ The future inside of use_server_future is not reactive
|
||||
/// let id = use_signal(|| 0);
|
||||
/// use_server_future(move || {
|
||||
/// async move {
|
||||
/// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
/// println!("{id}");
|
||||
/// }
|
||||
/// });
|
||||
/// // ✅ The closure that creates the future for use_server_future is reactive
|
||||
/// let id = use_signal(|| 0);
|
||||
/// use_server_future(move || {
|
||||
/// // The closure itself is reactive which means the future will subscribe to any signals you read here
|
||||
/// let cloned_id = id();
|
||||
/// async move {
|
||||
/// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
/// println!("{cloned_id}");
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// </div>
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # async fn fetch_article(id: u32) -> String { unimplemented!() }
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn App() -> Element {
|
||||
/// let mut article_id = use_signal(|| 0);
|
||||
/// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
||||
/// // The future will rerun any time the
|
||||
/// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
||||
/// let article = use_server_future(move || fetch_article(article_id()))?;
|
||||
///
|
||||
/// rsx! {
|
||||
/// "{article().unwrap()}"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||
#[track_caller]
|
||||
pub fn use_server_future<T, F>(
|
||||
mut future: impl FnMut() -> F + 'static,
|
||||
) -> Result<Resource<T>, RenderError>
|
||||
where
|
||||
T: Serialize + DeserializeOwned + 'static,
|
||||
F: Future<Output = T> + 'static,
|
||||
{
|
||||
let serialize_context = use_hook(dioxus_fullstack_protocol::serialize_context);
|
||||
|
||||
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||
#[allow(unused)]
|
||||
let storage_entry: dioxus_fullstack_protocol::SerializeContextEntry<T> =
|
||||
use_hook(|| serialize_context.create_entry());
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
let caller = std::panic::Location::caller();
|
||||
|
||||
// If this is the first run and we are on the web client, the data might be cached
|
||||
#[cfg(feature = "web")]
|
||||
let initial_web_result =
|
||||
use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
|
||||
|
||||
let resource = use_resource(move || {
|
||||
#[cfg(feature = "server")]
|
||||
let storage_entry = storage_entry.clone();
|
||||
|
||||
let user_fut = future();
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
let initial_web_result = initial_web_result.clone();
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
async move {
|
||||
// If this is the first run and we are on the web client, the data might be cached
|
||||
#[cfg(feature = "web")]
|
||||
match initial_web_result.take() {
|
||||
// The data was deserialized successfully from the server
|
||||
Some(Ok(o)) => return o,
|
||||
|
||||
// The data is still pending from the server. Don't try to resolve it on the client
|
||||
Some(Err(dioxus_fullstack_protocol::TakeDataError::DataPending)) => {
|
||||
std::future::pending::<()>().await
|
||||
}
|
||||
|
||||
// The data was not available on the server, rerun the future
|
||||
Some(Err(_)) => {}
|
||||
|
||||
// This isn't the first run, so we don't need do anything
|
||||
None => {}
|
||||
}
|
||||
|
||||
// Otherwise just run the future itself
|
||||
let out = user_fut.await;
|
||||
|
||||
// If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
||||
#[cfg(feature = "server")]
|
||||
storage_entry.insert(&out, caller);
|
||||
|
||||
out
|
||||
}
|
||||
});
|
||||
|
||||
// On the first run, force this task to be polled right away in case its value is ready
|
||||
use_hook(|| {
|
||||
let _ = resource.task().poll_now();
|
||||
});
|
||||
|
||||
// Suspend if the value isn't ready
|
||||
if resource.state().cloned() == UseResourceState::Pending {
|
||||
let task = resource.task();
|
||||
if !task.paused() {
|
||||
return Err(suspend(task).unwrap_err());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resource)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#![warn(missing_docs)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod hooks;
|
||||
pub use hooks::*;
|
||||
mod streaming;
|
||||
pub use streaming::*;
|
||||
@@ -0,0 +1,105 @@
|
||||
use dioxus_core::prelude::try_consume_context;
|
||||
use dioxus_signals::{Readable, Signal, Writable};
|
||||
|
||||
/// The status of the streaming response
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum StreamingStatus {
|
||||
/// The initial chunk is still being rendered. The http response parts can still be modified with
|
||||
/// [DioxusServerContext::response_parts_mut](https://docs.rs/dioxus-fullstack/0.6.3/dioxus_fullstack/prelude/struct.DioxusServerContext.html#method.response_parts_mut).
|
||||
RenderingInitialChunk,
|
||||
/// The initial chunk has been committed and the response is now streaming. The http response parts
|
||||
/// have already been sent to the client and can no longer be modified.
|
||||
InitialChunkCommitted,
|
||||
}
|
||||
|
||||
/// The context dioxus fullstack provides for the status of streaming responses on the server
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct StreamingContext {
|
||||
current_status: Signal<StreamingStatus>,
|
||||
}
|
||||
|
||||
impl Default for StreamingContext {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamingContext {
|
||||
/// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
|
||||
/// provide this context for you.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit the initial chunk of the response. This will be called automatically if you are using the
|
||||
/// dioxus router when the suspense boundary above the router is resolved. Otherwise, you will need
|
||||
/// to call this manually to start the streaming part of the response.
|
||||
///
|
||||
/// Once this method has been called, the http response parts can no longer be modified.
|
||||
pub fn commit_initial_chunk(&mut self) {
|
||||
self.current_status
|
||||
.set(StreamingStatus::InitialChunkCommitted);
|
||||
}
|
||||
|
||||
/// Get the current status of the streaming response. This method is reactive and will cause
|
||||
/// the current reactive context to rerun when the status changes.
|
||||
pub fn current_status(&self) -> StreamingStatus {
|
||||
*self.current_status.read()
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit the initial chunk of the response. This will be called automatically if you are using the
|
||||
/// dioxus router when the suspense boundary above the router is resolved. Otherwise, you will need
|
||||
/// to call this manually to start the streaming part of the response.
|
||||
///
|
||||
/// On the client, this will do nothing.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_fullstack_hooks::*;
|
||||
/// # fn Children() -> Element { unimplemented!() }
|
||||
/// fn App() -> Element {
|
||||
/// // This will start streaming immediately after the current render is complete.
|
||||
/// use_hook(commit_initial_chunk);
|
||||
///
|
||||
/// rsx! { Children {} }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn commit_initial_chunk() {
|
||||
if let Some(mut streaming) = try_consume_context::<StreamingContext>() {
|
||||
streaming.commit_initial_chunk();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current status of the streaming response. This method is reactive and will cause
|
||||
/// the current reactive context to rerun when the status changes.
|
||||
///
|
||||
/// On the client, this will always return `StreamingStatus::InitialChunkCommitted`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_fullstack_hooks::*;
|
||||
/// #[component]
|
||||
/// fn MetaTitle(title: String) -> Element {
|
||||
/// // If streaming has already started, warn the user that the meta tag will not show
|
||||
/// // up in the initial chunk.
|
||||
/// use_hook(|| {
|
||||
/// if current_status() == StreamingStatus::InitialChunkCommitted {
|
||||
/// log::warn!("Since `MetaTitle` was rendered after the initial chunk was committed, the meta tag will not show up in the head without javascript enabled.");
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// rsx! { meta { property: "og:title", content: title } }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn current_status() -> StreamingStatus {
|
||||
if let Some(streaming) = try_consume_context::<StreamingContext>() {
|
||||
streaming.current_status()
|
||||
} else {
|
||||
StreamingStatus::InitialChunkCommitted
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user