dioxus_fullstack_protocol/
lib.rs

1#![warn(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use base64::Engine;
5use dioxus_core::CapturedError;
6use serde::Serialize;
7use std::{cell::RefCell, io::Cursor, rc::Rc};
8
9#[cfg(feature = "web")]
10thread_local! {
11    static CONTEXT: RefCell<Option<HydrationContext>> = const { RefCell::new(None) };
12}
13
14/// Data shared between the frontend and the backend for hydration
15/// of server functions.
16#[derive(Default, Clone)]
17pub struct HydrationContext {
18    #[cfg(feature = "web")]
19    /// Is resolving suspense done on the client
20    suspense_finished: bool,
21    data: Rc<RefCell<HTMLData>>,
22}
23
24impl HydrationContext {
25    /// Create a new serialize context from the serialized data
26    pub fn from_serialized(
27        data: &[u8],
28        debug_types: Option<Vec<String>>,
29        debug_locations: Option<Vec<String>>,
30    ) -> Self {
31        Self {
32            #[cfg(feature = "web")]
33            suspense_finished: false,
34            data: Rc::new(RefCell::new(HTMLData::from_serialized(
35                data,
36                debug_types,
37                debug_locations,
38            ))),
39        }
40    }
41
42    /// Serialize the data in the context to be sent to the client
43    pub fn serialized(&self) -> SerializedHydrationData {
44        self.data.borrow().serialized()
45    }
46
47    /// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
48    pub fn create_entry<T>(&self) -> SerializeContextEntry<T> {
49        let entry_index = self.data.borrow_mut().create_entry();
50
51        SerializeContextEntry {
52            index: entry_index,
53            context: self.clone(),
54            phantom: std::marker::PhantomData,
55        }
56    }
57
58    /// Get the entry for the error in the suspense boundary
59    pub fn error_entry(&self) -> SerializeContextEntry<Option<CapturedError>> {
60        // The first entry is reserved for the error
61        let entry_index = self.data.borrow_mut().create_entry_with_id(0);
62
63        SerializeContextEntry {
64            index: entry_index,
65            context: self.clone(),
66            phantom: std::marker::PhantomData,
67        }
68    }
69
70    /// Extend this data with the data from another [`HydrationContext`]
71    pub fn extend(&self, other: &Self) {
72        self.data.borrow_mut().extend(&other.data.borrow());
73    }
74
75    #[cfg(feature = "web")]
76    /// Run a closure inside of this context
77    pub fn in_context<T>(&self, f: impl FnOnce() -> T) -> T {
78        CONTEXT.with(|context| {
79            let old = context.borrow().clone();
80            *context.borrow_mut() = Some(self.clone());
81            let result = f();
82            *context.borrow_mut() = old;
83            result
84        })
85    }
86
87    pub(crate) fn insert<T: Serialize>(
88        &self,
89        id: usize,
90        value: &T,
91        location: &'static std::panic::Location<'static>,
92    ) {
93        self.data.borrow_mut().insert(id, value, location);
94    }
95
96    pub(crate) fn get<T: serde::de::DeserializeOwned>(
97        &self,
98        id: usize,
99    ) -> Result<T, TakeDataError> {
100        // If suspense is finished on the client, we can assume that the data is available
101        #[cfg(feature = "web")]
102        if self.suspense_finished {
103            return Err(TakeDataError::DataNotAvailable);
104        }
105        self.data.borrow().get(id)
106    }
107}
108
109/// An entry into the serialized context. The order entries are created in must be consistent
110/// between the server and the client.
111pub struct SerializeContextEntry<T> {
112    /// The index this context will be inserted into inside the serialize context
113    index: usize,
114    /// The context this entry is associated with
115    context: HydrationContext,
116    phantom: std::marker::PhantomData<T>,
117}
118
119impl<T> Clone for SerializeContextEntry<T> {
120    fn clone(&self) -> Self {
121        Self {
122            index: self.index,
123            context: self.context.clone(),
124            phantom: std::marker::PhantomData,
125        }
126    }
127}
128
129impl<T> SerializeContextEntry<T> {
130    /// Insert data into an entry that was created with [`HydrationContext::create_entry`]
131    pub fn insert(self, value: &T, location: &'static std::panic::Location<'static>)
132    where
133        T: Serialize,
134    {
135        self.context.insert(self.index, value, location);
136    }
137
138    /// Grab the data from the serialize context
139    pub fn get(&self) -> Result<T, TakeDataError>
140    where
141        T: serde::de::DeserializeOwned,
142    {
143        self.context.get(self.index)
144    }
145}
146
147/// Check if the client is currently rendering a component for hydration. Always returns true on the server.
148pub fn is_hydrating() -> bool {
149    #[cfg(feature = "web")]
150    {
151        // On the client, we can check if the context is set
152        CONTEXT.with(|context| context.borrow().is_some())
153    }
154    #[cfg(not(feature = "web"))]
155    {
156        true
157    }
158}
159
160/// Get or insert the current serialize context. On the client, the hydration context this returns
161/// will always return `TakeDataError::DataNotAvailable` if hydration of the current chunk is finished.
162pub fn serialize_context() -> HydrationContext {
163    #[cfg(feature = "web")]
164    // On the client, the hydration logic provides the context in a global
165    if let Some(current_context) = CONTEXT.with(|context| context.borrow().clone()) {
166        current_context
167    } else {
168        // If the context is not set, then suspense is not active
169        HydrationContext {
170            suspense_finished: true,
171            ..Default::default()
172        }
173    }
174    #[cfg(not(feature = "web"))]
175    {
176        // On the server each scope creates the context lazily
177        dioxus_core::has_context()
178            .unwrap_or_else(|| dioxus_core::provide_context(HydrationContext::default()))
179    }
180}
181
182pub(crate) struct HTMLData {
183    /// The position of the cursor in the data. This is only used on the client
184    pub(crate) cursor: usize,
185    /// The data required for hydration
186    pub data: Vec<Option<Vec<u8>>>,
187    /// The types of each serialized data
188    ///
189    /// NOTE: we don't store this in the main data vec because we don't want to include it in
190    /// release mode and we can't assume both the client and server are built with debug assertions
191    /// matching
192    #[cfg(debug_assertions)]
193    pub debug_types: Vec<Option<String>>,
194    /// The locations of each serialized data
195    #[cfg(debug_assertions)]
196    pub debug_locations: Vec<Option<String>>,
197}
198
199impl Default for HTMLData {
200    fn default() -> Self {
201        Self {
202            cursor: 1,
203            data: Vec::new(),
204            #[cfg(debug_assertions)]
205            debug_types: Vec::new(),
206            #[cfg(debug_assertions)]
207            debug_locations: Vec::new(),
208        }
209    }
210}
211
212impl HTMLData {
213    fn from_serialized(
214        data: &[u8],
215        debug_types: Option<Vec<String>>,
216        debug_locations: Option<Vec<String>>,
217    ) -> Self {
218        let data = ciborium::from_reader(Cursor::new(data)).unwrap();
219        Self {
220            cursor: 1,
221            data,
222            #[cfg(debug_assertions)]
223            debug_types: debug_types
224                .unwrap_or_default()
225                .into_iter()
226                .map(Some)
227                .collect(),
228            #[cfg(debug_assertions)]
229            debug_locations: debug_locations
230                .unwrap_or_default()
231                .into_iter()
232                .map(Some)
233                .collect(),
234        }
235    }
236
237    /// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
238    fn create_entry(&mut self) -> usize {
239        let id = self.cursor;
240        self.cursor += 1;
241        self.create_entry_with_id(id)
242    }
243
244    fn create_entry_with_id(&mut self, id: usize) -> usize {
245        while id + 1 > self.data.len() {
246            self.data.push(None);
247            #[cfg(debug_assertions)]
248            {
249                self.debug_types.push(None);
250                self.debug_locations.push(None);
251            }
252        }
253        id
254    }
255
256    /// Insert data into an entry that was created with [`Self::create_entry`]
257    fn insert<T: Serialize>(
258        &mut self,
259        id: usize,
260        value: &T,
261        location: &'static std::panic::Location<'static>,
262    ) {
263        let mut serialized = Vec::new();
264        ciborium::into_writer(value, &mut serialized).unwrap();
265        self.data[id] = Some(serialized);
266        #[cfg(debug_assertions)]
267        {
268            self.debug_types[id] = Some(std::any::type_name::<T>().to_string());
269            self.debug_locations[id] = Some(location.to_string());
270        }
271    }
272
273    /// Get the data from the serialize context
274    fn get<T: serde::de::DeserializeOwned>(&self, index: usize) -> Result<T, TakeDataError> {
275        if index >= self.data.len() {
276            tracing::trace!(
277                "Tried to take more data than was available, len: {}, index: {}; This is normal if the server function was started on the client, but may indicate a bug if the server function result should be deserialized from the server",
278                self.data.len(),
279                index
280            );
281            return Err(TakeDataError::DataNotAvailable);
282        }
283        let bytes = self.data[index].as_ref();
284        match bytes {
285            Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
286                Ok(x) => Ok(x),
287                Err(err) => {
288                    #[cfg(debug_assertions)]
289                    {
290                        let debug_type = self.debug_types.get(index);
291                        let debug_locations = self.debug_locations.get(index);
292
293                        if let (Some(Some(debug_type)), Some(Some(debug_locations))) =
294                            (debug_type, debug_locations)
295                        {
296                            let client_type = std::any::type_name::<T>();
297                            let client_location = std::panic::Location::caller();
298                            // We we have debug types and a location, we can provide a more helpful error message
299                            tracing::error!(
300                                "Error deserializing data: {err:?}\n\nThis type was serialized on the server at {debug_locations} with the type name {debug_type}. The client failed to deserialize the type {client_type} at {client_location}.",
301                            );
302                            return Err(TakeDataError::DeserializationError(err));
303                        }
304                    }
305                    // Otherwise, just log the generic deserialization error
306                    tracing::error!("Error deserializing data: {:?}", err);
307                    Err(TakeDataError::DeserializationError(err))
308                }
309            },
310            None => Err(TakeDataError::DataPending),
311        }
312    }
313
314    /// Extend this data with the data from another [`HTMLData`]
315    pub(crate) fn extend(&mut self, other: &Self) {
316        // Make sure this vectors error entry exists even if it is empty
317        if self.data.is_empty() {
318            self.data.push(None);
319            #[cfg(debug_assertions)]
320            {
321                self.debug_types.push(None);
322                self.debug_locations.push(None);
323            }
324        }
325
326        let mut other_data_iter = other.data.iter().cloned();
327        #[cfg(debug_assertions)]
328        let mut other_debug_types_iter = other.debug_types.iter().cloned();
329        #[cfg(debug_assertions)]
330        let mut other_debug_locations_iter = other.debug_locations.iter().cloned();
331
332        // Merge the error entry from the other context
333        if let Some(Some(other_error)) = other_data_iter.next() {
334            self.data[0] = Some(other_error.clone());
335            #[cfg(debug_assertions)]
336            {
337                self.debug_types[0] = other_debug_types_iter.next().unwrap_or(None);
338                self.debug_locations[0] = other_debug_locations_iter.next().unwrap_or(None);
339            }
340        }
341
342        // Don't copy the error from the other context
343        self.data.extend(other_data_iter);
344        #[cfg(debug_assertions)]
345        {
346            self.debug_types.extend(other_debug_types_iter);
347            self.debug_locations.extend(other_debug_locations_iter);
348        }
349    }
350
351    /// Encode data as base64. This is intended to be used in the server to send data to the client.
352    pub(crate) fn serialized(&self) -> SerializedHydrationData {
353        let mut serialized = Vec::new();
354        ciborium::into_writer(&self.data, &mut serialized).unwrap();
355
356        let data = base64::engine::general_purpose::STANDARD.encode(serialized);
357
358        let format_js_list_of_strings = |list: &[Option<String>]| {
359            let body = list
360                .iter()
361                .map(|s| match s {
362                    Some(s) => {
363                        // Escape backslashes, quotes, and newlines
364                        let escaped = s
365                            .replace(r#"\"#, r#"\\"#)
366                            .replace("\n", r#"\n"#)
367                            .replace(r#"""#, r#"\""#);
368
369                        format!(r#""{escaped}""#)
370                    }
371                    None => r#""unknown""#.to_string(),
372                })
373                .collect::<Vec<_>>()
374                .join(",");
375            format!("[{}]", body)
376        };
377
378        SerializedHydrationData {
379            data,
380            #[cfg(debug_assertions)]
381            debug_types: format_js_list_of_strings(&self.debug_types),
382            #[cfg(debug_assertions)]
383            debug_locations: format_js_list_of_strings(&self.debug_locations),
384        }
385    }
386}
387
388/// Data that was serialized on the server for hydration on the client. This includes
389/// extra information about the types and sources of the serialized data in debug mode
390pub struct SerializedHydrationData {
391    /// The base64 encoded serialized data
392    pub data: String,
393    /// A list of the types of each serialized data
394    #[cfg(debug_assertions)]
395    pub debug_types: String,
396    /// A list of the locations of each serialized data
397    #[cfg(debug_assertions)]
398    pub debug_locations: String,
399}
400
401/// An error that can occur when trying to take data from the server
402#[derive(Debug)]
403pub enum TakeDataError {
404    /// Deserializing the data failed
405    DeserializationError(ciborium::de::Error<std::io::Error>),
406    /// No data was available
407    DataNotAvailable,
408    /// The server serialized a placeholder for the data, but it isn't available yet
409    DataPending,
410}
411
412impl std::fmt::Display for TakeDataError {
413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414        match self {
415            Self::DeserializationError(e) => write!(f, "DeserializationError: {}", e),
416            Self::DataNotAvailable => write!(f, "DataNotAvailable"),
417            Self::DataPending => write!(f, "DataPending"),
418        }
419    }
420}
421
422impl std::error::Error for TakeDataError {}
423
424/// Create a new entry in the serialize context for the head element hydration
425pub fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
426    serialize_context().create_entry()
427}