From 12e92a4ca0271b9dc9297ee1de3401abebdee79a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 7 Jan 2025 17:16:16 -0800 Subject: [PATCH] small cleanups --- packages/fullstack/src/hooks/server_cached.rs | 14 ++-- packages/fullstack/src/html_storage/data.rs | 21 ++++++ packages/fullstack/src/html_storage/mod.rs | 44 ++++++------ .../fullstack/src/html_storage/serialize.rs | 69 ++++++++----------- 4 files changed, 81 insertions(+), 67 deletions(-) create mode 100644 packages/fullstack/src/html_storage/data.rs diff --git a/packages/fullstack/src/hooks/server_cached.rs b/packages/fullstack/src/hooks/server_cached.rs index c651a155ee..07d88b2c02 100644 --- a/packages/fullstack/src/hooks/server_cached.rs +++ b/packages/fullstack/src/hooks/server_cached.rs @@ -28,24 +28,30 @@ pub fn use_server_cached( use_hook(|| server_cached(server_fn, location)) } +#[allow(unreachable_code)] pub(crate) fn server_cached( value: impl FnOnce() -> O, #[allow(unused)] location: &'static std::panic::Location<'static>, ) -> O { + // On the server, we serialize the data into the HTML Data context (which gets serialized into the HTML itself) #[cfg(feature = "server")] { let serialize = crate::html_storage::serialize_context(); let data = value(); serialize.push(&data, location); - data + return data; } - #[cfg(all(not(feature = "server"), feature = "web"))] + + // On the web, we try to take the data from the server data + #[cfg(feature = "web")] { - dioxus_web::take_server_data() + return dioxus_web::take_server_data() .ok() .flatten() - .unwrap_or_else(value) + .unwrap_or_else(value); } + + // A no-op on all other platforms #[cfg(not(any(feature = "server", feature = "web")))] { value() diff --git a/packages/fullstack/src/html_storage/data.rs b/packages/fullstack/src/html_storage/data.rs new file mode 100644 index 0000000000..70368cdb8d --- /dev/null +++ b/packages/fullstack/src/html_storage/data.rs @@ -0,0 +1,21 @@ +use serde::Serialize; + +/// Serialize a type to CBOR bytes. +/// +/// This trait is implemented for all types that implement `Serialize`. +/// +/// Todo(jon): we want to allocate into one big block per request instead of a bunch of small blocks. +pub trait SerializeCbor { + fn to_cbor_bytes(&self) -> Result, ciborium::ser::Error>; +} + +impl SerializeCbor for T +where + T: Serialize, +{ + fn to_cbor_bytes(&self) -> Result, ciborium::ser::Error> { + let mut serialized = Vec::new(); + ciborium::into_writer(self, &mut serialized)?; + Ok(serialized) + } +} diff --git a/packages/fullstack/src/html_storage/mod.rs b/packages/fullstack/src/html_storage/mod.rs index 14fb5431fa..b25358d427 100644 --- a/packages/fullstack/src/html_storage/mod.rs +++ b/packages/fullstack/src/html_storage/mod.rs @@ -1,14 +1,21 @@ -#![allow(unused)] -use base64::Engine; -use dioxus_lib::prelude::{has_context, provide_context, use_hook}; -use serialize::serde_to_writable; -use std::{cell::RefCell, io::Cursor, rc::Rc, sync::atomic::AtomicUsize}; +#![cfg(feature = "server")] -use base64::engine::general_purpose::STANDARD; -use serde::{de::DeserializeOwned, Serialize}; +use data::SerializeCbor; +use dioxus_lib::prelude::{has_context, provide_context, use_hook}; +use serde::Serialize; +use std::{any::type_name, cell::RefCell, rc::Rc}; +pub(crate) mod data; pub(crate) mod serialize; +pub(crate) fn use_serialize_context() -> SerializeContext { + use_hook(serialize_context) +} + +pub(crate) fn serialize_context() -> SerializeContext { + has_context().unwrap_or_else(|| provide_context(SerializeContext::default())) +} + #[derive(Default, Clone)] pub(crate) struct SerializeContext { data: Rc>, @@ -40,14 +47,6 @@ impl SerializeContext { } } -pub(crate) fn use_serialize_context() -> SerializeContext { - use_hook(serialize_context) -} - -pub(crate) fn serialize_context() -> SerializeContext { - has_context().unwrap_or_else(|| provide_context(SerializeContext::default())) -} - #[derive(Default)] pub(crate) struct HTMLData { /// The data required for hydration @@ -69,11 +68,13 @@ impl HTMLData { fn create_entry(&mut self) -> usize { let id = self.data.len(); self.data.push(None); + #[cfg(debug_assertions)] { self.debug_types.push(None); self.debug_locations.push(None); } + id } @@ -84,27 +85,22 @@ impl HTMLData { value: &T, location: &'static std::panic::Location<'static>, ) { - let mut serialized = Vec::new(); - ciborium::into_writer(value, &mut serialized).unwrap(); - self.data[id] = Some(serialized); + self.data[id] = Some(value.to_cbor_bytes().unwrap()); #[cfg(debug_assertions)] { - self.debug_types[id] = Some(std::any::type_name::().to_string()); + self.debug_types[id] = Some(type_name::().to_string()); self.debug_locations[id] = Some(location.to_string()); } } /// Push resolved data into the serialized server data fn push(&mut self, data: &T, location: &'static std::panic::Location<'static>) { - let mut serialized = Vec::new(); - ciborium::into_writer(data, &mut serialized).unwrap(); - self.data.push(Some(serialized)); + self.data.push(Some(data.to_cbor_bytes().unwrap())); #[cfg(debug_assertions)] { - self.debug_types - .push(Some(std::any::type_name::().to_string())); + self.debug_types.push(Some(type_name::().to_string())); self.debug_locations.push(Some(location.to_string())); } } diff --git a/packages/fullstack/src/html_storage/serialize.rs b/packages/fullstack/src/html_storage/serialize.rs index daa48e51eb..60ef55fad8 100644 --- a/packages/fullstack/src/html_storage/serialize.rs +++ b/packages/fullstack/src/html_storage/serialize.rs @@ -1,23 +1,20 @@ use dioxus_lib::prelude::dioxus_core::DynamicNode; -use dioxus_lib::prelude::{ - has_context, try_consume_context, ErrorContext, ScopeId, SuspenseBoundaryProps, - SuspenseContext, VNode, VirtualDom, -}; +use dioxus_lib::prelude::{has_context, ErrorContext, ScopeId, SuspenseContext, VNode, VirtualDom}; use serde::Serialize; use base64::engine::general_purpose::STANDARD; use base64::Engine; +use super::data::SerializeCbor; use super::SerializeContext; +use crate::html_storage::HTMLData; #[allow(unused)] pub(crate) fn serde_to_writable( value: &T, write_to: &mut impl std::fmt::Write, ) -> Result<(), ciborium::ser::Error> { - let mut serialized = Vec::new(); - ciborium::into_writer(value, &mut serialized).unwrap(); - write_to.write_str(STANDARD.encode(serialized).as_str())?; + write_to.write_str(STANDARD.encode(value.to_cbor_bytes().unwrap()).as_str())?; Ok(()) } @@ -35,7 +32,7 @@ impl super::HTMLData { fn serialize_errors(&mut self, vdom: &VirtualDom, scope: ScopeId) { // If there is an error boundary on the suspense boundary, grab the error from the context API // and throw it on the client so that it bubbles up to the nearest error boundary - let mut error = vdom.in_runtime(|| { + let error = vdom.in_runtime(|| { scope .consume_context::() .and_then(|error_context| error_context.errors().first().cloned()) @@ -43,19 +40,12 @@ impl super::HTMLData { self.push(&error, std::panic::Location::caller()); } - fn take_from_virtual_dom(&mut self, vdom: &VirtualDom) { - self.take_from_scope(vdom, ScopeId::ROOT) - } - fn take_from_scope(&mut self, vdom: &VirtualDom, scope: ScopeId) { vdom.in_runtime(|| { scope.in_runtime(|| { // Grab any serializable server context from this scope - let context: Option = has_context(); - if let Some(context) = context { - let borrow = context.data.borrow(); - let mut data = borrow.data.iter().cloned(); - self.extend(&borrow); + if let Some(context) = has_context::() { + self.extend(&context.data.borrow()); } }); }); @@ -70,6 +60,7 @@ impl super::HTMLData { self.take_from_vnode(vdom, &node); } } + if let Some(node) = scope.try_root_node() { self.take_from_vnode(vdom, node); } @@ -97,30 +88,32 @@ impl super::HTMLData { #[cfg(feature = "server")] /// Encode data as base64. This is intended to be used in the server to send data to the client. pub(crate) fn serialized(&self) -> SerializedHydrationData { - let mut serialized = Vec::new(); - ciborium::into_writer(&self.data, &mut serialized).unwrap(); - let data = base64::engine::general_purpose::STANDARD.encode(serialized); - - let format_js_list_of_strings = |list: &[Option]| { - let body = list - .iter() - .map(|s| match s { - Some(s) => format!(r#""{s}""#), - None => r#""unknown""#.to_string(), - }) - .collect::>() - .join(","); - format!("[{}]", body) - }; + let serialized = self.data.to_cbor_bytes().unwrap(); SerializedHydrationData { - data, + data: base64::engine::general_purpose::STANDARD.encode(serialized), + #[cfg(debug_assertions)] - debug_types: format_js_list_of_strings(&self.debug_types), + debug_types: Self::serialize_js_list(&self.debug_types), + #[cfg(debug_assertions)] - debug_locations: format_js_list_of_strings(&self.debug_locations), + debug_locations: Self::serialize_js_list(&self.debug_locations), } } + + // quick and dirty helper to serialize a list of strings into a javascript list for serialization + fn serialize_js_list(list: &[Option]) -> String { + let body = list + .iter() + .map(|s| match s { + Some(s) => format!(r#""{s}""#), + None => r#""unknown""#.to_string(), + }) + .collect::>() + .join(","); + + format!("[{}]", body) + } } #[cfg(feature = "server")] @@ -142,11 +135,9 @@ impl SerializedHydrationData { pub(crate) fn new(virtual_dom: &VirtualDom, scope: ScopeId) -> Self { // After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node // Extract any data we serialized for hydration (from server futures) - let html_data = - crate::html_storage::HTMLData::extract_from_suspense_boundary(virtual_dom, scope); - + // // serialize the server state into a base64 string - html_data.serialized() + HTMLData::extract_from_suspense_boundary(virtual_dom, scope).serialized() } /// Write the additional script element that contains the hydration data