Bubble errors from the server to the client on suspense boundaries (#2772)
* Bubble errors from the server to the client on suspense boundaries * make resolving suspense boundaries O(n)
This commit is contained in:
1 parent
3003362b52
commit
b0a2e2418c
12 files changed
+190
-39
No files matched your search
@@ -1,7 +1,7 @@
|
||||
use dioxus_lib::prelude::dioxus_core::DynamicNode;
|
||||
use dioxus_lib::prelude::{
|
||||
has_context, try_consume_context, ScopeId, SuspenseBoundaryProps, SuspenseContext, VNode,
|
||||
VirtualDom,
|
||||
has_context, try_consume_context, ErrorContext, ScopeId, SuspenseBoundaryProps,
|
||||
SuspenseContext, VNode, VirtualDom,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -26,6 +26,14 @@ impl super::HTMLData {
|
||||
/// 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();
|
||||
// If there is an error boundary on the suspense boundary, grab the error from the context API
|
||||
// and throw it on the client so that it bubbles up to the nearest error boundary
|
||||
let mut error = vdom.in_runtime(|| {
|
||||
scope
|
||||
.consume_context::<ErrorContext>()
|
||||
.and_then(|error_context| error_context.errors().first().cloned())
|
||||
});
|
||||
data.push(&error);
|
||||
data.take_from_scope(vdom, scope);
|
||||
data
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! A shared pool of renderers for efficient server side rendering.
|
||||
use crate::streaming::StreamingRenderer;
|
||||
use crate::streaming::{Mount, StreamingRenderer};
|
||||
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
|
||||
use dioxus_ssr::{
|
||||
incremental::{CachedRender, RenderFreshness},
|
||||
@@ -15,6 +15,13 @@ use tokio::task::JoinHandle;
|
||||
use crate::prelude::*;
|
||||
use dioxus_lib::prelude::*;
|
||||
|
||||
/// A suspense boundary that is pending with a placeholder in the client
|
||||
struct PendingSuspenseBoundary {
|
||||
mount: Mount,
|
||||
children: Vec<ScopeId>,
|
||||
}
|
||||
|
||||
/// Spawn a task in the background. If wasm is enabled, this will use the single threaded tokio runtime
|
||||
fn spawn_platform<Fut>(f: impl FnOnce() -> Fut + Send + 'static) -> JoinHandle<Fut::Output>
|
||||
where
|
||||
Fut: Future + 'static,
|
||||
@@ -56,6 +63,7 @@ impl SsrRendererPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Look for a cached route in the incremental cache and send it into the render channel if it exists
|
||||
fn check_cached_route(
|
||||
&self,
|
||||
route: &str,
|
||||
@@ -91,6 +99,8 @@ impl SsrRendererPool {
|
||||
None
|
||||
}
|
||||
|
||||
/// Render a virtual dom into a stream. This method will return immediately and continue streaming the result in the background
|
||||
/// The streaming is canceled when the stream the function returns is dropped
|
||||
async fn render_to(
|
||||
self: Arc<Self>,
|
||||
cfg: &ServeConfig,
|
||||
@@ -181,6 +191,9 @@ impl SsrRendererPool {
|
||||
{
|
||||
let scope_to_mount_mapping = scope_to_mount_mapping.clone();
|
||||
let stream = stream.clone();
|
||||
// We use a stack to keep track of what suspense boundaries we are nested in to add children to the correct boundary
|
||||
// The stack starts with the root scope because the root is a suspense boundary
|
||||
let pending_suspense_boundaries_stack = RwLock::new(vec![]);
|
||||
renderer.set_render_components(move |renderer, to, vdom, scope| {
|
||||
let is_suspense_boundary =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||
@@ -191,10 +204,47 @@ impl SsrRendererPool {
|
||||
.is_some();
|
||||
if is_suspense_boundary {
|
||||
let mount = stream.render_placeholder(
|
||||
|to| renderer.render_scope(to, vdom, scope),
|
||||
|to| {
|
||||
{
|
||||
pending_suspense_boundaries_stack
|
||||
.write()
|
||||
.unwrap()
|
||||
.push(scope);
|
||||
}
|
||||
let out = renderer.render_scope(to, vdom, scope);
|
||||
{
|
||||
pending_suspense_boundaries_stack.write().unwrap().pop();
|
||||
}
|
||||
out
|
||||
},
|
||||
&mut *to,
|
||||
)?;
|
||||
scope_to_mount_mapping.write().unwrap().insert(scope, mount);
|
||||
// Add the suspense boundary to the list of pending suspense boundaries
|
||||
// We will replace the mount with the resolved contents later once the suspense boundary is resolved
|
||||
let mut scope_to_mount_mapping_write =
|
||||
scope_to_mount_mapping.write().unwrap();
|
||||
scope_to_mount_mapping_write.insert(
|
||||
scope,
|
||||
PendingSuspenseBoundary {
|
||||
mount,
|
||||
children: vec![],
|
||||
},
|
||||
);
|
||||
// Add the scope to the list of children of the parent suspense boundary
|
||||
let pending_suspense_boundaries_stack =
|
||||
pending_suspense_boundaries_stack.read().unwrap();
|
||||
// If there is a parent suspense boundary, add the scope to the list of children
|
||||
// This suspense boundary will start capturing errors when the parent is resolved
|
||||
if let Some(parent) = pending_suspense_boundaries_stack.last() {
|
||||
let parent = scope_to_mount_mapping_write.get_mut(parent).unwrap();
|
||||
parent.children.push(scope);
|
||||
}
|
||||
// Otherwise this is a root suspense boundary, so we need to start capturing errors immediately
|
||||
else {
|
||||
vdom.in_runtime(|| {
|
||||
start_capturing_errors(scope);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
renderer.render_scope(to, vdom, scope)?
|
||||
}
|
||||
@@ -233,12 +283,12 @@ impl SsrRendererPool {
|
||||
|
||||
// Just rerender the resolved nodes
|
||||
for scope in resolved_suspense_nodes {
|
||||
let mount = {
|
||||
let pending_suspense_boundary = {
|
||||
let mut lock = scope_to_mount_mapping.write().unwrap();
|
||||
lock.remove(&scope)
|
||||
};
|
||||
// If the suspense boundary was immediately removed, it may not have a mount. We can just skip resolving it
|
||||
if let Some(mount) = mount {
|
||||
if let Some(pending_suspense_boundary) = pending_suspense_boundary {
|
||||
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| {
|
||||
@@ -247,7 +297,7 @@ impl SsrRendererPool {
|
||||
};
|
||||
let resolved_data = serialize_server_data(&virtual_dom, scope);
|
||||
if let Err(err) = stream.replace_placeholder(
|
||||
mount,
|
||||
pending_suspense_boundary.mount,
|
||||
render_suspense,
|
||||
resolved_data,
|
||||
&mut resolved_chunk,
|
||||
@@ -258,13 +308,22 @@ impl SsrRendererPool {
|
||||
}
|
||||
|
||||
stream.render(resolved_chunk);
|
||||
}
|
||||
// Freeze the suspense boundary to prevent future reruns of any child nodes of the suspense boundary
|
||||
if let Some(suspense) = SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||
&virtual_dom.runtime(),
|
||||
scope,
|
||||
) {
|
||||
suspense.freeze();
|
||||
// Freeze the suspense boundary to prevent future reruns of any child nodes of the suspense boundary
|
||||
if let Some(suspense) =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||
&virtual_dom.runtime(),
|
||||
scope,
|
||||
)
|
||||
{
|
||||
suspense.freeze();
|
||||
// Go to every child suspense boundary and add an error boundary. Since we cannot rerun any nodes above the child suspense boundary,
|
||||
// we need to capture the errors and send them to the client as it resolves
|
||||
virtual_dom.in_runtime(|| {
|
||||
for &suspense_scope in pending_suspense_boundary.children.iter() {
|
||||
start_capturing_errors(suspense_scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,6 +364,13 @@ impl SsrRendererPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Start capturing errors at a suspense boundary. If the parent suspense boundary is frozen, we need to capture the errors in the suspense boundary
|
||||
/// and send them to the client to continue bubbling up
|
||||
fn start_capturing_errors(suspense_scope: ScopeId) {
|
||||
// Add an error boundary to the scope
|
||||
suspense_scope.in_runtime(provide_error_boundary);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user