1use crate::document::ServerDocument;
3use crate::html_storage::serialize::SerializedHydrationData;
4use crate::streaming::{Mount, StreamingRenderer};
5use dioxus_cli_config::base_path;
6use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
7use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness};
8use dioxus_lib::document::Document;
9use dioxus_ssr::Renderer;
10use futures_channel::mpsc::Sender;
11use futures_util::{Stream, StreamExt};
12use std::fmt::Write;
13use std::rc::Rc;
14use std::sync::Arc;
15use std::sync::RwLock;
16use std::{collections::HashMap, future::Future};
17use tokio::task::JoinHandle;
18
19use crate::{prelude::*, StreamingMode};
20use dioxus_lib::prelude::*;
21
22struct PendingSuspenseBoundary {
24 mount: Mount,
25 children: Vec<ScopeId>,
26}
27
28fn spawn_platform<Fut>(f: impl FnOnce() -> Fut + Send + 'static) -> JoinHandle<Fut::Output>
30where
31 Fut: Future + 'static,
32 Fut::Output: Send + 'static,
33{
34 #[cfg(not(target_arch = "wasm32"))]
35 {
36 use tokio_util::task::LocalPoolHandle;
37 static TASK_POOL: std::sync::OnceLock<LocalPoolHandle> = std::sync::OnceLock::new();
38
39 let pool = TASK_POOL.get_or_init(|| {
40 let threads = std::thread::available_parallelism()
41 .unwrap_or(std::num::NonZeroUsize::new(1).unwrap());
42 LocalPoolHandle::new(threads.into())
43 });
44
45 pool.spawn_pinned(f)
46 }
47 #[cfg(target_arch = "wasm32")]
48 {
49 tokio::task::spawn_local(f())
50 }
51}
52
53struct SsrRendererPool {
54 renderers: RwLock<Vec<Renderer>>,
55 incremental_cache: Option<RwLock<dioxus_isrg::IncrementalRenderer>>,
56}
57
58impl SsrRendererPool {
59 fn new(
60 initial_size: usize,
61 incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
62 ) -> Self {
63 let renderers = RwLock::new((0..initial_size).map(|_| pre_renderer()).collect());
64 Self {
65 renderers,
66 incremental_cache: incremental.map(|cache| RwLock::new(cache.build())),
67 }
68 }
69
70 fn check_cached_route(
72 &self,
73 route: &str,
74 render_into: &mut Sender<Result<String, dioxus_isrg::IncrementalRendererError>>,
75 ) -> Option<RenderFreshness> {
76 if let Some(incremental) = &self.incremental_cache {
77 if let Ok(mut incremental) = incremental.write() {
78 match incremental.get(route) {
79 Ok(Some(cached_render)) => {
80 let CachedRender {
81 freshness,
82 response,
83 ..
84 } = cached_render;
85 _ = render_into.start_send(String::from_utf8(response.to_vec()).map_err(
86 |err| dioxus_isrg::IncrementalRendererError::Other(Box::new(err)),
87 ));
88 return Some(freshness);
89 }
90 Err(e) => {
91 tracing::error!(
92 "Failed to get route \"{route}\" from incremental cache: {e}"
93 );
94 }
95 _ => {}
96 }
97 }
98 }
99 None
100 }
101
102 async fn render_to(
105 self: Arc<Self>,
106 cfg: &ServeConfig,
107 route: String,
108 virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
109 server_context: &DioxusServerContext,
110 ) -> Result<
111 (
112 RenderFreshness,
113 impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
114 ),
115 dioxus_isrg::IncrementalRendererError,
116 > {
117 struct ReceiverWithDrop {
118 receiver: futures_channel::mpsc::Receiver<
119 Result<String, dioxus_isrg::IncrementalRendererError>,
120 >,
121 cancel_task: Option<tokio::task::JoinHandle<()>>,
122 }
123
124 impl Stream for ReceiverWithDrop {
125 type Item = Result<String, dioxus_isrg::IncrementalRendererError>;
126
127 fn poll_next(
128 mut self: std::pin::Pin<&mut Self>,
129 cx: &mut std::task::Context<'_>,
130 ) -> std::task::Poll<Option<Self::Item>> {
131 self.receiver.poll_next_unpin(cx)
132 }
133 }
134
135 impl Drop for ReceiverWithDrop {
137 fn drop(&mut self) {
138 if let Some(cancel_task) = self.cancel_task.take() {
139 cancel_task.abort();
140 }
141 }
142 }
143
144 let (mut into, rx) = futures_channel::mpsc::channel::<
145 Result<String, dioxus_isrg::IncrementalRendererError>,
146 >(1000);
147
148 if let Some(freshness) = self.check_cached_route(&route, &mut into) {
150 return Ok((
151 freshness,
152 ReceiverWithDrop {
153 receiver: rx,
154 cancel_task: None,
155 },
156 ));
157 }
158
159 let wrapper = FullstackHTMLTemplate { cfg: cfg.clone() };
160
161 let server_context = server_context.clone();
162 let mut renderer = self
163 .renderers
164 .write()
165 .unwrap()
166 .pop()
167 .unwrap_or_else(pre_renderer);
168
169 let myself = self.clone();
170 let streaming_mode = cfg.streaming_mode;
171
172 let join_handle = spawn_platform(move || async move {
173 let mut virtual_dom = virtual_dom_factory();
174 let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
175 virtual_dom.provide_root_context(document.clone());
176 let history;
179 if let Some(base_path) = base_path() {
180 let base_path = base_path.trim_matches('/');
181 let base_path = format!("/{base_path}");
182 let route = route.strip_prefix(&base_path).unwrap_or(&route);
183 history =
184 dioxus_history::MemoryHistory::with_initial_path(route).with_prefix(base_path);
185 } else {
186 history = dioxus_history::MemoryHistory::with_initial_path(&route);
187 }
188 virtual_dom.provide_root_context(Rc::new(history) as Rc<dyn dioxus_history::History>);
189 virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
190
191 with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());
193
194 let mut pre_body = String::new();
195
196 if let Err(err) = wrapper.render_head(&mut pre_body, &virtual_dom) {
197 _ = into.start_send(Err(err));
198 return;
199 }
200
201 let stream = Arc::new(StreamingRenderer::new(pre_body, into));
202 let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new()));
203
204 renderer.pre_render = true;
205 {
206 let scope_to_mount_mapping = scope_to_mount_mapping.clone();
207 let stream = stream.clone();
208 renderer.set_render_components(streaming_render_component_callback(
209 stream,
210 scope_to_mount_mapping,
211 ));
212 }
213
214 macro_rules! throw_error {
215 ($e:expr) => {
216 stream.close_with_error($e);
217 return;
218 };
219 }
220
221 if streaming_mode == StreamingMode::Disabled {
224 ProvideServerContext::new(virtual_dom.wait_for_suspense(), server_context.clone())
225 .await
226 }
227
228 let mut initial_frame = renderer.render(&virtual_dom);
230
231 if let Err(err) = wrapper.render_after_main(&mut initial_frame, &virtual_dom) {
233 throw_error!(err);
234 }
235 stream.render(initial_frame);
236
237 while virtual_dom.suspended_tasks_remaining() {
239 ProvideServerContext::new(
240 virtual_dom.wait_for_suspense_work(),
241 server_context.clone(),
242 )
243 .await;
244 let resolved_suspense_nodes = ProvideServerContext::new(
245 virtual_dom.render_suspense_immediate(),
246 server_context.clone(),
247 )
248 .await;
249
250 for scope in resolved_suspense_nodes {
252 let pending_suspense_boundary = {
253 let mut lock = scope_to_mount_mapping.write().unwrap();
254 lock.remove(&scope)
255 };
256 if let Some(pending_suspense_boundary) = pending_suspense_boundary {
258 let mut resolved_chunk = String::new();
259 let render_suspense = |into: &mut String| {
261 renderer.reset_hydration();
262 renderer.render_scope(into, &virtual_dom, scope)
263 };
264 let resolved_data = serialize_server_data(&virtual_dom, scope);
265 if let Err(err) = stream.replace_placeholder(
266 pending_suspense_boundary.mount,
267 render_suspense,
268 resolved_data,
269 &mut resolved_chunk,
270 ) {
271 throw_error!(dioxus_isrg::IncrementalRendererError::RenderError(err));
272 }
273
274 stream.render(resolved_chunk);
275 if let Some(suspense) =
277 SuspenseContext::downcast_suspense_boundary_from_scope(
278 &virtual_dom.runtime(),
279 scope,
280 )
281 {
282 suspense.freeze();
283 virtual_dom.in_runtime(|| {
286 for &suspense_scope in pending_suspense_boundary.children.iter() {
287 start_capturing_errors(suspense_scope);
288 }
289 });
290 }
291 }
292 }
293 }
294
295 let mut post_streaming = String::new();
297
298 if let Err(err) = wrapper.render_after_body(&mut post_streaming) {
299 throw_error!(err);
300 }
301
302 if let Some(incremental) = &self.incremental_cache {
304 let mut cached_render = String::new();
305 if let Err(err) = wrapper.render_head(&mut cached_render, &virtual_dom) {
306 throw_error!(err);
307 }
308 renderer.reset_hydration();
309 if let Err(err) = renderer.render_to(&mut cached_render, &virtual_dom) {
310 throw_error!(dioxus_isrg::IncrementalRendererError::RenderError(err));
311 }
312 if let Err(err) = wrapper.render_after_main(&mut cached_render, &virtual_dom) {
313 throw_error!(err);
314 }
315 cached_render.push_str(&post_streaming);
316
317 if let Ok(mut incremental) = incremental.write() {
318 let _ = incremental.cache(route, cached_render);
319 }
320 }
321
322 stream.render(post_streaming);
323
324 renderer.reset_render_components();
325 myself.renderers.write().unwrap().push(renderer);
326 });
327
328 Ok((
329 RenderFreshness::now(None),
330 ReceiverWithDrop {
331 receiver: rx,
332 cancel_task: Some(join_handle),
333 },
334 ))
335 }
336}
337
338fn streaming_render_component_callback(
343 stream: Arc<StreamingRenderer<IncrementalRendererError>>,
344 scope_to_mount_mapping: Arc<RwLock<HashMap<ScopeId, PendingSuspenseBoundary>>>,
345) -> impl Fn(&mut Renderer, &mut dyn Write, &VirtualDom, ScopeId) -> std::fmt::Result
346 + Send
347 + Sync
348 + 'static {
349 let pending_suspense_boundaries_stack = RwLock::new(vec![]);
352 move |renderer, to, vdom, scope| {
353 let is_suspense_boundary =
354 SuspenseContext::downcast_suspense_boundary_from_scope(&vdom.runtime(), scope)
355 .filter(|s| s.has_suspended_tasks())
356 .is_some();
357 if is_suspense_boundary {
358 let mount = stream.render_placeholder(
359 |to| {
360 {
361 pending_suspense_boundaries_stack
362 .write()
363 .unwrap()
364 .push(scope);
365 }
366 let out = renderer.render_scope(to, vdom, scope);
367 {
368 pending_suspense_boundaries_stack.write().unwrap().pop();
369 }
370 out
371 },
372 &mut *to,
373 )?;
374 let mut scope_to_mount_mapping_write = scope_to_mount_mapping.write().unwrap();
377 scope_to_mount_mapping_write.insert(
378 scope,
379 PendingSuspenseBoundary {
380 mount,
381 children: vec![],
382 },
383 );
384 let pending_suspense_boundaries_stack =
386 pending_suspense_boundaries_stack.read().unwrap();
387 if let Some(parent) = pending_suspense_boundaries_stack.last() {
390 let parent = scope_to_mount_mapping_write.get_mut(parent).unwrap();
391 parent.children.push(scope);
392 }
393 else {
395 vdom.in_runtime(|| {
396 start_capturing_errors(scope);
397 });
398 }
399 } else {
400 renderer.render_scope(to, vdom, scope)?
401 }
402 Ok(())
403 }
404}
405
406fn start_capturing_errors(suspense_scope: ScopeId) {
409 suspense_scope.in_runtime(provide_error_boundary);
411}
412
413fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> SerializedHydrationData {
414 let html_data =
417 crate::html_storage::HTMLData::extract_from_suspense_boundary(virtual_dom, scope);
418
419 html_data.serialized()
421}
422
423#[derive(Clone)]
425pub struct SSRState {
426 renderers: Arc<SsrRendererPool>,
428}
429
430impl SSRState {
431 pub fn new(cfg: &ServeConfig) -> Self {
433 Self {
434 renderers: Arc::new(SsrRendererPool::new(4, cfg.incremental.clone())),
435 }
436 }
437
438 pub async fn render<'a>(
440 &'a self,
441 route: String,
442 cfg: &'a ServeConfig,
443 virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
444 server_context: &'a DioxusServerContext,
445 ) -> Result<
446 (
447 RenderFreshness,
448 impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
449 ),
450 dioxus_isrg::IncrementalRendererError,
451 > {
452 self.renderers
453 .clone()
454 .render_to(cfg, route, virtual_dom_factory, server_context)
455 .await
456 }
457}
458
459pub struct FullstackHTMLTemplate {
461 cfg: ServeConfig,
462}
463
464impl FullstackHTMLTemplate {
465 pub fn new(cfg: &ServeConfig) -> Self {
467 Self { cfg: cfg.clone() }
468 }
469}
470
471impl FullstackHTMLTemplate {
472 pub fn render_head<R: std::fmt::Write>(
474 &self,
475 to: &mut R,
476 virtual_dom: &VirtualDom,
477 ) -> Result<(), dioxus_isrg::IncrementalRendererError> {
478 let ServeConfig { index, .. } = &self.cfg;
479
480 let title = {
481 let document: Option<std::rc::Rc<ServerDocument>> =
482 virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
483 document.and_then(|document| document.title())
485 };
486
487 to.write_str(&index.head_before_title)?;
488 if let Some(title) = title {
489 to.write_str(&title)?;
490 } else {
491 to.write_str(&index.title)?;
492 }
493 to.write_str(&index.head_after_title)?;
494
495 let document: Option<std::rc::Rc<ServerDocument>> =
496 virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
497 if let Some(document) = document {
498 document.render(to)?;
500
501 document.start_streaming();
503 }
504
505 self.render_before_body(to)?;
506
507 Ok(())
508 }
509
510 fn render_before_body<R: std::fmt::Write>(
512 &self,
513 to: &mut R,
514 ) -> Result<(), dioxus_isrg::IncrementalRendererError> {
515 let ServeConfig { index, .. } = &self.cfg;
516
517 to.write_str(&index.close_head)?;
518
519 write!(to, "<script>{INITIALIZE_STREAMING_JS}</script>")?;
520
521 Ok(())
522 }
523
524 pub fn render_after_main<R: std::fmt::Write>(
526 &self,
527 to: &mut R,
528 virtual_dom: &VirtualDom,
529 ) -> Result<(), dioxus_isrg::IncrementalRendererError> {
530 let ServeConfig { index, .. } = &self.cfg;
531
532 let resolved_data = serialize_server_data(virtual_dom, ScopeId::ROOT);
535 let raw_data = resolved_data.data;
537 write!(
538 to,
539 r#"<script>window.initial_dioxus_hydration_data="{raw_data}";"#,
540 )?;
541 #[cfg(debug_assertions)]
542 {
543 let debug_types = &resolved_data.debug_types;
545 let debug_locations = &resolved_data.debug_locations;
546 write!(
547 to,
548 r#"window.initial_dioxus_hydration_debug_types={debug_types};"#,
549 )?;
550 write!(
551 to,
552 r#"window.initial_dioxus_hydration_debug_locations={debug_locations};"#,
553 )?;
554 }
555 write!(to, r#"</script>"#,)?;
556 to.write_str(&index.post_main)?;
557
558 Ok(())
559 }
560
561 pub fn render_after_body<R: std::fmt::Write>(
563 &self,
564 to: &mut R,
565 ) -> Result<(), dioxus_isrg::IncrementalRendererError> {
566 let ServeConfig { index, .. } = &self.cfg;
567
568 to.write_str(&index.after_closing_body_tag)?;
569
570 Ok(())
571 }
572
573 pub fn wrap_body<R: std::fmt::Write>(
575 &self,
576 to: &mut R,
577 virtual_dom: &VirtualDom,
578 body: impl std::fmt::Display,
579 ) -> Result<(), dioxus_isrg::IncrementalRendererError> {
580 self.render_head(to, virtual_dom)?;
581 write!(to, "{body}")?;
582 self.render_after_main(to, virtual_dom)?;
583 self.render_after_body(to)?;
584
585 Ok(())
586 }
587}
588
589fn pre_renderer() -> Renderer {
590 let mut renderer = Renderer::default();
591 renderer.pre_render = true;
592 renderer
593}