Expose streaming options to choose between full prerendering and out of order streaming (#3288)

* expose streaming options to choose between full prerendering and out of order streaming

* add examples to streaming_mode
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-12-04 13:25:08 -08:00
1 parent 2db758ad7b
commit 29a7325bb7
6 files changed
+149 -64

No files matched your search

+82 -61
View File
@@ -3,18 +3,19 @@ use crate::document::ServerDocument;
use crate::streaming::{Mount, StreamingRenderer};
use dioxus_cli_config::base_path;
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
use dioxus_isrg::{CachedRender, RenderFreshness};
use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness};
use dioxus_lib::document::Document;
use dioxus_ssr::Renderer;
use futures_channel::mpsc::Sender;
use futures_util::{Stream, StreamExt};
use std::fmt::Write;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::RwLock;
use std::{collections::HashMap, future::Future};
use tokio::task::JoinHandle;
use crate::prelude::*;
use crate::{prelude::*, StreamingMode};
use dioxus_lib::prelude::*;
/// A suspense boundary that is pending with a placeholder in the client
@@ -165,6 +166,7 @@ impl SsrRendererPool {
.unwrap_or_else(pre_renderer);
let myself = self.clone();
let streaming_mode = cfg.streaming_mode;
let join_handle = spawn_platform(move || async move {
let mut virtual_dom = virtual_dom_factory();
@@ -202,65 +204,10 @@ 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(
&vdom.runtime(),
scope,
)
.filter(|s| s.has_suspended_tasks())
.is_some();
if is_suspense_boundary {
let mount = stream.render_placeholder(
|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,
)?;
// 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)?
}
Ok(())
});
renderer.set_render_components(streaming_render_component_callback(
stream,
scope_to_mount_mapping,
));
}
macro_rules! throw_error {
@@ -270,6 +217,12 @@ impl SsrRendererPool {
};
}
// If streaming is disabled, wait for the virtual dom to finish all suspense work
// before rendering anything
if streaming_mode == StreamingMode::Disabled {
virtual_dom.wait_for_suspense().await;
}
// Render the initial frame with loading placeholders
let mut initial_frame = renderer.render(&virtual_dom);
@@ -380,6 +333,74 @@ impl SsrRendererPool {
}
}
/// Create the streaming render component callback. It will keep track of what scopes are mounted to what pending
/// suspense boundaries in the DOM.
///
/// This mapping is used to replace the DOM mount with the resolved contents once the suspense boundary is finished.
fn streaming_render_component_callback(
stream: Arc<StreamingRenderer<IncrementalRendererError>>,
scope_to_mount_mapping: Arc<RwLock<HashMap<ScopeId, PendingSuspenseBoundary>>>,
) -> impl Fn(&mut Renderer, &mut dyn Write, &VirtualDom, ScopeId) -> std::fmt::Result
+ Send
+ Sync
+ 'static {
// 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![]);
move |renderer, to, vdom, scope| {
let is_suspense_boundary =
SuspenseContext::downcast_suspense_boundary_from_scope(&vdom.runtime(), scope)
.filter(|s| s.has_suspended_tasks())
.is_some();
if is_suspense_boundary {
let mount = stream.render_placeholder(
|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,
)?;
// 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)?
}
Ok(())
}
}
/// 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) {