diff --git a/packages/fullstack/examples/axum-hello-world/Cargo.toml b/packages/fullstack/examples/axum-hello-world/Cargo.toml index 9761864bb..502d75bae 100644 --- a/packages/fullstack/examples/axum-hello-world/Cargo.toml +++ b/packages/fullstack/examples/axum-hello-world/Cargo.toml @@ -16,6 +16,9 @@ serde = "1.0.159" execute = "0.2.12" tower-http = { version = "0.4.1", features = ["auth"] } simple_logger = "4.2.0" +wasm-logger = "0.2.0" +log.workspace = true +reqwest = "0.11.18" [features] default = [] diff --git a/packages/fullstack/examples/axum-hello-world/src/main.rs b/packages/fullstack/examples/axum-hello-world/src/main.rs index e00b9ae05..a5724f8ee 100644 --- a/packages/fullstack/examples/axum-hello-world/src/main.rs +++ b/packages/fullstack/examples/axum-hello-world/src/main.rs @@ -16,10 +16,22 @@ struct AppProps { } fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || cx.props.count); + render! { + Child {} + } +} + +fn Child(cx: Scope) -> Element { + let state = + use_server_future(cx, (), |()| async move { get_server_data().await.unwrap() })?.value(); + + let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { + div { + "Server state: {state}" + } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } @@ -42,7 +54,7 @@ fn app(cx: Scope) -> Element { #[server(PostServerData)] async fn post_server_data(data: String) -> Result<(), ServerFnError> { - let axum::extract::Host(host): axum::extract::Host = extract()?; + let axum::extract::Host(host): axum::extract::Host = extract().await?; println!("Server received: {}", data); println!("{:?}", host); @@ -51,10 +63,15 @@ async fn post_server_data(data: String) -> Result<(), ServerFnError> { #[server(GetServerData)] async fn get_server_data() -> Result { - Ok("Hello from the server!".to_string()) + Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?) } fn main() { + #[cfg(feature = "web")] + wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); + #[cfg(feature = "ssr")] + simple_logger::SimpleLogger::new().init().unwrap(); + launch!(@([127, 0, 0, 1], 8080), app, { serve_cfg: ServeConfigBuilder::new(app, AppProps { count: 0 }), }); diff --git a/packages/fullstack/examples/salvo-hello-world/Cargo.toml b/packages/fullstack/examples/salvo-hello-world/Cargo.toml index b85651f06..977ad7843 100644 --- a/packages/fullstack/examples/salvo-hello-world/Cargo.toml +++ b/packages/fullstack/examples/salvo-hello-world/Cargo.toml @@ -14,6 +14,10 @@ tokio = { workspace = true, features = ["full"], optional = true } serde = "1.0.159" salvo = { version = "0.37.9", optional = true } execute = "0.2.12" +reqwest = "0.11.18" +simple_logger = "4.2.0" +log.workspace = true +wasm-logger = "0.2.0" [features] default = [] diff --git a/packages/fullstack/examples/salvo-hello-world/src/main.rs b/packages/fullstack/examples/salvo-hello-world/src/main.rs index bf6c2872a..09c68e1f4 100644 --- a/packages/fullstack/examples/salvo-hello-world/src/main.rs +++ b/packages/fullstack/examples/salvo-hello-world/src/main.rs @@ -7,25 +7,31 @@ #![allow(non_snake_case, unused)] use dioxus::prelude::*; -use dioxus_fullstack::prelude::*; +use dioxus_fullstack::{launch, prelude::*}; use serde::{Deserialize, Serialize}; -fn main() { - launch!(@([127, 0, 0, 1], 8080), app, (AppProps { count: 5 }), { - incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)), - }); -} - #[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] struct AppProps { count: i32, } fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || cx.props.count); + render! { + Child {} + } +} + +fn Child(cx: Scope) -> Element { + let state = + use_server_future(cx, (), |()| async move { get_server_data().await.unwrap() })?.value(); + + let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { + div { + "Server state: {state}" + } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } @@ -40,7 +46,7 @@ fn app(cx: Scope) -> Element { } } }, - "Run a server function" + "Run a server function!" } "Server said: {text}" }) @@ -48,15 +54,23 @@ fn app(cx: Scope) -> Element { #[server(PostServerData)] async fn post_server_data(data: String) -> Result<(), ServerFnError> { - // The server context contains information about the current request and allows you to modify the response. - let cx = server_context(); println!("Server received: {}", data); - println!("Request parts are {:?}", cx.request_parts()); Ok(()) } #[server(GetServerData)] async fn get_server_data() -> Result { - Ok("Hello from the server!".to_string()) + Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?) +} + +fn main() { + #[cfg(feature = "web")] + wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); + #[cfg(feature = "ssr")] + simple_logger::SimpleLogger::new().init().unwrap(); + + launch!(@([127, 0, 0, 1], 8080), app, { + serve_cfg: ServeConfigBuilder::new(app, AppProps { count: 0 }), + }); } diff --git a/packages/fullstack/examples/warp-hello-world/Cargo.toml b/packages/fullstack/examples/warp-hello-world/Cargo.toml index 3c21372a9..d9e82b4c4 100644 --- a/packages/fullstack/examples/warp-hello-world/Cargo.toml +++ b/packages/fullstack/examples/warp-hello-world/Cargo.toml @@ -14,6 +14,10 @@ tokio = { workspace = true, features = ["full"], optional = true } serde = "1.0.159" warp = { version = "0.3.3", optional = true } execute = "0.2.12" +reqwest = "0.11.18" +simple_logger = "4.2.0" +log.workspace = true +wasm-logger = "0.2.0" [features] default = [] diff --git a/packages/fullstack/examples/warp-hello-world/src/main.rs b/packages/fullstack/examples/warp-hello-world/src/main.rs index bf6c2872a..09c68e1f4 100644 --- a/packages/fullstack/examples/warp-hello-world/src/main.rs +++ b/packages/fullstack/examples/warp-hello-world/src/main.rs @@ -7,25 +7,31 @@ #![allow(non_snake_case, unused)] use dioxus::prelude::*; -use dioxus_fullstack::prelude::*; +use dioxus_fullstack::{launch, prelude::*}; use serde::{Deserialize, Serialize}; -fn main() { - launch!(@([127, 0, 0, 1], 8080), app, (AppProps { count: 5 }), { - incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)), - }); -} - #[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] struct AppProps { count: i32, } fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || cx.props.count); + render! { + Child {} + } +} + +fn Child(cx: Scope) -> Element { + let state = + use_server_future(cx, (), |()| async move { get_server_data().await.unwrap() })?.value(); + + let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { + div { + "Server state: {state}" + } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } @@ -40,7 +46,7 @@ fn app(cx: Scope) -> Element { } } }, - "Run a server function" + "Run a server function!" } "Server said: {text}" }) @@ -48,15 +54,23 @@ fn app(cx: Scope) -> Element { #[server(PostServerData)] async fn post_server_data(data: String) -> Result<(), ServerFnError> { - // The server context contains information about the current request and allows you to modify the response. - let cx = server_context(); println!("Server received: {}", data); - println!("Request parts are {:?}", cx.request_parts()); Ok(()) } #[server(GetServerData)] async fn get_server_data() -> Result { - Ok("Hello from the server!".to_string()) + Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?) +} + +fn main() { + #[cfg(feature = "web")] + wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); + #[cfg(feature = "ssr")] + simple_logger::SimpleLogger::new().init().unwrap(); + + launch!(@([127, 0, 0, 1], 8080), app, { + serve_cfg: ServeConfigBuilder::new(app, AppProps { count: 0 }), + }); } diff --git a/packages/fullstack/src/hooks/mod.rs b/packages/fullstack/src/hooks/mod.rs new file mode 100644 index 000000000..f42ce3f78 --- /dev/null +++ b/packages/fullstack/src/hooks/mod.rs @@ -0,0 +1,2 @@ +pub mod server_cached; +pub mod server_future; diff --git a/packages/fullstack/src/hooks/server_cached.rs b/packages/fullstack/src/hooks/server_cached.rs new file mode 100644 index 000000000..80bb93008 --- /dev/null +++ b/packages/fullstack/src/hooks/server_cached.rs @@ -0,0 +1,34 @@ +use serde::{de::DeserializeOwned, Serialize}; + +/// This allows you to send data from the server to the client. The data is serialized into the HTML on the server and hydrated on the client. +/// +/// When you run this function on the client, you need to be careful to insure the order you run it initially is the same order you run it on the server. +/// +/// If Dioxus fullstack cannot find the data on the client, it will run the closure again to get the data. +/// +/// # Example +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_fullstack::prelude::*; +/// +/// fn app(cx: Scope) -> Element { +/// let state1 = use_state(cx, || from_server(|| { +/// 1234 +/// })); +/// } +/// ``` +pub fn server_cached(server_fn: impl Fn() -> O) -> O { + #[cfg(feature = "ssr")] + { + let data = server_fn(); + let sc = crate::prelude::server_context(); + if let Err(err) = sc.push_html_data(&data) { + log::error!("Failed to push HTML data: {}", err); + } + data + } + #[cfg(not(feature = "ssr"))] + { + crate::html_storage::deserialize::take_server_data().unwrap_or_else(server_fn) + } +} diff --git a/packages/fullstack/src/hooks/server_future.rs b/packages/fullstack/src/hooks/server_future.rs new file mode 100644 index 000000000..9a13df235 --- /dev/null +++ b/packages/fullstack/src/hooks/server_future.rs @@ -0,0 +1,152 @@ +use dioxus::prelude::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::any::Any; +use std::cell::Cell; +use std::cell::Ref; +use std::cell::RefCell; +use std::fmt::Debug; +use std::future::Future; +use std::rc::Rc; +use std::sync::Arc; + +/// A future that resolves to a value. +/// +/// This runs the future only once - though the future may be regenerated +/// through the [`UseServerFuture::restart`] method. +/// +/// This is commonly used for components that cannot be rendered until some +/// asynchronous operation has completed. +/// +/// Whenever the hooks dependencies change, the future will be re-evaluated. +/// If a future is pending when the dependencies change, the previous future +/// will be allowed to continue +/// +/// - dependencies: a tuple of references to values that are PartialEq + Clone +pub fn use_server_future( + cx: &ScopeState, + dependencies: D, + future: impl FnOnce(D::Out) -> F, +) -> Option<&UseServerFuture> +where + T: 'static + Serialize + DeserializeOwned + Debug, + F: Future + 'static, + D: UseFutureDep, +{ + let state = cx.use_hook(move || UseServerFuture { + update: cx.schedule_update(), + needs_regen: Cell::new(true), + value: Default::default(), + task: Cell::new(None), + dependencies: Vec::new(), + }); + + let first_run = { state.value.borrow().as_ref().is_none() && state.task.get().is_none() }; + + #[cfg(not(feature = "ssr"))] + { + if first_run { + match crate::html_storage::deserialize::take_server_data() { + Some(data) => { + log::trace!("Loaded {data:?} from server"); + *state.value.borrow_mut() = Some(Box::new(data)); + state.needs_regen.set(false); + return Some(state); + } + None => { + log::trace!("Failed to load from server... running future"); + } + }; + } + } + + if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { + // We don't need regen anymore + state.needs_regen.set(false); + + // Create the new future + let fut = future(dependencies.out()); + + // Clone in our cells + let value = state.value.clone(); + let schedule_update = state.update.clone(); + + // Cancel the current future + if let Some(current) = state.task.take() { + cx.remove_future(current); + } + + state.task.set(Some(cx.push_future(async move { + let data; + #[cfg(feature = "ssr")] + { + data = fut.await; + if first_run { + if let Err(err) = crate::prelude::server_context().push_html_data(&data) { + log::error!("Failed to push HTML data: {}", err); + }; + } + } + #[cfg(not(feature = "ssr"))] + { + data = fut.await; + } + *value.borrow_mut() = Some(Box::new(data)); + + schedule_update(); + }))); + } + + if first_run { + #[cfg(feature = "ssr")] + { + log::trace!("Suspending first run of use_server_future"); + cx.suspend(); + } + None + } else { + Some(state) + } +} + +pub struct UseServerFuture { + update: Arc, + needs_regen: Cell, + task: Cell>, + dependencies: Vec>, + value: Rc>>>, +} + +impl UseServerFuture { + /// Restart the future with new dependencies. + /// + /// Will not cancel the previous future, but will ignore any values that it + /// generates. + pub fn restart(&self) { + self.needs_regen.set(true); + (self.update)(); + } + + /// Forcefully cancel a future + pub fn cancel(&self, cx: &ScopeState) { + if let Some(task) = self.task.take() { + cx.remove_future(task); + } + } + + /// Return any value, even old values if the future has not yet resolved. + /// + /// If the future has never completed, the returned value will be `None`. + pub fn value(&self) -> Ref<'_, T> { + Ref::map(self.value.borrow(), |v| v.as_deref().unwrap()) + } + + /// Get the ID of the future in Dioxus' internal scheduler + pub fn task(&self) -> Option { + self.task.get() + } + + /// Get the current state of the future. + pub fn reloading(&self) -> bool { + self.task.get().is_some() + } +} diff --git a/packages/fullstack/src/html_storage/deserialize.rs b/packages/fullstack/src/html_storage/deserialize.rs new file mode 100644 index 000000000..822b45076 --- /dev/null +++ b/packages/fullstack/src/html_storage/deserialize.rs @@ -0,0 +1,79 @@ +use serde::de::DeserializeOwned; + +use base64::engine::general_purpose::STANDARD; +use base64::Engine; + +use super::HTMLDataCursor; + +#[allow(unused)] +pub(crate) fn serde_from_bytes(string: &[u8]) -> Option { + let decompressed = match STANDARD.decode(string) { + Ok(bytes) => bytes, + Err(err) => { + log::error!("Failed to decode base64: {}", err); + return None; + } + }; + + match postcard::from_bytes(&decompressed) { + Ok(data) => Some(data), + Err(err) => { + log::error!("Failed to deserialize: {}", err); + None + } + } +} + +static SERVER_DATA: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + #[cfg(target_arch = "wasm32")] + { + let window = web_sys::window()?.document()?; + let element = match window.get_element_by_id("dioxus-storage-data") { + Some(element) => element, + None => { + log::error!("Failed to get element by id: dioxus-storage-data"); + return None; + } + }; + let attribute = match element.get_attribute("data-serialized") { + Some(attribute) => attribute, + None => { + log::error!("Failed to get attribute: data-serialized"); + return None; + } + }; + + let data: super::HTMLData = serde_from_bytes(attribute.as_bytes())?; + + Some(data.cursor()) + } + #[cfg(not(target_arch = "wasm32"))] + { + None + } + }); + +pub(crate) fn take_server_data() -> Option { + SERVER_DATA.as_ref()?.take() +} + +#[cfg(not(feature = "ssr"))] +/// Get the props from the document. This is only available in the browser. +/// +/// When dioxus-fullstack renders the page, it will serialize the root props and put them in the document. This function gets them from the document. +pub fn get_root_props_from_document() -> Option { + #[cfg(not(target_arch = "wasm32"))] + { + None + } + #[cfg(target_arch = "wasm32")] + { + let attribute = web_sys::window()? + .document()? + .get_element_by_id("dioxus-storage-props")? + .get_attribute("data-serialized")?; + + serde_from_bytes(attribute.as_bytes()) + } +} diff --git a/packages/fullstack/src/html_storage/mod.rs b/packages/fullstack/src/html_storage/mod.rs new file mode 100644 index 000000000..2d64953ae --- /dev/null +++ b/packages/fullstack/src/html_storage/mod.rs @@ -0,0 +1,107 @@ +#![allow(unused)] + +use std::sync::atomic::AtomicUsize; + +use serde::{de::DeserializeOwned, Serialize}; + +pub(crate) mod deserialize; + +pub(crate) mod serialize; + +#[derive(serde::Serialize, serde::Deserialize, Default)] +pub(crate) struct HTMLData { + pub data: Vec>, +} + +impl HTMLData { + pub(crate) fn push(&mut self, value: &T) { + let serialized = postcard::to_allocvec(value).unwrap(); + self.data.push(serialized); + } + + pub(crate) fn cursor(self) -> HTMLDataCursor { + HTMLDataCursor { + data: self.data, + index: AtomicUsize::new(0), + } + } +} + +pub(crate) struct HTMLDataCursor { + data: Vec>, + index: AtomicUsize, +} + +impl HTMLDataCursor { + pub fn take(&self) -> Option { + let current = self.index.load(std::sync::atomic::Ordering::SeqCst); + if current >= self.data.len() { + log::error!( + "Tried to take more data than was available, len: {}, index: {}", + self.data.len(), + current + ); + return None; + } + let mut cursor = &self.data[current]; + self.index.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + match postcard::from_bytes(cursor) { + Ok(x) => Some(x), + Err(e) => { + log::error!("Error deserializing data: {:?}", e); + None + } + } + } +} + +#[test] +fn serialized_and_deserializes() { + use postcard::to_allocvec; + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] + struct Data { + a: u32, + b: String, + bytes: Vec, + nested: Nested, + } + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] + struct Nested { + a: u32, + b: u16, + c: u8, + } + + for x in 0..10usize { + for y in 0..10 { + let mut as_string: Vec = Vec::new(); + let data = vec![ + Data { + a: x as u32, + b: "hello".to_string(), + bytes: vec![0; x], + nested: Nested { + a: 1, + b: x as u16, + c: 3 + }, + }; + y + ]; + serialize::serde_to_writable(&data, &mut as_string).unwrap(); + + println!("{:?}", as_string); + println!( + "original size: {}", + std::mem::size_of::() * data.len() + ); + println!("serialized size: {}", to_allocvec(&data).unwrap().len()); + println!("compressed size: {}", as_string.len()); + + let decoded: Vec = deserialize::serde_from_bytes(&as_string).unwrap(); + assert_eq!(data, decoded); + } + } +} diff --git a/packages/fullstack/src/props_html/serialize_props.rs b/packages/fullstack/src/html_storage/serialize.rs similarity index 50% rename from packages/fullstack/src/props_html/serialize_props.rs rename to packages/fullstack/src/html_storage/serialize.rs index 0c6ae3c49..7f0a7efc7 100644 --- a/packages/fullstack/src/props_html/serialize_props.rs +++ b/packages/fullstack/src/html_storage/serialize.rs @@ -15,12 +15,26 @@ pub(crate) fn serde_to_writable( #[cfg(feature = "ssr")] /// Encode data into a element. This is inteded to be used in the server to send data to the client. -pub(crate) fn encode_in_element( - data: T, +pub(crate) fn encode_props_in_element( + data: &T, write_to: &mut impl std::io::Write, ) -> std::io::Result<()> { - write_to - .write_all(r#""#.as_bytes()) +} + +#[cfg(feature = "ssr")] +/// Encode data into a element. This is inteded to be used in the server to send data to the client. +pub(crate) fn encode_in_element( + data: &super::HTMLData, + write_to: &mut impl std::io::Write, +) -> std::io::Result<()> { + write_to.write_all( + r#""#.as_bytes()) } diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 4728c1846..04b65ba4c 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -5,7 +5,7 @@ pub use once_cell; -mod props_html; +mod html_storage; #[cfg(feature = "router")] pub mod router; @@ -14,6 +14,7 @@ pub mod router; mod adapters; #[cfg(feature = "ssr")] pub use adapters::*; +mod hooks; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; pub mod launch; @@ -35,8 +36,9 @@ pub mod prelude { pub use crate::adapters::salvo_adapter::*; #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; + use crate::hooks; #[cfg(not(feature = "ssr"))] - pub use crate::props_html::deserialize_props::get_root_props_from_document; + pub use crate::html_storage::deserialize::get_root_props_from_document; #[cfg(all(feature = "ssr", feature = "router"))] pub use crate::render::pre_cache_static_routes_with_props; #[cfg(feature = "ssr")] @@ -57,4 +59,6 @@ pub mod prelude { #[cfg(feature = "ssr")] pub use dioxus_ssr::incremental::IncrementalRendererConfig; pub use server_fn::{self, ServerFn as _, ServerFnError}; + + pub use hooks::{server_cached::server_cached, server_future::use_server_future}; } diff --git a/packages/fullstack/src/props_html/deserialize_props.rs b/packages/fullstack/src/props_html/deserialize_props.rs deleted file mode 100644 index f5c805865..000000000 --- a/packages/fullstack/src/props_html/deserialize_props.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::de::DeserializeOwned; - -use base64::engine::general_purpose::STANDARD; -use base64::Engine; - -#[allow(unused)] -pub(crate) fn serde_from_bytes(string: &[u8]) -> Option { - let decompressed = STANDARD.decode(string).ok()?; - - postcard::from_bytes(&decompressed).ok() -} - -#[cfg(not(feature = "ssr"))] -/// Get the props from the document. This is only available in the browser. -/// -/// When dioxus-fullstack renders the page, it will serialize the root props and put them in the document. This function gets them from the document. -pub fn get_root_props_from_document() -> Option { - #[cfg(not(target_arch = "wasm32"))] - { - None - } - #[cfg(target_arch = "wasm32")] - { - let attribute = web_sys::window()? - .document()? - .get_element_by_id("dioxus-storage")? - .get_attribute("data-serialized")?; - - serde_from_bytes(attribute.as_bytes()) - } -} diff --git a/packages/fullstack/src/props_html/mod.rs b/packages/fullstack/src/props_html/mod.rs deleted file mode 100644 index 143c40231..000000000 --- a/packages/fullstack/src/props_html/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -pub(crate) mod deserialize_props; - -pub(crate) mod serialize_props; - -#[test] -fn serialized_and_deserializes() { - use postcard::to_allocvec; - - #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] - struct Data { - a: u32, - b: String, - bytes: Vec, - nested: Nested, - } - - #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] - struct Nested { - a: u32, - b: u16, - c: u8, - } - - for x in 0..10usize { - for y in 0..10 { - let mut as_string: Vec = Vec::new(); - let data = vec![ - Data { - a: x as u32, - b: "hello".to_string(), - bytes: vec![0; x], - nested: Nested { - a: 1, - b: x as u16, - c: 3 - }, - }; - y - ]; - serialize_props::serde_to_writable(&data, &mut as_string).unwrap(); - - println!("{:?}", as_string); - println!( - "original size: {}", - std::mem::size_of::() * data.len() - ); - println!("serialized size: {}", to_allocvec(&data).unwrap().len()); - println!("compressed size: {}", as_string.len()); - - let decoded: Vec = deserialize_props::serde_from_bytes(&as_string).unwrap(); - assert_eq!(data, decoded); - } - } -} diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index fbd4bd7f3..9699a5eb3 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -2,19 +2,22 @@ use std::sync::Arc; +use crate::server_context::SERVER_CONTEXT; use dioxus::prelude::VirtualDom; use dioxus_ssr::{ incremental::{IncrementalRendererConfig, RenderFreshness, WrapBody}, Renderer, }; use serde::Serialize; +use std::sync::RwLock; +use tokio::task::spawn_blocking; -use crate::{prelude::*, server_context::with_server_context}; +use crate::prelude::*; use dioxus::prelude::*; enum SsrRendererPool { - Renderer(object_pool::Pool), - Incremental(object_pool::Pool), + Renderer(RwLock>), + Incremental(RwLock>), } impl SsrRendererPool { @@ -24,46 +27,130 @@ impl SsrRendererPool { route: String, component: Component

, props: P, - to: &mut WriteBuffer, server_context: &DioxusServerContext, - ) -> Result { - let wrapper = FullstackRenderer { cfg }; + ) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> { + let wrapper = FullstackRenderer { + cfg: cfg.clone(), + server_context: server_context.clone(), + }; match self { Self::Renderer(pool) => { let server_context = Box::new(server_context.clone()); - let mut vdom = VirtualDom::new_with_props(component, props); + let mut renderer = pool.write().unwrap().pop().unwrap_or_else(pre_renderer); - with_server_context(server_context, || { - let _ = vdom.rebuild(); + let (tx, rx) = tokio::sync::oneshot::channel(); + + spawn_blocking(move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(async move { + let mut vdom = VirtualDom::new_with_props(component, props); + let mut to = WriteBuffer { buffer: Vec::new() }; + // before polling the future, we need to set the context + let prev_context = + SERVER_CONTEXT.with(|ctx| ctx.replace(server_context)); + // poll the future, which may call server_context() + log::info!("Rebuilding vdom"); + let _ = vdom.rebuild(); + vdom.wait_for_suspense().await; + log::info!("Suspense resolved"); + // after polling the future, we need to restore the context + SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); + + if let Err(err) = wrapper.render_before_body(&mut *to) { + let _ = tx.send(Err(err)); + return; + } + if let Err(err) = renderer.render_to(&mut to, &vdom) { + let _ = tx.send(Err( + dioxus_router::prelude::IncrementalRendererError::RenderError( + err, + ), + )); + return; + } + if let Err(err) = wrapper.render_after_body(&mut *to) { + let _ = tx.send(Err(err)); + return; + } + match String::from_utf8(to.buffer) { + Ok(html) => { + let _ = + tx.send(Ok((renderer, RenderFreshness::now(None), html))); + } + Err(err) => { + dioxus_ssr::incremental::IncrementalRendererError::Other( + Box::new(err), + ); + } + } + }); }); - - let mut renderer = pool.pull(pre_renderer); - - // SAFETY: The fullstack renderer will only write UTF-8 to the buffer. - wrapper.render_before_body(&mut **to)?; - renderer.render_to(to, &vdom)?; - wrapper.render_after_body(&mut **to)?; - - Ok(RenderFreshness::now(None)) + let (renderer, freshness, html) = rx.await.unwrap()?; + pool.write().unwrap().push(renderer); + Ok((freshness, html)) } Self::Incremental(pool) => { let mut renderer = - pool.pull(|| incremental_pre_renderer(cfg.incremental.as_ref().unwrap())); - Ok(renderer - .render( - route, - component, - props, - &mut **to, - |vdom| { - let server_context = Box::new(server_context.clone()); - with_server_context(server_context, || { - let _ = vdom.rebuild(); - }); - }, - &wrapper, - ) - .await?) + pool.write().unwrap().pop().unwrap_or_else(|| { + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()) + }); + + let (tx, rx) = tokio::sync::oneshot::channel(); + + let server_context = server_context.clone(); + spawn_blocking(move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(async move { + let mut to = WriteBuffer { buffer: Vec::new() }; + match renderer + .render( + route, + component, + props, + &mut *to, + |vdom| { + Box::pin(async move { + // before polling the future, we need to set the context + let prev_context = SERVER_CONTEXT + .with(|ctx| ctx.replace(Box::new(server_context))); + // poll the future, which may call server_context() + log::info!("Rebuilding vdom"); + let _ = vdom.rebuild(); + vdom.wait_for_suspense().await; + log::info!("Suspense resolved"); + // after polling the future, we need to restore the context + SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); + }) + }, + &wrapper, + ) + .await + { + Ok(freshness) => { + match String::from_utf8(to.buffer).map_err(|err| { + dioxus_ssr::incremental::IncrementalRendererError::Other( + Box::new(err), + ) + }) { + Ok(html) => { + let _ = tx.send(Ok((freshness, html))); + } + Err(err) => { + let _ = tx.send(Err(err)); + } + } + } + Err(err) => { + let _ = tx.send(Err(err)); + } + } + }) + }); + let (freshness, html) = rx.await.unwrap()?; + + Ok((freshness, html)) } } } @@ -80,18 +167,22 @@ impl SSRState { pub(crate) fn new(cfg: &ServeConfig

) -> Self { if cfg.incremental.is_some() { return Self { - renderers: Arc::new(SsrRendererPool::Incremental(object_pool::Pool::new( - 10, - || incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), - ))), + renderers: Arc::new(SsrRendererPool::Incremental(RwLock::new(vec![ + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), + ]))), }; } Self { - renderers: Arc::new(SsrRendererPool::Renderer(object_pool::Pool::new( - 10, - pre_renderer, - ))), + renderers: Arc::new(SsrRendererPool::Renderer(RwLock::new(vec![ + pre_renderer(), + pre_renderer(), + pre_renderer(), + pre_renderer(), + ]))), } } @@ -106,30 +197,25 @@ impl SSRState { > + Send + 'a { async move { - let mut html = WriteBuffer { buffer: Vec::new() }; let ServeConfig { app, props, .. } = cfg; - let freshness = self + let (freshness, html) = self .renderers - .render_to(cfg, route, *app, props.clone(), &mut html, server_context) + .render_to(cfg, route, *app, props.clone(), server_context) .await?; - Ok(RenderResponse { - html: String::from_utf8(html.buffer).map_err(|err| { - dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)) - })?, - freshness, - }) + Ok(RenderResponse { html, freshness }) } } } -struct FullstackRenderer<'a, P: Clone + Send + Sync + 'static> { - cfg: &'a ServeConfig

, +struct FullstackRenderer { + cfg: ServeConfig

, + server_context: DioxusServerContext, } -impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::WrapBody - for FullstackRenderer<'a, P> +impl dioxus_ssr::incremental::WrapBody + for FullstackRenderer

{ fn render_before_body( &self, @@ -147,7 +233,29 @@ impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental:: to: &mut R, ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { // serialize the props - crate::props_html::serialize_props::encode_in_element(&self.cfg.props, to)?; + crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to)?; + // serialize the server state + crate::html_storage::serialize::encode_in_element( + &*self.server_context.html_data().map_err(|_| { + dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new({ + #[derive(Debug)] + struct HTMLDataReadError; + + impl std::fmt::Display for HTMLDataReadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str( + "Failed to read the server data to serialize it into the HTML", + ) + } + } + + impl std::error::Error for HTMLDataReadError {} + + HTMLDataReadError + })) + })?, + to, + )?; #[cfg(all(debug_assertions, feature = "hot-reload"))] { @@ -192,6 +300,7 @@ impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental:: } /// A rendered response from the server. +#[derive(Debug)] pub struct RenderResponse { pub(crate) html: String, pub(crate) freshness: RenderFreshness, @@ -232,7 +341,10 @@ where Rt: dioxus_router::prelude::Routable + Send + Sync + Serialize, ::Err: std::fmt::Display, { - let wrapper = FullstackRenderer { cfg }; + let wrapper = FullstackRenderer { + cfg: cfg.clone(), + server_context: Default::default(), + }; let mut renderer = incremental_pre_renderer( cfg.incremental .as_ref() diff --git a/packages/fullstack/src/server_context.rs b/packages/fullstack/src/server_context.rs index fcd0ef66b..6f3b4b6c9 100644 --- a/packages/fullstack/src/server_context.rs +++ b/packages/fullstack/src/server_context.rs @@ -1,3 +1,4 @@ +use crate::html_storage::HTMLData; pub use server_fn_impl::*; use std::sync::Arc; use std::sync::RwLock; @@ -13,6 +14,7 @@ pub struct DioxusServerContext { >, response_parts: std::sync::Arc>, pub(crate) parts: Arc>, + html_data: Arc>, } #[allow(clippy::derivable_impls)] @@ -24,6 +26,7 @@ impl Default for DioxusServerContext { http::response::Response::new(()).into_parts().0, )), parts: std::sync::Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)), + html_data: Arc::new(RwLock::new(HTMLData::default())), } } } @@ -45,6 +48,7 @@ mod server_fn_impl { response_parts: std::sync::Arc::new(RwLock::new( http::response::Response::new(()).into_parts().0, )), + html_data: Arc::new(RwLock::new(HTMLData::default())), } } @@ -100,11 +104,26 @@ mod server_fn_impl { ) -> Result { T::from_request(self).await } + + /// Insert some data into the html data store + pub(crate) fn push_html_data( + &self, + value: &T, + ) -> Result<(), PoisonError>> { + self.html_data.write().map(|mut map| { + map.push(value); + }) + } + + /// Get the html data store + pub(crate) fn html_data(&self) -> LockResult> { + self.html_data.read() + } } } std::thread_local! { - static SERVER_CONTEXT: std::cell::RefCell> = std::cell::RefCell::new(Box::new(DioxusServerContext::default() )); + pub(crate) static SERVER_CONTEXT: std::cell::RefCell> = std::cell::RefCell::new(Box::new(DioxusServerContext::default() )); } /// Get information about the current server request. diff --git a/packages/router/src/incremental.rs b/packages/router/src/incremental.rs index f465f45f6..aad60e27a 100644 --- a/packages/router/src/incremental.rs +++ b/packages/router/src/incremental.rs @@ -1,4 +1,6 @@ //! Exentsions to the incremental renderer to support pre-caching static routes. +use core::pin::Pin; +use std::future::Future; use std::str::FromStr; use dioxus::prelude::*; @@ -47,7 +49,9 @@ where route, &mut tokio::io::sink(), |vdom| { - let _ = vdom.rebuild(); + Box::pin(async move { + let _ = vdom.wait_for_suspense().await; + }) }, wrapper, ) @@ -65,7 +69,12 @@ where } /// Render a route to a writer. -pub async fn render_route( +pub async fn render_route< + R: WrapBody + Send + Sync, + Rt, + W, + F: FnOnce(&mut VirtualDom) -> Pin + '_>>, +>( renderer: &mut IncrementalRenderer, route: Rt, writer: &mut W, diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs index 0e999b493..fcecaacc5 100644 --- a/packages/ssr/src/incremental.rs +++ b/packages/ssr/src/incremental.rs @@ -6,10 +6,12 @@ use crate::fs_cache::ValidCachedPath; use dioxus_core::{Element, Scope, VirtualDom}; use rustc_hash::FxHasher; use std::{ + future::Future, hash::BuildHasherDefault, io::Write, ops::{Deref, DerefMut}, path::PathBuf, + pin::Pin, time::{Duration, SystemTime}, }; use tokio::io::{AsyncWrite, AsyncWriteExt, BufReader}; @@ -67,36 +69,29 @@ impl IncrementalRenderer { self.invalidate_after.is_some() } - fn render_and_cache<'a, P: 'static, R: WrapBody + Send + Sync>( + async fn render_and_cache<'a, P: 'static, R: WrapBody + Send + Sync>( &'a mut self, route: String, comp: fn(Scope

) -> Element, props: P, output: &'a mut (impl AsyncWrite + Unpin + Send), - rebuild_with: impl FnOnce(&mut VirtualDom), + rebuild_with: impl FnOnce(&mut VirtualDom) -> Pin + '_>>, renderer: &'a R, - ) -> impl std::future::Future> + 'a + Send - { + ) -> Result { let mut html_buffer = WriteBuffer { buffer: Vec::new() }; - let result_1; - let result2; { let mut vdom = VirtualDom::new_with_props(comp, props); - rebuild_with(&mut vdom); + rebuild_with(&mut vdom).await; - result_1 = renderer.render_before_body(&mut *html_buffer); - result2 = self.ssr_renderer.render_to(&mut html_buffer, &vdom); + renderer.render_before_body(&mut *html_buffer)?; + self.ssr_renderer.render_to(&mut html_buffer, &vdom)?; } - async move { - result_1?; - result2?; - renderer.render_after_body(&mut *html_buffer)?; - let html_buffer = html_buffer.buffer; + renderer.render_after_body(&mut *html_buffer)?; + let html_buffer = html_buffer.buffer; - output.write_all(&html_buffer).await?; + output.write_all(&html_buffer).await?; - self.add_to_cache(route, html_buffer) - } + self.add_to_cache(route, html_buffer) } fn add_to_cache( @@ -178,7 +173,7 @@ impl IncrementalRenderer { component: fn(Scope

) -> Element, props: P, output: &mut (impl AsyncWrite + Unpin + std::marker::Send), - rebuild_with: impl FnOnce(&mut VirtualDom), + rebuild_with: impl FnOnce(&mut VirtualDom) -> Pin + '_>>, renderer: &R, ) -> Result { // check if this route is cached