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 [`SerializeContext::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/// Get or insert the current serialize context. On the client, the hydration context this returns
148/// will always return `TakeDataError::DataNotAvailable` if hydration of the current chunk is finished.
149pub fn serialize_context() -> HydrationContext {
150    #[cfg(feature = "web")]
151    // On the client, the hydration logic provides the context in a global
152    if let Some(current_context) = CONTEXT.with(|context| context.borrow().clone()) {
153        current_context
154    } else {
155        // If the context is not set, then suspense is not active
156        HydrationContext {
157            suspense_finished: true,
158            ..Default::default()
159        }
160    }
161    #[cfg(not(feature = "web"))]
162    {
163        // On the server each scope creates the context lazily
164        dioxus_core::prelude::has_context()
165            .unwrap_or_else(|| dioxus_core::prelude::provide_context(HydrationContext::default()))
166    }
167}
168
169pub(crate) struct HTMLData {
170    /// The position of the cursor in the data. This is only used on the client
171    pub(crate) cursor: usize,
172    /// The data required for hydration
173    pub data: Vec<Option<Vec<u8>>>,
174    /// The types of each serialized data
175    ///
176    /// NOTE: we don't store this in the main data vec because we don't want to include it in
177    /// release mode and we can't assume both the client and server are built with debug assertions
178    /// matching
179    #[cfg(debug_assertions)]
180    pub debug_types: Vec<Option<String>>,
181    /// The locations of each serialized data
182    #[cfg(debug_assertions)]
183    pub debug_locations: Vec<Option<String>>,
184}
185
186impl Default for HTMLData {
187    fn default() -> Self {
188        Self {
189            cursor: 1,
190            data: Vec::new(),
191            #[cfg(debug_assertions)]
192            debug_types: Vec::new(),
193            #[cfg(debug_assertions)]
194            debug_locations: Vec::new(),
195        }
196    }
197}
198
199impl HTMLData {
200    fn from_serialized(
201        data: &[u8],
202        debug_types: Option<Vec<String>>,
203        debug_locations: Option<Vec<String>>,
204    ) -> Self {
205        let data = ciborium::from_reader(Cursor::new(data)).unwrap();
206        Self {
207            cursor: 1,
208            data,
209            #[cfg(debug_assertions)]
210            debug_types: debug_types
211                .unwrap_or_default()
212                .into_iter()
213                .map(Some)
214                .collect(),
215            #[cfg(debug_assertions)]
216            debug_locations: debug_locations
217                .unwrap_or_default()
218                .into_iter()
219                .map(Some)
220                .collect(),
221        }
222    }
223
224    /// 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.
225    fn create_entry(&mut self) -> usize {
226        let id = self.cursor;
227        self.cursor += 1;
228        self.create_entry_with_id(id)
229    }
230
231    fn create_entry_with_id(&mut self, id: usize) -> usize {
232        while id + 1 > self.data.len() {
233            self.data.push(None);
234            #[cfg(debug_assertions)]
235            {
236                self.debug_types.push(None);
237                self.debug_locations.push(None);
238            }
239        }
240        id
241    }
242
243    /// Insert data into an entry that was created with [`Self::create_entry`]
244    fn insert<T: Serialize>(
245        &mut self,
246        id: usize,
247        value: &T,
248        location: &'static std::panic::Location<'static>,
249    ) {
250        let mut serialized = Vec::new();
251        ciborium::into_writer(value, &mut serialized).unwrap();
252        self.data[id] = Some(serialized);
253        #[cfg(debug_assertions)]
254        {
255            self.debug_types[id] = Some(std::any::type_name::<T>().to_string());
256            self.debug_locations[id] = Some(location.to_string());
257        }
258    }
259
260    /// Get the data from the serialize context
261    fn get<T: serde::de::DeserializeOwned>(&self, index: usize) -> Result<T, TakeDataError> {
262        if index >= self.data.len() {
263            tracing::trace!(
264                "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",
265                self.data.len(),
266                index
267            );
268            return Err(TakeDataError::DataNotAvailable);
269        }
270        let bytes = self.data[index].as_ref();
271        match bytes {
272            Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
273                Ok(x) => Ok(x),
274                Err(err) => {
275                    #[cfg(debug_assertions)]
276                    {
277                        let debug_type = self.debug_types.get(index);
278                        let debug_locations = self.debug_locations.get(index);
279
280                        if let (Some(Some(debug_type)), Some(Some(debug_locations))) =
281                            (debug_type, debug_locations)
282                        {
283                            let client_type = std::any::type_name::<T>();
284                            let client_location = std::panic::Location::caller();
285                            // We we have debug types and a location, we can provide a more helpful error message
286                            tracing::error!(
287                                "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}.",
288                            );
289                            return Err(TakeDataError::DeserializationError(err));
290                        }
291                    }
292                    // Otherwise, just log the generic deserialization error
293                    tracing::error!("Error deserializing data: {:?}", err);
294                    Err(TakeDataError::DeserializationError(err))
295                }
296            },
297            None => Err(TakeDataError::DataPending),
298        }
299    }
300
301    /// Extend this data with the data from another [`HTMLData`]
302    pub(crate) fn extend(&mut self, other: &Self) {
303        // Make sure this vectors error entry exists even if it is empty
304        if self.data.is_empty() {
305            self.data.push(None);
306            #[cfg(debug_assertions)]
307            {
308                self.debug_types.push(None);
309                self.debug_locations.push(None);
310            }
311        }
312
313        let mut other_data_iter = other.data.iter().cloned();
314        #[cfg(debug_assertions)]
315        let mut other_debug_types_iter = other.debug_types.iter().cloned();
316        #[cfg(debug_assertions)]
317        let mut other_debug_locations_iter = other.debug_locations.iter().cloned();
318
319        // Merge the error entry from the other context
320        if let Some(Some(other_error)) = other_data_iter.next() {
321            self.data[0] = Some(other_error.clone());
322            #[cfg(debug_assertions)]
323            {
324                self.debug_types[0] = other_debug_types_iter.next().unwrap_or(None);
325                self.debug_locations[0] = other_debug_locations_iter.next().unwrap_or(None);
326            }
327        }
328
329        // Don't copy the error from the other context
330        self.data.extend(other_data_iter);
331        #[cfg(debug_assertions)]
332        {
333            self.debug_types.extend(other_debug_types_iter);
334            self.debug_locations.extend(other_debug_locations_iter);
335        }
336    }
337
338    /// Encode data as base64. This is intended to be used in the server to send data to the client.
339    pub(crate) fn serialized(&self) -> SerializedHydrationData {
340        let mut serialized = Vec::new();
341        ciborium::into_writer(&self.data, &mut serialized).unwrap();
342
343        let data = base64::engine::general_purpose::STANDARD.encode(serialized);
344
345        let format_js_list_of_strings = |list: &[Option<String>]| {
346            let body = list
347                .iter()
348                .map(|s| match s {
349                    Some(s) => format!(r#""{s}""#),
350                    None => r#""unknown""#.to_string(),
351                })
352                .collect::<Vec<_>>()
353                .join(",");
354            format!("[{}]", body)
355        };
356
357        SerializedHydrationData {
358            data,
359            #[cfg(debug_assertions)]
360            debug_types: format_js_list_of_strings(&self.debug_types),
361            #[cfg(debug_assertions)]
362            debug_locations: format_js_list_of_strings(&self.debug_locations),
363        }
364    }
365}
366
367/// Data that was serialized on the server for hydration on the client. This includes
368/// extra information about the types and sources of the serialized data in debug mode
369pub struct SerializedHydrationData {
370    /// The base64 encoded serialized data
371    pub data: String,
372    /// A list of the types of each serialized data
373    #[cfg(debug_assertions)]
374    pub debug_types: String,
375    /// A list of the locations of each serialized data
376    #[cfg(debug_assertions)]
377    pub debug_locations: String,
378}
379
380/// An error that can occur when trying to take data from the server
381#[derive(Debug)]
382pub enum TakeDataError {
383    /// Deserializing the data failed
384    DeserializationError(ciborium::de::Error<std::io::Error>),
385    /// No data was available
386    DataNotAvailable,
387    /// The server serialized a placeholder for the data, but it isn't available yet
388    DataPending,
389}
390
391impl std::fmt::Display for TakeDataError {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        match self {
394            Self::DeserializationError(e) => write!(f, "DeserializationError: {}", e),
395            Self::DataNotAvailable => write!(f, "DataNotAvailable"),
396            Self::DataPending => write!(f, "DataPending"),
397        }
398    }
399}
400
401impl std::error::Error for TakeDataError {}
402
403/// Create a new entry in the serialize context for the head element hydration
404pub fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
405    serialize_context().create_entry()
406}