Suspense boundaries/out of order streaming/anyhow like error handling (#2365)

* create static site generation helpers in the router crate

* work on integrating static site generation into fullstack

* move ssg into a separate crate

* integrate ssg with the launch builder

* simplify ssg example

* fix static_routes for child routes

* move CLI hot reloading websocket code into dioxus-hot-reload

* fix some unused imports

* use the same hot reloading websocket code for fullstack

* fix fullstack hot reloading

* move cli hot reloading logic into the hot reload crate

* ssg example working with dx serve

* add more examples

* fix clippy

* switch to a result for Element

* fix formatting

* fix hot reload doctest imports

* fix axum imports

* add show method to error context

* implement retaining nodes during suspense

* fix unterminated if statements

* communicate between tasks and suspense boundaries

* make suspense placeholders easier to use

* implement IntoDynNode and IntoVNode for more wrappers

* fix clippy examples

* fix rsx tests

* add streaming html utilities to the ssr package

* unify hydration and non-hydration ssr cache

* fix router with Result Element

* don't run server doc tests

* Fix hot reload websocket doc examples

* simple apps working with fullstack streaming

* fix preloading wasm

* Report errors encountered while streaming

* remove async from incremental renderer

* document new VirtualDom suspense methods

* make streaming work with incremental rendering

* fix static site generation

* document suspense structs

* create closure type; allow async event handlers in props; allow shorthand event handlers

* test forwarding event handlers with the shorthand syntax

* fix clippy

* fix imports in spawn async doctest

* fix empty rsx

* fix async result event handlers

* fix mounting router in multiple places

* Fix task dead cancel race condition

* simplify diffing before adding suspense

* fix binary size increase

* fix attribute diffing

* more diffing fixes

* create minimal fullstack feature

* smaller fullstack bundles

* allow mounting nodes that are already created and creating nodes without mounting them

* fix hot reload feature

* fix replacing components

* don't reclaim virtual nodes

* client side suspense working!

* fix CLI

* slightly smaller fullstack builds

* fix multiple suspended scopes

* fix merge errors

* yield back to tokio every few polls to fix suspending on many tasks at once

* remove logs

* document suspense boundary and update suspense example

* fix ssg

* make streaming optional

* fix some router and core tests

* fix suspense example

* fix serialization with out of order server futures

* add incremental streaming hackernews demo

* fix hackernews demo

* fix root hackernews redirect

* fix formatting

* add tests for suspense cases

* slightly smaller binaries

* slightly smaller

* improve error handling docs

* fix errors example link

* fix doc tests

* remove log file

* fix ssr cache type inference

* remove index.html

* fix ssg render template

* fix assigning ids on elements with dynamic attributes

* add desktop feature to the workspace examples

* remove router static generation example; ssg lives in the dioxus-static-generation package

* add a test for effects during suspense

* only run effects on mounted nodes

* fix multiple suspense roots

* fix node iterator

* fix closures without arguments

* fix dioxus-core readme doctest

* remove suspense logs

* fix scope stack

* fix clippy

* remove unused suspense boundary from hackernews

* assert that launch never returns for better compiler errors

* fix static generation launch function

* fix web renderer

* pass context providers into server functions

* add an example for FromContext

* clean up DioxusRouterExt

* fix server function context

* fix fullstack desktop example

* forward CLI serve settings to fullstack

* re-export serve config at the root of fullstack

* forward env directly instead of using a guard

* just set the port in the CLI for fullstack playwright tests

* fix fullstack dioxus-cli-config feature

* fix launch server merge conflicts

* fix fullstack launch context

* Merge branch 'main' into suspense-2.0

* fix fullstack html data

* remove drop virtual dom feature

* add a comment about only_write_templates binary size workaround

* remove explicit dependencies from use_server_future

* make ErrorContext and SuspenseContext more similar

* Tweak: small tweaks to tomls to make diff smaller

* only rerun components under suspense after the initial placeholders are sent to the client

* add module docs for suspense

* keep track of when suspense boundaries are resolved

* start implementing JS out of order streaming

* fix core tests

* implement the server side of suspense with js

* fix streaming ssr with nested suspense

* move streaming ssr code into fullstack

* revert minification changes

* serialize server future data as the html streams

* start loading scripts wasm immediately instead of defering the script

* very basic nested suspense example working with minimal html updates

* clean up some suspense/error docs

* fix hydrating nested pending server futures

* sort resolved boundaries by height

* Fix disconnecting clients while streaming

* fix static generation crate

* don't insert extra divs when hydrating streamed chunks

* wait to swap in the elements until they are hydrated

* remove inline streaming script

* hackernews partially working

* fix spa mode

* banish the open shadow dom

* fix removing placeholder

* set up streaming playwright test

* run web playwright tests on 9999 to avoid port conflicts with other local servers

* remove suspense nodes if the suspense boundary is replaced before the suspense resolves on the server

* ignore hydration of removed suspense boundaries

* use path based indexing to fix hydrating suspense after parent suspense with child is removed

* re-export dioxus error

* remove resolved suspense divs if the suspense boundary has been removed

* Fix client side initialized server futures

* ignore comment nodes while traversing nodes in core to avoid lists getting swapped out with suspense

* Pass initial hydration data to the client

* hide pre nodes

* don't panic if reclaiming an element fails

* fix scope stack when polling tasks

* improve deserialization out of length message

* Ok(VNode::placeholder()) -> VNode::empty()

* fix typo in rsx usage

* restore testing changes from suspense example

* clean up some logs and comments

* fix playwright tests

* clean up more changes in core

* clean up core tests

* remove anymap dependency

* clean up changes to hooks

* clean up changes in the router, rsx, and web

* revert changes to axum-hello-world

* fix use_server_future

* fix clippy in dioxus-core

* check that the next or previous node exist before checking if we should ignore them

* fix formatting

* fix suspense playwright test

* remove unused suspense code

* add more suspense playwright tests

* add more docs for error boundaries

* fix suspense core tests

* fix ErrorBoundary example

* remove a bunch of debug logging in js

* fix router failure_external_navigation

* use absolute paths in the interpreter build.rs

* strip '\r' while hashing ts files

* add a wrapper with a default error boundary and suspense boundary

* restore hot reloading

* ignore non-ts files when hashing

* sort ts files before hashing them

* fix rsx tests

* fix fullstack doc tests

* fix core tests

* fix axum auth example

* update suspense hydration diagram

* longer playwright build limit

* tiny fixes - spelling, formatting

* update diagram link

* remove comment and template nodes for suspense placeholders

* remove comment nodes as we hydrate text

* simplify hackernews example

* clean up hydrating text nodes

* switch to a separate environment variable for the base path for smaller binaries

* clean up file system html trait

* fix form data

* move streaming code into fullstack

* implement serialize and deserialize for CapturedError

* remove waits in the nested suspense playwright spec

* force sequential fullstack builds for CI

* longer nested suspense delay for CI

* fix --force-sequential flag

* wait to launch server until client build is done

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-07-01 20:50:36 -07:00
1 parent ffa36a67c3
commit 022e4ad203
175 files changed
+7872 -4010

No files matched your search

+175 -186
View File
@@ -1,15 +1,14 @@
//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
//!
//! # Example
//! ```rust
//! ```rust, no_run
//! #![allow(non_snake_case)]
//! use dioxus_lib::prelude::*;
//! use dioxus_fullstack::prelude::*;
//! use dioxus::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! // Hydrate the application on the client
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! launch(app);
//! #[cfg(feature = "server")]
//! {
//! tokio::runtime::Runtime::new()
@@ -22,7 +21,7 @@
//! listener,
//! axum::Router::new()
//! // Server side render the application, serve static assets, and register server functions
//! .serve_dioxus_application(ServerConfig::new(), app)
//! .serve_dioxus_application(ServeConfig::default(), app)
//! .into_make_service(),
//! )
//! .await
@@ -44,7 +43,7 @@
//! "Run a server function"
//! }
//! "Server said: {text}"
//! })
//! }
//! }
//!
//! #[server(GetServerData)]
@@ -61,7 +60,6 @@ use axum::{
response::IntoResponse,
};
use dioxus_lib::prelude::{Element, VirtualDom};
use futures_util::Future;
use http::header::*;
use std::sync::Arc;
@@ -74,21 +72,18 @@ pub trait DioxusRouterExt<S> {
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// ```rust, no_run
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_functions()
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// let router = axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_functions()
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
/// ```
fn register_server_functions(self) -> Self
@@ -101,21 +96,19 @@ pub trait DioxusRouterExt<S> {
/// Registers server functions with some additional context to insert into the [`DioxusServerContext`] for that handler.
///
/// # Example
/// ```rust
/// ```rust, no_run
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// # use std::sync::Arc;
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_functions_with_context(vec![Box::new(|| 1234567890u32)])
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// let router = axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_functions_with_context(Arc::new(vec![Box::new(|| Box::new(1234567890u32))]))
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
/// ```
fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self;
@@ -123,34 +116,24 @@ pub trait DioxusRouterExt<S> {
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
/// ```rust
/// ```rust, no_run
/// # #![allow(non_snake_case)]
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_static_assets("dist")
/// // Server render the application
/// // ...
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
///
/// fn app() -> Element {
/// unimplemented!()
/// let router = axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_static_assets("dist")
/// // Server render the application
/// // ...
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
/// ```
fn serve_static_assets(
self,
assets_path: impl Into<std::path::PathBuf>,
) -> impl Future<Output = Self> + Send + Sync
fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self
where
Self: Sized;
@@ -158,33 +141,26 @@ pub trait DioxusRouterExt<S> {
/// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
///
/// # Example
/// ```rust
/// ```rust, no_run
/// # #![allow(non_snake_case)]
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_dioxus_application(ServeConfig::new(), )
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// let router = axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_dioxus_application(ServeConfig::default(), app)
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
///
/// fn app() -> Element {
/// unimplemented!()
/// rsx! { "Hello World" }
/// }
/// ```
fn serve_dioxus_application(
self,
cfg: impl Into<ServeConfig>,
app: fn() -> Element,
) -> impl Future<Output = Self> + Send + Sync
fn serve_dioxus_application(self, cfg: impl Into<ServeConfig>, app: fn() -> Element) -> Self
where
Self: Sized;
}
@@ -210,7 +186,7 @@ where
move |server_context| {
for context_provider in context_providers.iter() {
let context = context_provider();
_ = server_context.insert_any(context);
server_context.insert_any(context);
}
},
req,
@@ -229,77 +205,70 @@ where
// TODO: This is a breaking change, but we should probably serve static assets from a different directory than dist where the server executable is located
// This would prevent issues like https://github.com/DioxusLabs/dioxus/issues/2327
fn serve_static_assets(
mut self,
assets_path: impl Into<std::path::PathBuf>,
) -> impl Future<Output = Self> + Send + Sync {
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
use tower_http::services::{ServeDir, ServeFile};
let assets_path = assets_path.into();
async move {
// Serve all files in dist folder except index.html
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&assets_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
// Serve all files in dist folder except index.html
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&assets_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
.collect::<Vec<_>>()
.join("/");
let route = format!("/{}", route);
if path.is_dir() {
self = self.nest_service(&route, ServeDir::new(path).precompressed_br());
} else {
self = self.nest_service(&route, ServeFile::new(path).precompressed_br());
}
})
.collect::<Vec<_>>()
.join("/");
let route = format!("/{}", route);
if path.is_dir() {
self = self.nest_service(&route, ServeDir::new(path).precompressed_br());
} else {
self = self.nest_service(&route, ServeFile::new(path).precompressed_br());
}
self
}
self
}
fn serve_dioxus_application(
self,
cfg: impl Into<ServeConfig>,
app: fn() -> Element,
) -> impl Future<Output = Self> + Send + Sync {
fn serve_dioxus_application(self, cfg: impl Into<ServeConfig>, app: fn() -> Element) -> Self {
let cfg = cfg.into();
async move {
let ssr_state = SSRState::new(&cfg);
// Add server functions and render index.html
let mut server = self
.serve_static_assets(cfg.assets_path.clone())
.await
.register_server_functions();
let ssr_state = SSRState::new(&cfg);
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
use dioxus_hot_reload::HotReloadRouterExt;
server = server.forward_cli_hot_reloading();
}
// Add server functions and render index.html
#[allow(unused_mut)]
let mut server = self
.serve_static_assets(cfg.assets_path.clone())
.register_server_functions();
server.fallback(get(render_handler).with_state((
cfg,
Arc::new(move || VirtualDom::new(app)),
ssr_state,
)))
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
use dioxus_hot_reload::HotReloadRouterExt;
server = server.forward_cli_hot_reloading();
}
server.fallback(
get(render_handler).with_state(
RenderHandleState::new(app)
.with_config(cfg)
.with_ssr_state(ssr_state),
),
)
}
}
@@ -313,12 +282,54 @@ fn apply_request_parts_to_response<B>(
}
}
type AxumHandler<F> = (
F,
ServeConfig,
SSRState,
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
);
/// State used by [`render_handler`] to render a dioxus component with axum
#[derive(Clone)]
pub struct RenderHandleState {
config: ServeConfig,
build_virtual_dom: Arc<dyn Fn() -> VirtualDom + Send + Sync>,
ssr_state: once_cell::sync::OnceCell<SSRState>,
}
impl RenderHandleState {
/// Create a new [`RenderHandleState`]
pub fn new(root: fn() -> Element) -> Self {
Self {
config: ServeConfig::default(),
build_virtual_dom: Arc::new(move || VirtualDom::new(root)),
ssr_state: Default::default(),
}
}
/// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be used to pass context into the root component of your application.
pub fn new_with_virtual_dom_factory(
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> Self {
Self {
config: ServeConfig::default(),
build_virtual_dom: Arc::new(build_virtual_dom),
ssr_state: Default::default(),
}
}
/// Set the [`ServeConfig`] for this [`RenderHandleState`]
pub fn with_config(mut self, config: ServeConfig) -> Self {
self.config = config;
self
}
/// Set the [`SSRState`] for this [`RenderHandleState`]. Sharing a [`SSRState`] between multiple [`RenderHandleState`]s is more efficient than creating a new [`SSRState`] for each [`RenderHandleState`].
pub fn with_ssr_state(mut self, ssr_state: SSRState) -> Self {
self.ssr_state = once_cell::sync::OnceCell::new();
if self.ssr_state.set(ssr_state).is_err() {
panic!("SSRState already set");
}
self
}
fn ssr_state(&self) -> &SSRState {
self.ssr_state.get_or_init(|| SSRState::new(&self.config))
}
}
/// SSR renderer handler for Axum with added context injection.
///
@@ -328,8 +339,7 @@ type AxumHandler<F> = (
/// use std::sync::{Arc, Mutex};
///
/// use axum::routing::get;
/// use dioxus_lib::prelude::*;
/// use dioxus_fullstack::{axum_adapter::render_handler_with_context, prelude::*};
/// use dioxus::prelude::*;
///
/// fn app() -> Element {
/// rsx! {
@@ -339,82 +349,61 @@ type AxumHandler<F> = (
///
/// #[tokio::main]
/// async fn main() {
/// let cfg = ServerConfig::new(app, ())
/// .assets_path("dist")
/// .build();
/// let ssr_state = SSRState::new(&cfg);
///
/// // This could be any state you want to be accessible from your server
/// // functions using `[DioxusServerContext::get]`.
/// let state = Arc::new(Mutex::new("state".to_string()));
///
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Register server functions, etc.
/// // Note you can use `register_server_functions_with_context`
/// // to inject the context into server functions running outside
/// // of an SSR render context.
/// .fallback(get(render_handler_with_context).with_state((
/// move |ctx| ctx.insert(state.clone()).unwrap(),
/// cfg,
/// ssr_state,
/// )))
/// .into_make_service(),
/// let router = axum::Router::new()
/// // Register server functions, etc.
/// // Note you can use `register_server_functions_with_context`
/// // to inject the context into server functions running outside
/// // of an SSR render context.
/// .fallback(get(render_handler)
/// .with_state(RenderHandleState::new(app))
/// )
/// .await
/// .unwrap();
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
/// ```
pub async fn render_handler_with_context<F: FnMut(&mut DioxusServerContext)>(
State((mut inject_context, cfg, ssr_state, virtual_dom_factory)): State<AxumHandler<F>>,
pub async fn render_handler(
State(state): State<RenderHandleState>,
request: Request<Body>,
) -> impl IntoResponse {
// Only respond to requests for HTML
if let Some(mime) = request.headers().get("Accept") {
let mime = mime.to_str().map(|mime| mime.to_ascii_lowercase());
match mime {
Ok(accepts) if accepts.contains("text/html") => {}
_ => return Err(StatusCode::NOT_ACCEPTABLE),
}
}
let cfg = &state.config;
let ssr_state = state.ssr_state();
let build_virtual_dom = state.build_virtual_dom.clone();
let (parts, _) = request.into_parts();
let url = parts.uri.path_and_query().unwrap().to_string();
let parts: Arc<tokio::sync::RwLock<http::request::Parts>> =
Arc::new(tokio::sync::RwLock::new(parts));
let mut server_context = DioxusServerContext::new(parts.clone());
inject_context(&mut server_context);
let parts: Arc<parking_lot::RwLock<http::request::Parts>> =
Arc::new(parking_lot::RwLock::new(parts));
let server_context = DioxusServerContext::from_shared_parts(parts.clone());
match ssr_state
.render(url, &cfg, move || virtual_dom_factory(), &server_context)
.render(url, cfg, move || build_virtual_dom(), &server_context)
.await
{
Ok(rendered) => {
let crate::render::RenderResponse { html, freshness } = rendered;
let mut response = axum::response::Html::from(html).into_response();
Ok((freshness, rx)) => {
let mut response = axum::response::Html::from(Body::from_stream(rx)).into_response();
freshness.write(response.headers_mut());
let headers = server_context.response_parts().unwrap().headers.clone();
let headers = server_context.response_parts().headers.clone();
apply_request_parts_to_response(headers, &mut response);
response
Ok(response)
}
Err(e) => {
tracing::error!("Failed to render page: {}", e);
report_err(e).into_response()
Ok(report_err(e).into_response())
}
}
}
type RenderHandlerExtractor = (
ServeConfig,
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
SSRState,
);
/// SSR renderer handler for Axum
pub async fn render_handler(
State((cfg, virtual_dom_factory, ssr_state)): State<RenderHandlerExtractor>,
request: Request<Body>,
) -> impl IntoResponse {
render_handler_with_context(
State((|_: &mut _| (), cfg, ssr_state, virtual_dom_factory)),
request,
)
.await
}
fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
@@ -439,7 +428,7 @@ async fn handle_server_fns_inner(
if let Some(mut service) =
server_fn::axum::get_server_fn_service(&path_string)
{
let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts)));
let server_context = DioxusServerContext::new(parts);
additional_context(&server_context);
// store Accepts and Referrer in case we need them for redirect (below)
@@ -467,7 +456,7 @@ async fn handle_server_fns_inner(
}
// apply the response parts from the server context to the response
let mut res_options = server_context.response_parts_mut().unwrap();
let mut res_options = server_context.response_parts_mut();
res.headers_mut().extend(res_options.headers.drain());
Ok(res)
+23 -11
View File
@@ -1,3 +1,4 @@
use dioxus_lib::prelude::use_hook;
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.
@@ -12,25 +13,36 @@ use serde::{de::DeserializeOwned, Serialize};
/// use dioxus_fullstack::prelude::*;
///
/// fn app() -> Element {
/// let state1 = server_cached(|| {
/// let state1 = use_server_cached(|| {
/// 1234
/// });
///
/// None
/// todo!()
/// }
/// ```
pub fn server_cached<O: 'static + Serialize + DeserializeOwned>(server_fn: impl Fn() -> O) -> O {
pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
server_fn: impl Fn() -> O,
) -> O {
#[cfg(feature = "server")]
{
let data = server_fn();
let sc = crate::prelude::server_context();
if let Err(err) = sc.push_html_data(&data) {
tracing::error!("Failed to push HTML data: {}", err);
}
data
let serialize = crate::html_storage::use_serialize_context();
use_hook(|| {
let data = server_fn();
serialize.push(&data);
data
})
}
#[cfg(not(feature = "server"))]
#[cfg(all(not(feature = "server"), feature = "web"))]
{
crate::html_storage::deserialize::take_server_data().unwrap_or_else(server_fn)
use_hook(|| {
dioxus_web::take_server_data()
.ok()
.flatten()
.unwrap_or_else(server_fn)
})
}
#[cfg(not(any(feature = "server", feature = "web")))]
{
use_hook(server_fn)
}
}
+105 -42
View File
@@ -2,46 +2,121 @@ use dioxus_lib::prelude::*;
use serde::{de::DeserializeOwned, Serialize};
use std::future::Future;
/// A future that resolves to a value.
/// 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 { todo!() }
/// 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"]
pub fn use_server_future<T, F>(_future: impl FnMut() -> F + 'static) -> Option<Resource<T>>
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 cb = use_callback(_future);
let mut first_run = use_hook(|| CopyValue::new(true));
#[cfg(feature = "server")]
let serialize_context = crate::html_storage::use_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
#[cfg(feature = "server")]
let server_storage_entry = use_hook(|| serialize_context.create_entry());
// 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(|| {
tracing::info!("First run of use_server_future");
std::rc::Rc::new(std::cell::RefCell::new(Some(
dioxus_web::take_server_data::<T>(),
)))
});
let resource = use_resource(move || {
#[cfg(feature = "server")]
let serialize_context = serialize_context.clone();
let user_fut = future();
#[cfg(feature = "web")]
let initial_web_result = initial_web_result.clone();
async move {
let user_fut = cb.call();
let currently_in_first_run = first_run.cloned();
// If this is the first run and we are on the web client, the data might be cached
if currently_in_first_run {
tracing::info!("First run of use_server_future");
// This is no longer the first run
first_run.set(false);
#[cfg(feature = "web")]
if let Some(o) = crate::html_storage::deserialize::take_server_data::<T>() {
// this is going to subscribe this resource to any reactivity given to use in the callback
// We're doing this regardless so inputs get tracked, even if we drop the future before polling it
kick_future(user_fut);
return o;
#[cfg(feature = "web")]
{
let initial = initial_web_result.borrow_mut().take();
match initial {
// This isn't the first run
None => {}
// This is the first run
Some(first_run) => {
match first_run {
// THe data was deserialized successfully from the server
Ok(Some(o)) => return o,
// The data is still pending from the server. Don't try to resolve it on the client
Ok(None) => {
tracing::trace!("Waiting for server data");
std::future::pending::<()>().await;
}
// The data was not available on the server, rerun the future
Err(_) => {}
}
}
}
}
// 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
// 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")]
if currently_in_first_run {
let _ = crate::server_context::server_context().push_html_data(&out);
}
serialize_context.insert(server_storage_entry, &out);
#[allow(clippy::let_and_return)]
out
@@ -56,24 +131,12 @@ where
// Suspend if the value isn't ready
match resource.state().cloned() {
UseResourceState::Pending => {
suspend(resource.task());
None
let task = resource.task();
if !task.paused() {
return Err(suspend(task).unwrap_err());
}
Ok(resource)
}
_ => Some(resource),
_ => Ok(resource),
}
}
#[cfg(feature = "web")]
#[inline]
fn kick_future<F, T>(user_fut: F)
where
F: Future<Output = T> + 'static,
{
// Kick the future to subscribe its dependencies
use futures_util::future::FutureExt;
let waker = futures_util::task::noop_waker();
let mut cx = std::task::Context::from_waker(&waker);
futures_util::pin_mut!(user_fut);
let _ = user_fut.poll_unpin(&mut cx);
}
@@ -1,59 +0,0 @@
use serde::de::DeserializeOwned;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use super::HTMLDataCursor;
#[allow(unused)]
pub(crate) fn serde_from_bytes<T: DeserializeOwned>(string: &[u8]) -> Option<T> {
let decompressed = match STANDARD.decode(string) {
Ok(bytes) => bytes,
Err(err) => {
tracing::error!("Failed to decode base64: {}", err);
return None;
}
};
match ciborium::from_reader(std::io::Cursor::new(decompressed)) {
Ok(data) => Some(data),
Err(err) => {
tracing::error!("Failed to deserialize: {}", err);
None
}
}
}
static SERVER_DATA: once_cell::sync::Lazy<Option<HTMLDataCursor>> =
once_cell::sync::Lazy::new(|| {
#[cfg(all(feature = "web", target_arch = "wasm32"))]
{
let window = web_sys::window()?.document()?;
let element = match window.get_element_by_id("dioxus-storage-data") {
Some(element) => element,
None => {
tracing::error!("Failed to get element by id: dioxus-storage-data");
return None;
}
};
let attribute = match element.get_attribute("data-serialized") {
Some(attribute) => attribute,
None => {
tracing::error!("Failed to get attribute: data-serialized");
return None;
}
};
let data: super::HTMLData = serde_from_bytes(attribute.as_bytes())?;
Some(data.cursor())
}
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
{
None
}
});
pub(crate) fn take_server_data<T: DeserializeOwned>() -> Option<T> {
SERVER_DATA.as_ref()?.take()
}
+47 -94
View File
@@ -1,112 +1,65 @@
#![allow(unused)]
use base64::Engine;
use std::{io::Cursor, sync::atomic::AtomicUsize};
use dioxus_lib::prelude::{has_context, provide_context, use_hook};
use serialize::serde_to_writable;
use std::{cell::RefCell, io::Cursor, rc::Rc, sync::atomic::AtomicUsize};
use base64::engine::general_purpose::STANDARD;
use serde::{de::DeserializeOwned, Serialize};
pub(crate) mod deserialize;
pub(crate) mod serialize;
#[derive(Default, Clone)]
pub(crate) struct SerializeContext {
data: Rc<RefCell<HTMLData>>,
}
impl SerializeContext {
/// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
pub(crate) fn create_entry(&self) -> usize {
self.data.borrow_mut().create_entry()
}
/// Insert data into an entry that was created with [`Self::create_entry`]
pub(crate) fn insert<T: Serialize>(&self, id: usize, value: &T) {
self.data.borrow_mut().insert(id, value);
}
/// Push resolved data into the serialized server data
pub(crate) fn push<T: Serialize>(&self, data: &T) {
self.data.borrow_mut().push(data);
}
}
pub(crate) fn use_serialize_context() -> SerializeContext {
use_hook(|| has_context().unwrap_or_else(|| provide_context(SerializeContext::default())))
}
#[derive(serde::Serialize, serde::Deserialize, Default)]
#[serde(transparent)]
pub(crate) struct HTMLData {
pub data: Vec<Vec<u8>>,
pub data: Vec<Option<Vec<u8>>>,
}
impl HTMLData {
pub(crate) fn push<T: Serialize>(&mut self, value: &T) {
/// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
pub(crate) fn create_entry(&mut self) -> usize {
let id = self.data.len();
self.data.push(None);
id
}
/// Insert data into an entry that was created with [`Self::create_entry`]
pub(crate) fn insert<T: Serialize>(&mut self, id: usize, value: &T) {
let mut serialized = Vec::new();
serialize::serde_to_writable(value, &mut serialized).unwrap();
self.data.push(serialized);
ciborium::into_writer(value, &mut serialized).unwrap();
self.data[id] = Some(serialized);
}
pub(crate) fn cursor(self) -> HTMLDataCursor {
HTMLDataCursor {
data: self.data,
index: AtomicUsize::new(0),
}
}
}
pub(crate) struct HTMLDataCursor {
data: Vec<Vec<u8>>,
index: AtomicUsize,
}
impl HTMLDataCursor {
pub fn take<T: DeserializeOwned>(&self) -> Option<T> {
let current = self.index.load(std::sync::atomic::Ordering::SeqCst);
if current >= self.data.len() {
tracing::error!(
"Tried to take more data than was available, len: {}, index: {}",
self.data.len(),
current
);
return None;
}
let mut cursor = &self.data[current];
let mut decoded = STANDARD.decode(cursor).unwrap();
self.index.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
match ciborium::from_reader(Cursor::new(decoded)) {
Ok(x) => Some(x),
Err(e) => {
tracing::error!("Error deserializing data: {:?}", e);
None
}
}
}
}
#[test]
fn serialized_and_deserializes() {
use ciborium::{from_reader, into_writer};
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
struct Data {
a: u32,
b: String,
bytes: Vec<u8>,
nested: Nested,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
struct Nested {
a: u32,
b: u16,
c: u8,
}
for x in 0..10usize {
for y in 0..10 {
let mut as_string: Vec<u8> = Vec::new();
let data = vec![
Data {
a: x as u32,
b: "hello".to_string(),
bytes: vec![0; x],
nested: Nested {
a: 1,
b: x as u16,
c: 3
},
};
y
];
serialize::serde_to_writable(&data, &mut as_string).unwrap();
println!("{:?}", as_string);
println!(
"original size: {}",
std::mem::size_of::<Data>() * data.len()
);
let mut bytes = Vec::new();
into_writer(&data, &mut bytes).unwrap();
println!("serialized size: {}", bytes.len());
println!("compressed size: {}", as_string.len());
let decoded: Vec<Data> = deserialize::serde_from_bytes(&as_string).unwrap();
assert_eq!(data, decoded);
}
/// Push resolved data into the serialized server data
pub(crate) fn push<T: Serialize>(&mut self, data: &T) {
let mut serialized = Vec::new();
ciborium::into_writer(data, &mut serialized).unwrap();
self.data.push(Some(serialized));
}
}
@@ -1,28 +1,88 @@
use dioxus_lib::prelude::dioxus_core::DynamicNode;
use dioxus_lib::prelude::{
has_context, try_consume_context, ScopeId, SuspenseBoundaryProps, VNode, VirtualDom,
};
use serde::Serialize;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use super::SerializeContext;
#[allow(unused)]
pub(crate) fn serde_to_writable<T: Serialize>(
value: &T,
write_to: &mut impl std::io::Write,
) -> Result<(), ciborium::ser::Error<std::io::Error>> {
write_to: &mut impl std::fmt::Write,
) -> Result<(), ciborium::ser::Error<std::fmt::Error>> {
let mut serialized = Vec::new();
ciborium::into_writer(value, &mut serialized)?;
write_to.write_all(STANDARD.encode(serialized).as_bytes())?;
ciborium::into_writer(value, &mut serialized).unwrap();
write_to.write_str(STANDARD.encode(serialized).as_str())?;
Ok(())
}
#[cfg(feature = "server")]
/// Encode data into a element. This is inteded to be used in the server to send data to the client.
pub(crate) fn encode_in_element(
data: &super::HTMLData,
write_to: &mut impl std::io::Write,
) -> Result<(), ciborium::ser::Error<std::io::Error>> {
write_to.write_all(
r#"<meta hidden="true" id="dioxus-storage-data" data-serialized=""#.as_bytes(),
)?;
serde_to_writable(&data, write_to)?;
Ok(write_to.write_all(r#"" />"#.as_bytes())?)
impl super::HTMLData {
/// Walks through the suspense boundary in a depth first order and extracts the data from the context API.
/// We use depth first order instead of relying on the order the hooks are called in because during suspense on the server, the order that futures are run in may be non deterministic.
pub(crate) fn extract_from_suspense_boundary(vdom: &VirtualDom, scope: ScopeId) -> Self {
let mut data = Self::default();
data.take_from_scope(vdom, scope);
data
}
fn take_from_virtual_dom(&mut self, vdom: &VirtualDom) {
self.take_from_scope(vdom, ScopeId::ROOT)
}
fn take_from_scope(&mut self, vdom: &VirtualDom, scope: ScopeId) {
vdom.in_runtime(|| {
scope.in_runtime(|| {
// Grab any serializable server context from this scope
let context: Option<SerializeContext> = has_context();
if let Some(context) = context {
let borrow = context.data.borrow();
let mut data = borrow.data.iter().cloned();
self.data.extend(data)
}
});
});
// then continue to any children
if let Some(scope) = vdom.get_scope(scope) {
// If this is a suspense boundary, move into the children first (even if they are suspended) because that will be run first on the client
if let Some(suspense_boundary) = SuspenseBoundaryProps::downcast_from_scope(scope) {
if let Some(node) = suspense_boundary.suspended_nodes.as_ref() {
self.take_from_vnode(vdom, node);
}
}
if let Some(node) = scope.try_root_node() {
self.take_from_vnode(vdom, node);
}
}
}
fn take_from_vnode(&mut self, vdom: &VirtualDom, vnode: &VNode) {
for (dynamic_node_index, dyn_node) in vnode.dynamic_nodes.iter().enumerate() {
match dyn_node {
DynamicNode::Component(comp) => {
if let Some(scope) = comp.mounted_scope(dynamic_node_index, vnode, vdom) {
self.take_from_scope(vdom, scope.id());
}
}
DynamicNode::Fragment(nodes) => {
for node in nodes {
self.take_from_vnode(vdom, node);
}
}
_ => {}
}
}
}
#[cfg(feature = "server")]
/// Encode data as base64. This is intended to be used in the server to send data to the client.
pub(crate) fn serialized(&self) -> String {
let mut serialized = Vec::new();
ciborium::into_writer(&self.data, &mut serialized).unwrap();
base64::engine::general_purpose::STANDARD.encode(serialized)
}
}
+26 -7
View File
@@ -5,10 +5,13 @@ use std::{any::Any, sync::Arc};
use dioxus_lib::prelude::{Element, VirtualDom};
pub use crate::Config;
#[allow(unused)]
pub(crate) type ContextProviders = Arc<
Vec<Box<dyn Fn() -> Box<dyn std::any::Any + Send + Sync + 'static> + Send + Sync + 'static>>,
>;
#[allow(unused)]
fn virtual_dom_factory(
root: fn() -> Element,
contexts: ContextProviders,
@@ -87,6 +90,22 @@ pub fn launch(
dioxus_mobile::launch::launch_virtual_dom(factory(), cfg)
}
#[cfg(not(any(
feature = "server",
feature = "web",
feature = "desktop",
feature = "mobile"
)))]
/// Launch a fullstack app with the given root component, contexts, and config.
#[allow(unused)]
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>>,
platform_config: Config,
) -> ! {
panic!("No platform feature enabled. Please enable one of the following features: axum, desktop, or web to use the launch API.")
}
#[cfg(feature = "server")]
#[allow(unused)]
/// Launch a server application
@@ -97,6 +116,8 @@ async fn launch_server(
) {
use clap::Parser;
use crate::prelude::RenderHandleState;
let args = dioxus_cli_config::ServeArguments::from_cli()
.unwrap_or_else(dioxus_cli_config::ServeArguments::parse);
let addr = args
@@ -116,8 +137,7 @@ async fn launch_server(
let cfg = platform_config.server_cfg.build();
let ssr_state = SSRState::new(&cfg);
let mut router = router.serve_static_assets(cfg.assets_path.clone()).await;
let mut router = router.serve_static_assets(cfg.assets_path.clone());
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
@@ -126,11 +146,10 @@ async fn launch_server(
}
router.fallback(
axum::routing::get(crate::axum_adapter::render_handler).with_state((
cfg,
Arc::new(build_virtual_dom),
ssr_state,
)),
axum::routing::get(crate::axum_adapter::render_handler).with_state(
RenderHandleState::new_with_virtual_dom_factory(build_virtual_dom)
.with_config(cfg),
),
)
};
let router = router.into_make_service();
+3 -5
View File
@@ -20,6 +20,8 @@ pub use config::*;
#[cfg(feature = "server")]
mod render;
#[cfg(feature = "server")]
mod streaming;
#[cfg(feature = "server")]
mod serve_config;
@@ -32,7 +34,7 @@ mod server_context;
/// A prelude of commonly used items in dioxus-fullstack.
pub mod prelude {
use crate::hooks;
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
pub use hooks::{server_cached::use_server_cached, server_future::use_server_future};
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
@@ -42,10 +44,6 @@ pub mod prelude {
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::render::{FullstackHTMLTemplate, SSRState};
#[cfg(feature = "router")]
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
pub use crate::router::FullstackRouterConfig;
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
+308 -234
View File
@@ -1,13 +1,15 @@
//! A shared pool of renderers for efficient server side rendering.
use crate::render::dioxus_core::NoOpMutations;
use crate::streaming::StreamingRenderer;
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
use dioxus_ssr::{
incremental::{RenderFreshness, WrapBody},
incremental::{CachedRender, RenderFreshness},
Renderer,
};
use std::future::Future;
use std::sync::Arc;
use futures_channel::mpsc::Sender;
use futures_util::{Stream, StreamExt};
use std::sync::RwLock;
use tokio::task::block_in_place;
use std::{collections::HashMap, future::Future};
use std::{fmt::Write, sync::Arc};
use tokio::task::JoinHandle;
use crate::prelude::*;
@@ -20,11 +22,16 @@ where
{
#[cfg(not(target_arch = "wasm32"))]
{
tokio::task::spawn_blocking(move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(f())
})
use tokio_util::task::LocalPoolHandle;
static TASK_POOL: std::sync::OnceLock<LocalPoolHandle> = std::sync::OnceLock::new();
let pool = TASK_POOL.get_or_init(|| {
let threads = std::thread::available_parallelism()
.unwrap_or(std::num::NonZeroUsize::new(1).unwrap());
LocalPoolHandle::new(threads.into())
});
pool.spawn_pinned(f)
}
#[cfg(target_arch = "wasm32")]
{
@@ -32,134 +39,281 @@ where
}
}
enum SsrRendererPool {
Renderer(RwLock<Vec<Renderer>>),
Incremental(RwLock<Vec<dioxus_ssr::incremental::IncrementalRenderer>>),
struct SsrRendererPool {
renderers: RwLock<Vec<Renderer>>,
incremental_cache: Option<RwLock<dioxus_ssr::incremental::IncrementalRenderer>>,
}
impl SsrRendererPool {
async fn render_to(
fn new(
initial_size: usize,
incremental: Option<dioxus_ssr::incremental::IncrementalRendererConfig>,
) -> Self {
let renderers = RwLock::new((0..initial_size).map(|_| pre_renderer()).collect());
Self {
renderers,
incremental_cache: incremental.map(|cache| RwLock::new(cache.build())),
}
}
fn check_cached_route(
&self,
route: &str,
render_into: &mut Sender<Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
) -> Option<RenderFreshness> {
if let Some(incremental) = &self.incremental_cache {
if let Ok(mut incremental) = incremental.write() {
match incremental.get(route) {
Ok(Some(cached_render)) => {
let CachedRender {
freshness,
response,
..
} = cached_render;
_ = render_into.start_send(String::from_utf8(response.to_vec()).map_err(
|err| {
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
err,
))
},
));
return Some(freshness);
}
Err(e) => {
tracing::error!(
"Failed to get route \"{route}\" from incremental cache: {e}"
);
}
_ => {}
}
}
}
None
}
async fn render_to(
self: Arc<Self>,
cfg: &ServeConfig,
route: String,
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
server_context: &DioxusServerContext,
) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> {
let wrapper = FullstackHTMLTemplate {
cfg: cfg.clone(),
server_context: server_context.clone(),
};
match self {
Self::Renderer(pool) => {
let server_context = server_context.clone();
let mut renderer = pool.write().unwrap().pop().unwrap_or_else(pre_renderer);
) -> Result<
(
RenderFreshness,
impl Stream<Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
),
dioxus_ssr::incremental::IncrementalRendererError,
> {
struct ReceiverWithDrop {
receiver: futures_channel::mpsc::Receiver<
Result<String, dioxus_ssr::incremental::IncrementalRendererError>,
>,
cancel_task: Option<tokio::task::JoinHandle<()>>,
}
let (tx, rx) = tokio::sync::oneshot::channel();
impl Stream for ReceiverWithDrop {
type Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>;
spawn_platform(move || async move {
let mut vdom = virtual_dom_factory();
let mut to = WriteBuffer { buffer: Vec::new() };
// poll the future, which may call server_context()
tracing::info!("Rebuilding vdom");
with_server_context(server_context.clone(), || {
block_in_place(|| vdom.rebuild(&mut NoOpMutations));
});
ProvideServerContext::new(vdom.wait_for_suspense(), server_context).await;
tracing::info!("Suspense resolved");
if let Err(err) = wrapper.render_before_body(&mut *to) {
let _ = tx.send(Err(err));
return;
}
if let Err(err) = renderer.render_to(&mut to, &vdom) {
let _ = tx.send(Err(
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
));
return;
}
if let Err(err) = wrapper.render_after_body(&mut *to) {
let _ = tx.send(Err(err));
return;
}
match String::from_utf8(to.buffer) {
Ok(html) => {
let _ = tx.send(Ok((renderer, RenderFreshness::now(None), html)));
}
Err(err) => {
_ = tx.send(Err(
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
err,
)),
));
}
}
});
let (renderer, freshness, html) = rx.await.unwrap()?;
pool.write().unwrap().push(renderer);
Ok((freshness, html))
}
Self::Incremental(pool) => {
let mut renderer =
pool.write().unwrap().pop().unwrap_or_else(|| {
incremental_pre_renderer(cfg.incremental.as_ref().unwrap())
});
let (tx, rx) = tokio::sync::oneshot::channel();
let server_context = server_context.clone();
spawn_platform(move || async move {
let mut to = WriteBuffer { buffer: Vec::new() };
match renderer
.render(
route,
virtual_dom_factory,
&mut *to,
|vdom| {
Box::pin(async move {
// poll the future, which may call server_context()
tracing::info!("Rebuilding vdom");
with_server_context(server_context.clone(), || {
block_in_place(|| vdom.rebuild(&mut NoOpMutations));
});
ProvideServerContext::new(
vdom.wait_for_suspense(),
server_context,
)
.await;
tracing::info!("Suspense resolved");
})
},
&wrapper,
)
.await
{
Ok(freshness) => {
match String::from_utf8(to.buffer).map_err(|err| {
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
err,
))
}) {
Ok(html) => {
let _ = tx.send(Ok((freshness, html)));
}
Err(err) => {
let _ = tx.send(Err(err));
}
}
}
Err(err) => {
let _ = tx.send(Err(err));
}
}
});
let (freshness, html) = rx.await.unwrap()?;
Ok((freshness, html))
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.receiver.poll_next_unpin(cx)
}
}
// When we drop the stream, we need to cancel the task that is feeding values to the stream
impl Drop for ReceiverWithDrop {
fn drop(&mut self) {
if let Some(cancel_task) = self.cancel_task.take() {
cancel_task.abort();
}
}
}
let (mut into, rx) = futures_channel::mpsc::channel::<
Result<String, dioxus_ssr::incremental::IncrementalRendererError>,
>(1000);
// before we even spawn anything, we can check synchronously if we have the route cached
if let Some(freshness) = self.check_cached_route(&route, &mut into) {
return Ok((
freshness,
ReceiverWithDrop {
receiver: rx,
cancel_task: None,
},
));
}
let wrapper = FullstackHTMLTemplate { cfg: cfg.clone() };
let server_context = server_context.clone();
let mut renderer = self
.renderers
.write()
.unwrap()
.pop()
.unwrap_or_else(pre_renderer);
let myself = self.clone();
let join_handle = spawn_platform(move || async move {
let mut virtual_dom = virtual_dom_factory();
let mut pre_body = String::new();
if let Err(err) = wrapper.render_before_body(&mut pre_body) {
_ = into.start_send(Err(err));
return;
}
if let Err(err) = write!(&mut pre_body, "<script>{INITIALIZE_STREAMING_JS}</script>") {
_ = into.start_send(Err(
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
));
return;
}
let stream = Arc::new(StreamingRenderer::new(pre_body, into));
let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new()));
renderer.pre_render = true;
{
let scope_to_mount_mapping = scope_to_mount_mapping.clone();
let stream = stream.clone();
renderer.set_render_components(move |renderer, to, vdom, scope| {
let is_suspense_boundary = vdom
.get_scope(scope)
.and_then(|s| SuspenseBoundaryProps::downcast_from_scope(s))
.filter(|s| s.suspended())
.is_some();
if is_suspense_boundary {
let mount = stream.render_placeholder(
|to| renderer.render_scope(to, vdom, scope),
&mut *to,
)?;
scope_to_mount_mapping.write().unwrap().insert(scope, mount);
} else {
renderer.render_scope(to, vdom, scope)?
}
Ok(())
});
}
macro_rules! throw_error {
($e:expr) => {
stream.close_with_error($e);
return;
};
}
// poll the future, which may call server_context()
tracing::info!("Rebuilding vdom");
with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());
// Render the initial frame with loading placeholders
let mut initial_frame = renderer.render(&virtual_dom);
// Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
// Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
let resolved_data = serialize_server_data(&virtual_dom, ScopeId::ROOT);
initial_frame.push_str(&format!(
r#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
));
// Along with the initial frame, we render the html after the main element, but before the body tag closes. This should include the script that starts loading the wasm bundle.
if let Err(err) = wrapper.render_after_main(&mut initial_frame) {
throw_error!(err);
}
stream.render(initial_frame);
// After the initial render, we need to resolve suspense
while virtual_dom.suspended_tasks_remaining() {
ProvideServerContext::new(
virtual_dom.wait_for_suspense_work(),
server_context.clone(),
)
.await;
let resolved_suspense_nodes = ProvideServerContext::new(
virtual_dom.render_suspense_immediate(),
server_context.clone(),
)
.await;
// Just rerender the resolved nodes
for scope in resolved_suspense_nodes {
let mount = {
let mut lock = scope_to_mount_mapping.write().unwrap();
lock.remove(&scope).unwrap()
};
let mut resolved_chunk = String::new();
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
let render_suspense = |into: &mut String| {
renderer.reset_hydration();
renderer.render_scope(into, &virtual_dom, scope)
};
let resolved_data = serialize_server_data(&virtual_dom, scope);
if let Err(err) = stream.replace_placeholder(
mount,
render_suspense,
resolved_data,
&mut resolved_chunk,
) {
throw_error!(
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err)
);
}
stream.render(resolved_chunk);
}
}
tracing::info!("Suspense resolved");
// After suspense is done, we render the html after the body
let mut post_streaming = String::new();
if let Err(err) = wrapper.render_after_body(&mut post_streaming) {
throw_error!(err);
}
// If incremental rendering is enabled, add the new render to the cache without the streaming bits
if let Some(incremental) = &self.incremental_cache {
let mut cached_render = String::new();
if let Err(err) = wrapper.render_before_body(&mut cached_render) {
throw_error!(err);
}
cached_render.push_str(&post_streaming);
if let Ok(mut incremental) = incremental.write() {
let _ = incremental.cache(route, cached_render);
}
}
stream.render(post_streaming);
renderer.reset_render_components();
myself.renderers.write().unwrap().push(renderer);
});
Ok((
RenderFreshness::now(None),
ReceiverWithDrop {
receiver: rx,
cancel_task: Some(join_handle),
},
))
}
}
fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> String {
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
// Extract any data we serialized for hydration (from server futures)
let html_data =
crate::html_storage::HTMLData::extract_from_suspense_boundary(virtual_dom, scope);
// serialize the server state into a base64 string
html_data.serialized()
}
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
#[derive(Clone)]
pub struct SSRState {
@@ -170,24 +324,8 @@ pub struct SSRState {
impl SSRState {
/// Create a new [`SSRState`].
pub fn new(cfg: &ServeConfig) -> Self {
if cfg.incremental.is_some() {
return Self {
renderers: Arc::new(SsrRendererPool::Incremental(RwLock::new(vec![
incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
]))),
};
}
Self {
renderers: Arc::new(SsrRendererPool::Renderer(RwLock::new(vec![
pre_renderer(),
pre_renderer(),
pre_renderer(),
pre_renderer(),
]))),
renderers: Arc::new(SsrRendererPool::new(4, cfg.incremental.clone())),
}
}
@@ -198,15 +336,17 @@ impl SSRState {
cfg: &'a ServeConfig,
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
server_context: &'a DioxusServerContext,
) -> Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError> {
let ServeConfig { .. } = cfg;
let (freshness, html) = self
.renderers
) -> Result<
(
RenderFreshness,
impl Stream<Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
),
dioxus_ssr::incremental::IncrementalRendererError,
> {
self.renderers
.clone()
.render_to(cfg, route, virtual_dom_factory, server_context)
.await?;
Ok(RenderResponse { html, freshness })
.await
}
}
@@ -214,93 +354,60 @@ impl SSRState {
#[derive(Default)]
pub struct FullstackHTMLTemplate {
cfg: ServeConfig,
server_context: DioxusServerContext,
}
impl FullstackHTMLTemplate {
/// Create a new [`FullstackHTMLTemplate`].
pub fn new(cfg: &ServeConfig, server_context: &DioxusServerContext) -> Self {
Self {
cfg: cfg.clone(),
server_context: server_context.clone(),
}
pub fn new(cfg: &ServeConfig) -> Self {
Self { cfg: cfg.clone() }
}
}
impl dioxus_ssr::incremental::WrapBody for FullstackHTMLTemplate {
fn render_before_body<R: std::io::Write>(
impl FullstackHTMLTemplate {
/// Render any content before the body of the page.
pub fn render_before_body<R: std::fmt::Write>(
&self,
to: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
let ServeConfig { index, .. } = &self.cfg;
to.write_all(index.pre_main.as_bytes())?;
to.write_str(&index.pre_main)?;
Ok(())
}
fn render_after_body<R: std::io::Write>(
/// Render all content after the main element of the page.
pub fn render_after_main<R: std::fmt::Write>(
&self,
to: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
// serialize the server state
crate::html_storage::serialize::encode_in_element(
&*self.server_context.html_data().map_err(|_| {
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new({
#[derive(Debug)]
struct HTMLDataReadError;
impl std::fmt::Display for HTMLDataReadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
"Failed to read the server data to serialize it into the HTML",
)
}
}
impl std::error::Error for HTMLDataReadError {}
HTMLDataReadError
}))
})?,
to,
)
.map_err(|err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)))?;
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{
// In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work
let disconnect_js = dioxus_hot_reload::RECONNECT_SCRIPT;
to.write_all(r#"<script>"#.as_bytes())?;
to.write_all(disconnect_js.as_bytes())?;
to.write_all(r#"</script>"#.as_bytes())?;
to.write_str(r#"<script>"#)?;
to.write_str(disconnect_js)?;
to.write_str(r#"</script>"#)?;
}
let ServeConfig { index, .. } = &self.cfg;
to.write_all(index.post_main.as_bytes())?;
to.write_str(&index.post_main)?;
Ok(())
}
}
/// A rendered response from the server.
#[derive(Debug)]
pub struct RenderResponse {
pub(crate) html: String,
pub(crate) freshness: RenderFreshness,
}
/// Render all content after the body of the page.
pub fn render_after_body<R: std::fmt::Write>(
&self,
to: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
let ServeConfig { index, .. } = &self.cfg;
impl RenderResponse {
/// Get the rendered HTML.
pub fn html(&self) -> &str {
&self.html
}
to.write_str(&index.after_closing_body_tag)?;
/// Get the freshness of the rendered HTML.
pub fn freshness(&self) -> RenderFreshness {
self.freshness
Ok(())
}
}
@@ -309,36 +416,3 @@ fn pre_renderer() -> Renderer {
renderer.pre_render = true;
renderer
}
fn incremental_pre_renderer(
cfg: &IncrementalRendererConfig,
) -> dioxus_ssr::incremental::IncrementalRenderer {
let mut renderer = cfg.clone().build();
renderer.renderer_mut().pre_render = true;
renderer
}
struct WriteBuffer {
buffer: Vec<u8>,
}
impl std::fmt::Write for WriteBuffer {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.buffer.extend_from_slice(s.as_bytes());
Ok(())
}
}
impl std::ops::Deref for WriteBuffer {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.buffer
}
}
impl std::ops::DerefMut for WriteBuffer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.buffer
}
}
+11 -6
View File
@@ -12,8 +12,7 @@ pub struct ServeConfigBuilder {
pub(crate) index_html: Option<String>,
pub(crate) index_path: Option<PathBuf>,
pub(crate) assets_path: Option<PathBuf>,
pub(crate) incremental:
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
pub(crate) incremental: Option<dioxus_ssr::incremental::IncrementalRendererConfig>,
}
impl ServeConfigBuilder {
@@ -30,7 +29,7 @@ impl ServeConfigBuilder {
/// Enable incremental static generation
pub fn incremental(mut self, cfg: dioxus_ssr::incremental::IncrementalRendererConfig) -> Self {
self.incremental = Some(std::sync::Arc::new(cfg));
self.incremental = Some(cfg);
self
}
@@ -108,9 +107,15 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
post_main.1.to_string(),
);
let (post_main, after_closing_body_tag) =
post_main.split_once("</body>").unwrap_or_else(|| {
panic!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
});
IndexHtml {
pre_main,
post_main,
post_main: post_main.to_string(),
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
}
}
@@ -118,6 +123,7 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
pub(crate) struct IndexHtml {
pub(crate) pre_main: String,
pub(crate) post_main: String,
pub(crate) after_closing_body_tag: String,
}
/// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
@@ -127,8 +133,7 @@ pub struct ServeConfig {
pub(crate) index: IndexHtml,
#[allow(dead_code)]
pub(crate) assets_path: PathBuf,
pub(crate) incremental:
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
pub(crate) incremental: Option<dioxus_ssr::incremental::IncrementalRendererConfig>,
}
impl Default for ServeConfig {
+38 -84
View File
@@ -1,8 +1,7 @@
use crate::html_storage::HTMLData;
use parking_lot::RwLock;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
type SendSyncAnyMap =
std::collections::HashMap<std::any::TypeId, Box<dyn Any + Send + Sync + 'static>>;
@@ -13,44 +12,50 @@ type SendSyncAnyMap =
/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope.
#[derive(Clone)]
pub struct DioxusServerContext {
shared_context: std::sync::Arc<std::sync::RwLock<SendSyncAnyMap>>,
response_parts: std::sync::Arc<std::sync::RwLock<http::response::Parts>>,
pub(crate) parts: Arc<tokio::sync::RwLock<http::request::Parts>>,
html_data: Arc<RwLock<HTMLData>>,
shared_context: std::sync::Arc<RwLock<SendSyncAnyMap>>,
response_parts: std::sync::Arc<RwLock<http::response::Parts>>,
pub(crate) parts: Arc<RwLock<http::request::Parts>>,
}
#[allow(clippy::derivable_impls)]
impl Default for DioxusServerContext {
fn default() -> Self {
Self {
shared_context: std::sync::Arc::new(std::sync::RwLock::new(HashMap::new())),
shared_context: std::sync::Arc::new(RwLock::new(HashMap::new())),
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
parts: std::sync::Arc::new(tokio::sync::RwLock::new(
http::request::Request::new(()).into_parts().0,
)),
html_data: Arc::new(RwLock::new(HTMLData::default())),
parts: std::sync::Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)),
}
}
}
mod server_fn_impl {
use super::*;
use parking_lot::{RwLockReadGuard, RwLockWriteGuard};
use std::any::{Any, TypeId};
use std::sync::LockResult;
use std::sync::{PoisonError, RwLockReadGuard, RwLockWriteGuard};
impl DioxusServerContext {
/// Create a new server context from a request
pub fn new(parts: impl Into<Arc<tokio::sync::RwLock<http::request::Parts>>>) -> Self {
pub fn new(parts: http::request::Parts) -> Self {
Self {
parts: parts.into(),
parts: Arc::new(RwLock::new(parts)),
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
}
}
/// Create a server context from a shared parts
#[allow(unused)]
pub(crate) fn from_shared_parts(parts: Arc<RwLock<http::request::Parts>>) -> Self {
Self {
parts,
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
html_data: Arc::new(RwLock::new(HTMLData::default())),
}
}
@@ -58,79 +63,46 @@ mod server_fn_impl {
pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
self.shared_context
.read()
.ok()?
.get(&TypeId::of::<T>())
.map(|v| v.downcast_ref::<T>().unwrap().clone())
}
/// Insert a value into the shared server context
pub fn insert<T: Any + Send + Sync + 'static>(
&self,
value: T,
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
pub fn insert<T: Any + Send + Sync + 'static>(&self, value: T) {
self.shared_context
.write()
.map(|mut map| map.insert(TypeId::of::<T>(), Box::new(value)))
.map(|_| ())
.insert(TypeId::of::<T>(), Box::new(value));
}
/// Insert a Boxed `Any` value into the shared server context
pub fn insert_any(
&self,
value: Box<dyn Any + Send + Sync>,
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
pub fn insert_any(&self, value: Box<dyn Any + Send + Sync>) {
self.shared_context
.write()
.map(|mut map| map.insert((*value).type_id(), value))
.map(|_| ())
.insert((*value).type_id(), value);
}
/// Get the response parts from the server context
pub fn response_parts(&self) -> LockResult<RwLockReadGuard<'_, http::response::Parts>> {
pub fn response_parts(&self) -> RwLockReadGuard<'_, http::response::Parts> {
self.response_parts.read()
}
/// Get the response parts from the server context
pub fn response_parts_mut(
&self,
) -> LockResult<RwLockWriteGuard<'_, http::response::Parts>> {
pub fn response_parts_mut(&self) -> RwLockWriteGuard<'_, http::response::Parts> {
self.response_parts.write()
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub async fn request_parts(
&self,
) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
self.parts.read().await
pub fn request_parts(&self) -> parking_lot::RwLockReadGuard<'_, http::request::Parts> {
self.parts.read()
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub fn request_parts_blocking(
&self,
) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
self.parts.blocking_read()
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub async fn request_parts_mut(
&self,
) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
self.parts.write().await
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub fn request_parts_mut_blocking(
&self,
) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
self.parts.blocking_write()
pub fn request_parts_mut(&self) -> parking_lot::RwLockWriteGuard<'_, http::request::Parts> {
self.parts.write()
}
/// Extract some part from the request
@@ -139,21 +111,6 @@ mod server_fn_impl {
) -> Result<T, R> {
T::from_request(self).await
}
/// Insert some data into the html data store
pub(crate) fn push_html_data<T: serde::Serialize>(
&self,
value: &T,
) -> Result<(), PoisonError<RwLockWriteGuard<'_, HTMLData>>> {
self.html_data.write().map(|mut map| {
map.push(value);
})
}
/// Get the html data store
pub(crate) fn html_data(&self) -> LockResult<RwLockReadGuard<'_, HTMLData>> {
self.html_data.read()
}
}
}
@@ -189,7 +146,7 @@ pub fn with_server_context<O>(context: DioxusServerContext, f: impl FnOnce() ->
/// A future that provides the server context to the inner future
#[pin_project::pin_project]
pub struct ProvideServerContext<F: std::future::Future> {
context: Option<DioxusServerContext>,
context: DioxusServerContext,
#[pin]
f: F,
}
@@ -197,10 +154,7 @@ pub struct ProvideServerContext<F: std::future::Future> {
impl<F: std::future::Future> ProvideServerContext<F> {
/// Create a new future that provides the server context to the inner future
pub fn new(f: F, context: DioxusServerContext) -> Self {
Self {
context: Some(context),
f,
}
Self { f, context }
}
}
@@ -212,10 +166,8 @@ impl<F: std::future::Future> std::future::Future for ProvideServerContext<F> {
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.project();
let context = this.context.take().unwrap();
let result = with_server_context(context.clone(), || this.f.poll(cx));
*this.context = Some(context);
result
let context = this.context.clone();
with_server_context(context, || this.f.poll(cx))
}
}
@@ -302,7 +254,9 @@ impl<
{
type Rejection = R;
#[allow(clippy::all)]
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
Ok(I::from_request_parts(&mut *req.request_parts_mut().await, &()).await?)
let mut lock = req.request_parts_mut();
I::from_request_parts(&mut lock, &()).await
}
}
+144
View File
@@ -0,0 +1,144 @@
//! There are two common ways to render suspense:
//! 1. Stream the HTML in order - this will work even if javascript is disabled, but if there is something slow at the top of your page, and fast at the bottom, nothing will render until the slow part is done
//! 2. Render placeholders and stream the HTML out of order - this will only work if javascript is enabled. This lets you render any parts of your page that resolve quickly, and then render the rest of the page as it becomes available
//!
//! Dioxus currently uses a the second out of order streaming approach which requires javascript. The rendering structure is as follows:
//! ```html
//! // Initial content is sent down with placeholders
//! <div>
//! Header
//! <div class="flex flex-col">
//! // If we reach a suspense placeholder that may be replaced later, we insert a template node with a unique id to replace later
//! <div>Loading user info...</div>
//! </div>
//! Footer
//! </div>
//! // After the initial render is done, we insert divs that are hidden with new content.
//! // We use divs instead of templates for better SEO
//! <script>
//! // Code to hook up hydration replacement
//! </script>
//! <div hidden id="ds-1-r">
//! <div>Final HTML</div>
//! </div>
//! <script>
//! window.dx_hydrate(2, "suspenseboundarydata");
//! </script>
//! ```
use futures_channel::mpsc::Sender;
use std::{
fmt::{Display, Write},
sync::{Arc, RwLock},
};
/// Sections are identified by a unique id based on the suspense path. We only track the path of suspense boundaries because the client may render different components than the server.
#[derive(Clone, Debug, Default)]
struct MountPath {
parent: Option<Arc<MountPath>>,
id: usize,
}
impl MountPath {
fn child(&self) -> Self {
Self {
parent: Some(Arc::new(self.clone())),
id: 0,
}
}
}
impl Display for MountPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(parent) = &self.parent {
write!(f, "{},", parent)?;
}
write!(f, "{}", self.id)
}
}
pub(crate) struct StreamingRenderer<E = std::convert::Infallible> {
channel: RwLock<Sender<Result<String, E>>>,
current_path: RwLock<MountPath>,
}
impl<E> StreamingRenderer<E> {
/// Create a new streaming renderer with the given head that renders into a channel
pub(crate) fn new(
before_body: impl Display,
mut render_into: Sender<Result<String, E>>,
) -> Self {
let start_html = before_body.to_string();
_ = render_into.start_send(Ok(start_html));
Self {
channel: render_into.into(),
current_path: Default::default(),
}
}
/// Render a new chunk of html that will never change
pub(crate) fn render(&self, html: impl Display) {
_ = self
.channel
.write()
.unwrap()
.start_send(Ok(html.to_string()));
}
/// Render a new chunk of html that may change
pub(crate) fn render_placeholder<W: Write + ?Sized>(
&self,
html: impl FnOnce(&mut W) -> std::fmt::Result,
into: &mut W,
) -> Result<Mount, std::fmt::Error> {
let id = self.current_path.read().unwrap().clone();
// Increment the id for the next placeholder
self.current_path.write().unwrap().id += 1;
// While we are inside the placeholder, set the suspense path to the suspense boundary that we are rendering
let old_path = std::mem::replace(&mut *self.current_path.write().unwrap(), id.child());
html(into)?;
// Restore the old path
*self.current_path.write().unwrap() = old_path;
Ok(Mount { id })
}
/// Replace a placeholder that was rendered previously
pub(crate) fn replace_placeholder<W: Write + ?Sized>(
&self,
id: Mount,
html: impl FnOnce(&mut W) -> std::fmt::Result,
data: impl Display,
into: &mut W,
) -> std::fmt::Result {
// Then replace the suspense placeholder with the new content
write!(into, r#"<div id="ds-{id}-r" hidden>"#)?;
// While we are inside the placeholder, set the suspense path to the suspense boundary that we are rendering
let old_path = std::mem::replace(&mut *self.current_path.write().unwrap(), id.id.child());
html(into)?;
// Restore the old path
*self.current_path.write().unwrap() = old_path;
write!(
into,
r#"</div><script>window.dx_hydrate([{id}], "{data}")</script>"#
)
}
/// Close the stream with an error
pub(crate) fn close_with_error(&self, error: E) {
_ = self.channel.write().unwrap().start_send(Err(error));
}
}
/// A mounted placeholder in the dom that may change in the future
#[derive(Clone, Debug)]
pub(crate) struct Mount {
id: MountPath,
}
impl Display for Mount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.id)
}
}