1use super::*;
23/// The mode in which SSR is being run.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum SsrMode {
6/// Synchronous mode.
7 ///
8 /// When a suspense boundary is hit, only the fallback is rendered.
9Sync,
10/// Blocking mode.
11 ///
12 /// When a suspense boundary is hit, rendering is paused until the suspense is resolved.
13Blocking,
14/// Streaming mode.
15 ///
16 /// When a suspense boundary is hit, the fallback is rendered. Once the suspense is resolved,
17 /// the rendered HTML is streamed to the client.
18Streaming,
19}
2021/// Render a [`View`] into a static [`String`]. Useful for rendering to a string on the server side.
22#[must_use]
23pub fn render_to_string(view: impl FnOnce() -> View) -> String {
24is_not_ssr! {
25let _ = view;
26panic!("`render_to_string` only available in SSR mode");
27 }
28is_ssr! {
29use std::cell::LazyCell;
3031thread_local! {
32/// Use a static variable here so that we can reuse the same root for multiple calls to
33 /// this function.
34static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
35 }
36 SSR_ROOT.with(|root| {
37 root.dispose();
38 root.run_in(|| {
39 render_to_string_in_scope(view)
40 })
41 })
42 }
43}
4445/// Render a [`View`] into a static [`String`] in the current reactive scope.
46///
47/// Implementation detail of [`render_to_string`].
48#[must_use]
49pub fn render_to_string_in_scope(view: impl FnOnce() -> View) -> String {
50is_not_ssr! {
51let _ = view;
52panic!("`render_to_string` only available in SSR mode");
53 }
54is_ssr! {
55let mut buf = String::new();
5657let handle = create_child_scope(|| {
58 provide_context(HydrationRegistry::new());
59 provide_context(SsrMode::Sync);
6061let prev = IS_HYDRATING.replace(true);
62let view = view();
63 IS_HYDRATING.set(prev);
64 ssr_node::render_recursive_view(&view, &mut buf);
65 });
66 handle.dispose();
67 buf
68 }
69}
7071/// Renders a [`View`] into a static [`String`] while awaiting for all suspense boundaries to
72/// resolve. Useful for rendering to a string on the server side.
73///
74/// This sets the SSR mode to "blocking" mode. This means that rendering will wait until suspense
75/// is resolved before returning.
76///
77/// # Example
78/// ```
79/// # use sycamore::prelude::*;
80/// # use sycamore::web::render_to_string_await_suspense;
81/// #[component]
82/// async fn AsyncComponent() -> View {
83/// // Do some async operations.
84/// # view! {}
85/// }
86///
87/// # tokio_test::block_on(async move {
88/// let ssr = render_to_string_await_suspense(AsyncComponent).await;
89/// # })
90/// ```
91#[must_use]
92#[cfg(feature = "suspense")]
93pub async fn render_to_string_await_suspense(view: impl FnOnce() -> View) -> String {
94is_not_ssr! {
95let _ = view;
96panic!("`render_to_string` only available in SSR mode");
97 }
98is_ssr! {
99use std::num::NonZeroU32;
100use std::cell::LazyCell;
101use std::fmt::Write;
102use std::collections::HashMap;
103104use futures::StreamExt;
105106const BUFFER_SIZE: usize = 5;
107108thread_local! {
109/// Use a static variable here so that we can reuse the same root for multiple calls to
110 /// this function.
111static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
112 }
113 IS_HYDRATING.set(true);
114 sycamore_futures::provide_executor_scope(async {
115let mut buf = String::new();
116117let (sender, mut receiver) = futures::channel::mpsc::channel(BUFFER_SIZE);
118 SSR_ROOT.with(|root| {
119 root.dispose();
120 root.run_in(|| {
121// We run this in a new scope so that we can dispose everything after we render it.
122provide_context(HydrationRegistry::new());
123 provide_context(SsrMode::Blocking);
124let suspense_state = SuspenseState { sender };
125126 provide_context(suspense_state);
127128let view = view();
129 ssr_node::render_recursive_view(&view, &mut buf);
130 });
131 });
132133// Split at suspense fragment locations.
134let split = buf.split("<!--sycamore-suspense-").collect::<Vec<_>>();
135// Calculate the number of suspense fragments.
136let n = split.len() - 1;
137138// Now we wait until all suspense fragments are resolved.
139let mut fragment_map = HashMap::new();
140if n == 0 {
141 receiver.close();
142 }
143let mut i = 0;
144while let Some(fragment) = receiver.next().await {
145 fragment_map.insert(fragment.key, fragment.view);
146 i += 1;
147if i == n {
148// We have received all suspense fragments so we shouldn't need the receiver anymore.
149receiver.close();
150 }
151 }
152 IS_HYDRATING.set(false);
153154// Finally, replace all suspense marker nodes with rendered values.
155if let [first, rest @ ..] = split.as_slice() {
156 rest.iter().fold(first.to_string(), |mut acc, s| {
157// Try to parse the key.
158let (num, rest) = s.split_once("-->").expect("end of suspense marker not found");
159let key: u32 = num.parse().expect("could not parse suspense key");
160let key = NonZeroU32::try_from(key).expect("suspense key cannot be 0");
161let fragment = fragment_map.get(&key).expect("fragment not found");
162 ssr_node::render_recursive_view(fragment, &mut acc);
163164write!(&mut acc, "{rest}").unwrap();
165 acc
166 })
167 } else {
168unreachable!("split should always have at least one element")
169 }
170 }).await
171}
172}
173174/// Renders a [`View`] to a stream.
175///
176/// This sets the SSR mode to "streaming" mode. This means that the initial HTML with fallbacks is
177/// sent first and then the suspense fragments are streamed as they are resolved.
178///
179/// The streamed suspense fragments are in the form of HTML template elements and a small script
180/// that moves the template elements into the right area of the DOM.
181///
182/// # Executor
183///
184/// This function (unlike [`render_to_string_await_suspense`]) does not automatically create an
185/// executor. You must provide the executor yourself by using `tokio::task::LocalSet`.
186///
187/// # Example
188/// ```
189/// # use sycamore::prelude::*;
190/// # use sycamore::web::{render_to_string_stream, Suspense};
191/// # use futures::StreamExt;
192/// #[component]
193/// async fn AsyncComponent() -> View {
194/// // Do some async operations.
195/// # view! {}
196/// }
197///
198/// #[component]
199/// fn App() -> View {
200/// view! {
201/// Suspense(fallback=|| "Loading...".into()) {
202/// AsyncComponent {}
203/// }
204/// }
205/// }
206///
207/// # tokio_test::block_on(async move {
208/// // Create a channel for sending the created stream from the local set.
209/// let (tx, rx) = tokio::sync::oneshot::channel();
210/// tokio::task::spawn_blocking(|| {
211/// let handle = tokio::runtime::Handle::current();
212/// handle.block_on(async move {
213/// let local = tokio::task::LocalSet::new();
214/// local.run_until(async move {
215/// let stream = render_to_string_stream(App);
216/// tx.send(stream).ok().unwrap();
217/// }).await;
218/// // Run the remaining tasks in the local set.
219/// local.await;
220/// });
221/// });
222/// let stream = rx.await.unwrap();
223/// tokio::pin!(stream);
224/// while let Some(string) = stream.next().await {
225/// // Send the string to the client.
226/// // Usually, the web framework already supports converting a stream into a response.
227/// }
228/// # })
229/// ```
230#[cfg(feature = "suspense")]
231pub fn render_to_string_stream(
232 view: impl FnOnce() -> View,
233) -> impl futures::Stream<Item = String> + Send {
234is_not_ssr! {
235let _ = view;
236panic!("`render_to_string` only available in SSR mode");
237#[allow(unreachable_code)] // TODO: never type cannot be coerced into `impl Stream` somehow.
238futures::stream::empty()
239 }
240is_ssr! {
241use std::cell::LazyCell;
242243use futures::StreamExt;
244245const BUFFER_SIZE: usize = 5;
246247thread_local! {
248/// Use a static variable here so that we can reuse the same root for multiple calls to
249 /// this function.
250static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
251 }
252 IS_HYDRATING.set(true);
253let mut buf = String::new();
254let (sender, mut receiver) = futures::channel::mpsc::channel(BUFFER_SIZE);
255 SSR_ROOT.with(|root| {
256 root.dispose();
257 root.run_in(|| {
258// We run this in a new scope so that we can dispose everything after we render it.
259provide_context(HydrationRegistry::new());
260 provide_context(SsrMode::Streaming);
261let suspense_state = SuspenseState { sender };
262263 provide_context(suspense_state);
264265let view = view();
266 ssr_node::render_recursive_view(&view, &mut buf);
267 });
268 });
269270// Calculate the number of suspense fragments.
271let mut n = buf.matches("<!--sycamore-suspense-").count();
272273// ```js
274 // function __sycamore_suspense(key) {
275 // let start = document.querySelector(`suspense-start[data-key="${key}"]`)
276 // let end = document.querySelector(`suspense-end[data-key="${key}"]`)
277 // let template = document.getElementById(`sycamore-suspense-${key}`)
278 // start.parentNode.insertBefore(template.content, start)
279 // while (start.nextSibling != end) {
280 // start.parentNode.removeChild(start.nextSibling)
281 // }
282 // }
283 // ```
284static SUSPENSE_REPLACE_SCRIPT: &str = r#"<script>function __sycamore_suspense(e){let s=document.querySelector(`suspense-start[data-key="${e}"]`),n=document.querySelector(`suspense-end[data-key="${e}"]`),r=document.getElementById(`sycamore-suspense-${e}`);for(s.parentNode.insertBefore(r.content,s);s.nextSibling!=n;)s.parentNode.removeChild(s.nextSibling);}</script>"#;
285async_stream::stream! {
286let mut initial = String::new();
287 initial.push_str("<!doctype html>");
288 initial.push_str(&buf);
289 initial.push_str(SUSPENSE_REPLACE_SCRIPT);
290yield initial;
291292if n == 0 {
293 receiver.close();
294 }
295let mut i = 0;
296while let Some(fragment) = receiver.next().await {
297let buf_fragment = render_suspense_fragment(fragment);
298// Check if we have any nested suspense.
299let n_add = buf_fragment.matches("<!--sycamore-suspense-").count();
300 n += n_add;
301302yield buf_fragment;
303304 i += 1;
305if i == n {
306// We have received all suspense fragments so we shouldn't need the receiver anymore.
307receiver.close();
308 }
309 }
310 }
311 }
312}
313314#[cfg_ssr]
315#[cfg(feature = "suspense")]
316fn render_suspense_fragment(SuspenseFragment { key, view }: SuspenseFragment) -> String {
317use std::fmt::Write;
318319let mut buf = String::new();
320write!(&mut buf, "<template id=\"sycamore-suspense-{key}\">",).unwrap();
321 ssr_node::render_recursive_view(&view, &mut buf);
322write!(
323&mut buf,
324"</template><script>__sycamore_suspense({key})</script>"
325)
326 .unwrap();
327328 buf
329}
330331#[cfg(test)]
332#[cfg(feature = "suspense")]
333#[cfg_ssr]
334mod tests {
335use expect_test::expect;
336use futures::channel::oneshot;
337338use super::*;
339340#[component(inline_props)]
341async fn AsyncComponent(receiver: oneshot::Receiver<()>) -> View {
342 receiver.await.unwrap();
343view! {
344"Hello, async!"
345}
346 }
347348#[component(inline_props)]
349fn App(receiver: oneshot::Receiver<()>) -> View {
350view! {
351 Suspense(fallback=|| "fallback".into()) {
352 AsyncComponent(receiver=receiver)
353 }
354 }
355 }
356357#[test]
358fn render_to_string_renders_fallback() {
359let (sender, receiver) = oneshot::channel();
360let res = render_to_string(move || view! { App(receiver=receiver) });
361assert_eq!(
362 res,
363"<!--/--><!--/-->fallback<!--/--><!--/--><!--/--><!--/-->"
364);
365assert!(sender.send(()).is_err(), "receiver should be dropped");
366 }
367368#[tokio::test]
369async fn render_to_string_await_suspense_works() {
370let (sender, receiver) = oneshot::channel();
371let ssr = render_to_string_await_suspense(move || view! { App(receiver=receiver) });
372futures::pin_mut!(ssr);
373assert!(futures::poll!(&mut ssr).is_pending());
374375 sender.send(()).unwrap();
376let res = ssr.await;
377378let expect = expect![[
379r#"<suspense-start data-key="1" data-hk="0.0"></suspense-start><!--/--><!--/--><!--/-->Hello, async!<!--/--><!--/--><!--/--><suspense-end data-key="1"></suspense-end>"#
380]];
381 expect.assert_eq(&res);
382 }
383}