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:
1 parent
ffa36a67c3
commit
022e4ad203
175 files changed
+7872
-4010
No files matched your search
+308
-234
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user