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:
1 parent
2db758ad7b
commit
29a7325bb7
6 files changed
+149
-64
No files matched your search
@@ -17,7 +17,11 @@ fn main() {
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
dioxus::launch(|| rsx! { Router::<Route> {} });
|
LaunchBuilder::new()
|
||||||
|
.with_cfg(server_only! {
|
||||||
|
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
|
||||||
|
})
|
||||||
|
.launch(|| rsx! { Router::<Route> {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Routable)]
|
#[derive(Clone, Routable)]
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ use crate::document::ServerDocument;
|
|||||||
use crate::streaming::{Mount, StreamingRenderer};
|
use crate::streaming::{Mount, StreamingRenderer};
|
||||||
use dioxus_cli_config::base_path;
|
use dioxus_cli_config::base_path;
|
||||||
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
|
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_lib::document::Document;
|
||||||
use dioxus_ssr::Renderer;
|
use dioxus_ssr::Renderer;
|
||||||
use futures_channel::mpsc::Sender;
|
use futures_channel::mpsc::Sender;
|
||||||
use futures_util::{Stream, StreamExt};
|
use futures_util::{Stream, StreamExt};
|
||||||
|
use std::fmt::Write;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::{collections::HashMap, future::Future};
|
use std::{collections::HashMap, future::Future};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::{prelude::*, StreamingMode};
|
||||||
use dioxus_lib::prelude::*;
|
use dioxus_lib::prelude::*;
|
||||||
|
|
||||||
/// A suspense boundary that is pending with a placeholder in the client
|
/// A suspense boundary that is pending with a placeholder in the client
|
||||||
@@ -165,6 +166,7 @@ impl SsrRendererPool {
|
|||||||
.unwrap_or_else(pre_renderer);
|
.unwrap_or_else(pre_renderer);
|
||||||
|
|
||||||
let myself = self.clone();
|
let myself = self.clone();
|
||||||
|
let streaming_mode = cfg.streaming_mode;
|
||||||
|
|
||||||
let join_handle = spawn_platform(move || async move {
|
let join_handle = spawn_platform(move || async move {
|
||||||
let mut virtual_dom = virtual_dom_factory();
|
let mut virtual_dom = virtual_dom_factory();
|
||||||
@@ -202,65 +204,10 @@ impl SsrRendererPool {
|
|||||||
{
|
{
|
||||||
let scope_to_mount_mapping = scope_to_mount_mapping.clone();
|
let scope_to_mount_mapping = scope_to_mount_mapping.clone();
|
||||||
let stream = stream.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
|
renderer.set_render_components(streaming_render_component_callback(
|
||||||
// The stack starts with the root scope because the root is a suspense boundary
|
stream,
|
||||||
let pending_suspense_boundaries_stack = RwLock::new(vec![]);
|
scope_to_mount_mapping,
|
||||||
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(())
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! throw_error {
|
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
|
// Render the initial frame with loading placeholders
|
||||||
let mut initial_frame = renderer.render(&virtual_dom);
|
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
|
/// 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
|
/// and send them to the client to continue bubbling up
|
||||||
fn start_capturing_errors(suspense_scope: ScopeId) {
|
fn start_capturing_errors(suspense_scope: ScopeId) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub struct ServeConfigBuilder {
|
|||||||
pub(crate) index_path: Option<PathBuf>,
|
pub(crate) index_path: Option<PathBuf>,
|
||||||
pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
|
pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
|
||||||
pub(crate) context_providers: ContextProviders,
|
pub(crate) context_providers: ContextProviders,
|
||||||
|
pub(crate) streaming_mode: StreamingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LaunchConfig for ServeConfigBuilder {}
|
impl LaunchConfig for ServeConfigBuilder {}
|
||||||
@@ -30,6 +31,7 @@ impl ServeConfigBuilder {
|
|||||||
index_path: None,
|
index_path: None,
|
||||||
incremental: None,
|
incremental: None,
|
||||||
context_providers: Default::default(),
|
context_providers: Default::default(),
|
||||||
|
streaming_mode: StreamingMode::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +143,40 @@ impl ServeConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the streaming mode for the server. By default, streaming is disabled.
|
||||||
|
///
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// # fn app() -> Element { todo!() }
|
||||||
|
/// dioxus::LaunchBuilder::new()
|
||||||
|
/// .with_context(server_only! {
|
||||||
|
/// dioxus::fullstack::ServeConfig::builder().streaming_mode(dioxus::fullstack::StreamingMode::OutOfOrder)
|
||||||
|
/// })
|
||||||
|
/// .launch(app);
|
||||||
|
/// ```
|
||||||
|
pub fn streaming_mode(mut self, mode: StreamingMode) -> Self {
|
||||||
|
self.streaming_mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable out of order streaming. This will cause server futures to be resolved out of order and streamed to the client as they resolve.
|
||||||
|
///
|
||||||
|
/// It is equivalent to calling `streaming_mode(StreamingMode::OutOfOrder)`
|
||||||
|
///
|
||||||
|
/// /// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// # fn app() -> Element { todo!() }
|
||||||
|
/// dioxus::LaunchBuilder::new()
|
||||||
|
/// .with_context(server_only! {
|
||||||
|
/// dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
|
||||||
|
/// })
|
||||||
|
/// .launch(app);
|
||||||
|
/// ```
|
||||||
|
pub fn enable_out_of_order_streaming(mut self) -> Self {
|
||||||
|
self.streaming_mode = StreamingMode::OutOfOrder;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the ServeConfig. This may fail if the index.html file is not found.
|
/// Build the ServeConfig. This may fail if the index.html file is not found.
|
||||||
pub fn build(self) -> Result<ServeConfig, UnableToLoadIndex> {
|
pub fn build(self) -> Result<ServeConfig, UnableToLoadIndex> {
|
||||||
// The CLI always bundles static assets into the exe/public directory
|
// The CLI always bundles static assets into the exe/public directory
|
||||||
@@ -164,6 +200,7 @@ impl ServeConfigBuilder {
|
|||||||
index,
|
index,
|
||||||
incremental: self.incremental,
|
incremental: self.incremental,
|
||||||
context_providers: self.context_providers,
|
context_providers: self.context_providers,
|
||||||
|
streaming_mode: self.streaming_mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,6 +299,17 @@ pub(crate) struct IndexHtml {
|
|||||||
pub(crate) after_closing_body_tag: String,
|
pub(crate) after_closing_body_tag: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The streaming mode to use while rendering the page
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq)]
|
||||||
|
pub enum StreamingMode {
|
||||||
|
/// Streaming is disabled; all server futures should be resolved before hydrating the page on the client
|
||||||
|
#[default]
|
||||||
|
Disabled,
|
||||||
|
/// Out of order streaming is enabled; server futures are resolved out of order and streamed to the client
|
||||||
|
/// as they resolve
|
||||||
|
OutOfOrder,
|
||||||
|
}
|
||||||
|
|
||||||
/// 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`].
|
/// 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`].
|
||||||
/// See [`ServeConfigBuilder`] to create a ServeConfig
|
/// See [`ServeConfigBuilder`] to create a ServeConfig
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -269,6 +317,7 @@ pub struct ServeConfig {
|
|||||||
pub(crate) index: IndexHtml,
|
pub(crate) index: IndexHtml,
|
||||||
pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
|
pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
|
||||||
pub(crate) context_providers: ContextProviders,
|
pub(crate) context_providers: ContextProviders,
|
||||||
|
pub(crate) streaming_mode: StreamingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LaunchConfig for ServeConfig {}
|
impl LaunchConfig for ServeConfig {}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ use dioxus::{prelude::*, CapturedError};
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::LaunchBuilder::new()
|
dioxus::LaunchBuilder::new()
|
||||||
|
.with_cfg(server_only! {
|
||||||
|
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
|
||||||
|
})
|
||||||
.with_context(1234u32)
|
.with_context(1234u32)
|
||||||
.launch(app);
|
.launch(app);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ use dioxus::prelude::*;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(app);
|
LaunchBuilder::new()
|
||||||
|
.with_cfg(server_only! {
|
||||||
|
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
|
||||||
|
})
|
||||||
|
.launch(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
|
|||||||
@@ -107,5 +107,9 @@ fn NestedSuspendedComponent(id: i32) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(app);
|
LaunchBuilder::new()
|
||||||
|
.with_cfg(server_only! {
|
||||||
|
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
|
||||||
|
})
|
||||||
|
.launch(app);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user