dioxus_fullstack_core/
transport.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// use base64::Engine;
9// use dioxus_core::{CapturedError, Error};
10// use serde::{de::DeserializeOwned, Deserialize, Serialize};
11// use std::{cell::RefCell, io::Cursor, rc::Rc, sync::Arc};
12
13#[cfg(feature = "web")]
14thread_local! {
15    static CONTEXT: RefCell<Option<HydrationContext>> = const { RefCell::new(None) };
16}
17
18/// Data shared between the frontend and the backend for hydration
19/// of server functions.
20#[derive(Default, Clone)]
21pub struct HydrationContext {
22    #[cfg(feature = "web")]
23    /// Is resolving suspense done on the client
24    suspense_finished: bool,
25    data: Rc<RefCell<HTMLData>>,
26}
27
28impl HydrationContext {
29    /// Create a new serialize context from the serialized data
30    pub fn from_serialized(
31        data: &[u8],
32        debug_types: Option<Vec<String>>,
33        debug_locations: Option<Vec<String>>,
34    ) -> Self {
35        Self {
36            #[cfg(feature = "web")]
37            suspense_finished: false,
38            data: Rc::new(RefCell::new(HTMLData::from_serialized(
39                data,
40                debug_types,
41                debug_locations,
42            ))),
43        }
44    }
45
46    /// Serialize the data in the context to be sent to the client
47    pub fn serialized(&self) -> SerializedHydrationData {
48        self.data.borrow().serialized()
49    }
50
51    /// 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.
52    pub fn create_entry<T>(&self) -> SerializeContextEntry<T> {
53        let entry_index = self.data.borrow_mut().create_entry();
54
55        SerializeContextEntry {
56            index: entry_index,
57            context: self.clone(),
58            phantom: std::marker::PhantomData,
59        }
60    }
61
62    /// Get the entry for the error in the suspense boundary
63    pub fn error_entry(&self) -> SerializeContextEntry<Option<CapturedError>> {
64        // The first entry is reserved for the error
65        let entry_index = self.data.borrow_mut().create_entry_with_id(0);
66
67        SerializeContextEntry {
68            index: entry_index,
69            context: self.clone(),
70            phantom: std::marker::PhantomData,
71        }
72    }
73
74    /// Extend this data with the data from another [`HydrationContext`]
75    pub fn extend(&self, other: &Self) {
76        self.data.borrow_mut().extend(&other.data.borrow());
77    }
78
79    #[cfg(feature = "web")]
80    /// Run a closure inside of this context
81    pub fn in_context<T>(&self, f: impl FnOnce() -> T) -> T {
82        CONTEXT.with(|context| {
83            let old = context.borrow().clone();
84            *context.borrow_mut() = Some(self.clone());
85            let result = f();
86            *context.borrow_mut() = old;
87            result
88        })
89    }
90
91    // pub(crate) fn insert<T: Transportable<M>, M: 'static>(
92    pub(crate) fn insert<T: Serialize>(
93        &self,
94        id: usize,
95        value: &T,
96        location: &'static std::panic::Location<'static>,
97    ) {
98        self.data.borrow_mut().insert(id, value, location);
99    }
100
101    // pub(crate) fn get<T: Transportable<M>, M: 'static>(
102    pub(crate) fn get<T: serde::de::DeserializeOwned>(
103        &self,
104        id: usize,
105    ) -> Result<T, TakeDataError> {
106        // If suspense is finished on the client, we can assume that the data is available
107        #[cfg(feature = "web")]
108        if self.suspense_finished {
109            return Err(TakeDataError::DataNotAvailable);
110        }
111        self.data.borrow().get(id)
112    }
113}
114
115/// An entry into the serialized context. The order entries are created in must be consistent
116/// between the server and the client.
117pub struct SerializeContextEntry<T> {
118    /// The index this context will be inserted into inside the serialize context
119    index: usize,
120    /// The context this entry is associated with
121    context: HydrationContext,
122    phantom: std::marker::PhantomData<T>,
123}
124
125impl<T> Clone for SerializeContextEntry<T> {
126    fn clone(&self) -> Self {
127        Self {
128            index: self.index,
129            context: self.context.clone(),
130            phantom: std::marker::PhantomData,
131        }
132    }
133}
134
135impl<T> SerializeContextEntry<T> {
136    /// Insert data into an entry that was created with [`HydrationContext::create_entry`]
137    pub fn insert(&self, value: &T, location: &'static std::panic::Location<'static>)
138    where
139        T: Serialize,
140    {
141        self.context.insert(self.index, value, location);
142    }
143
144    /// Grab the data from the serialize context
145    pub fn get(&self) -> Result<T, TakeDataError>
146    where
147        T: serde::de::DeserializeOwned,
148    {
149        self.context.get(self.index)
150    }
151    // /// Insert data into an entry that was created with [`HydrationContext::create_entry`]
152    // pub fn insert<M: 'static>(self, value: &T, location: &'static std::panic::Location<'static>)
153    // where
154    //     T: Transportable<M>,
155    // {
156    //     self.context.insert(self.index, value, location);
157    // }
158
159    // /// Grab the data from the serialize context
160    // pub fn get<M: 'static>(&self) -> Result<T, TakeDataError>
161    // where
162    //     T: Transportable<M>,
163    // {
164    //     self.context.get(self.index)
165    // }
166}
167
168/// Check if the client is currently rendering a component for hydration. Always returns true on the server.
169pub fn is_hydrating() -> bool {
170    #[cfg(feature = "web")]
171    {
172        // On the client, we can check if the context is set
173        CONTEXT.with(|context| context.borrow().is_some())
174    }
175    #[cfg(not(feature = "web"))]
176    {
177        true
178    }
179}
180
181/// Get or insert the current serialize context. On the client, the hydration context this returns
182/// will always return `TakeDataError::DataNotAvailable` if hydration of the current chunk is finished.
183pub fn serialize_context() -> HydrationContext {
184    #[cfg(feature = "web")]
185    // On the client, the hydration logic provides the context in a global
186    if let Some(current_context) = CONTEXT.with(|context| context.borrow().clone()) {
187        current_context
188    } else {
189        // If the context is not set, then suspense is not active
190        HydrationContext {
191            suspense_finished: true,
192            ..Default::default()
193        }
194    }
195    #[cfg(not(feature = "web"))]
196    {
197        // On the server each scope creates the context lazily
198        dioxus_core::has_context()
199            .unwrap_or_else(|| dioxus_core::provide_context(HydrationContext::default()))
200    }
201}
202
203pub(crate) struct HTMLData {
204    /// The position of the cursor in the data. This is only used on the client
205    pub(crate) cursor: usize,
206    /// The data required for hydration
207    pub data: Vec<Option<Vec<u8>>>,
208    /// The types of each serialized data
209    ///
210    /// NOTE: we don't store this in the main data vec because we don't want to include it in
211    /// release mode and we can't assume both the client and server are built with debug assertions
212    /// matching
213    #[cfg(debug_assertions)]
214    pub debug_types: Vec<Option<String>>,
215    /// The locations of each serialized data
216    #[cfg(debug_assertions)]
217    pub debug_locations: Vec<Option<String>>,
218}
219
220impl Default for HTMLData {
221    fn default() -> Self {
222        Self {
223            cursor: 1,
224            data: Vec::new(),
225            #[cfg(debug_assertions)]
226            debug_types: Vec::new(),
227            #[cfg(debug_assertions)]
228            debug_locations: Vec::new(),
229        }
230    }
231}
232
233impl HTMLData {
234    #[allow(unused)]
235    fn from_serialized(
236        data: &[u8],
237        debug_types: Option<Vec<String>>,
238        debug_locations: Option<Vec<String>>,
239    ) -> Self {
240        let data = ciborium::from_reader(Cursor::new(data)).unwrap();
241        Self {
242            cursor: 1,
243            data,
244            #[cfg(debug_assertions)]
245            debug_types: debug_types
246                .unwrap_or_default()
247                .into_iter()
248                .map(Some)
249                .collect(),
250            #[cfg(debug_assertions)]
251            debug_locations: debug_locations
252                .unwrap_or_default()
253                .into_iter()
254                .map(Some)
255                .collect(),
256        }
257    }
258
259    /// 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.
260    fn create_entry(&mut self) -> usize {
261        let id = self.cursor;
262        self.cursor += 1;
263        self.create_entry_with_id(id)
264    }
265
266    fn create_entry_with_id(&mut self, id: usize) -> usize {
267        while id + 1 > self.data.len() {
268            self.data.push(None);
269            #[cfg(debug_assertions)]
270            {
271                self.debug_types.push(None);
272                self.debug_locations.push(None);
273            }
274        }
275        id
276    }
277
278    /// Insert data into an entry that was created with [`Self::create_entry`]
279    // fn insert<T: Transportable<M>, M: 'static>(
280    fn insert<T: Serialize>(
281        &mut self,
282        id: usize,
283        value: &T,
284        #[allow(unused)] location: &'static std::panic::Location<'static>,
285    ) {
286        // let serialized = value.transport_to_bytes();
287        let mut serialized = Vec::new();
288        ciborium::into_writer(value, &mut serialized).unwrap();
289        self.data[id] = Some(serialized);
290        #[cfg(debug_assertions)]
291        {
292            self.debug_types[id] = Some(std::any::type_name::<T>().to_string());
293            self.debug_locations[id] = Some(location.to_string());
294        }
295    }
296
297    /// Get the data from the serialize context
298    // fn get<T: Transportable<M>, M: 'static>(&self, index: usize) -> Result<T, TakeDataError> {
299    fn get<T: serde::de::DeserializeOwned>(&self, index: usize) -> Result<T, TakeDataError> {
300        if index >= self.data.len() {
301            tracing::trace!(
302                "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",
303                self.data.len(),
304                index
305            );
306            return Err(TakeDataError::DataNotAvailable);
307        }
308        let bytes = self.data[index].as_ref();
309        match bytes {
310            Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
311                // Some(bytes) => match T::transport_from_bytes(bytes) {
312                Ok(x) => Ok(x),
313                Err(err) => {
314                    #[cfg(debug_assertions)]
315                    {
316                        let debug_type = self.debug_types.get(index);
317                        let debug_locations = self.debug_locations.get(index);
318
319                        if let (Some(Some(debug_type)), Some(Some(debug_locations))) =
320                            (debug_type, debug_locations)
321                        {
322                            let client_type = std::any::type_name::<T>();
323                            let client_location = std::panic::Location::caller();
324                            // We we have debug types and a location, we can provide a more helpful error message
325                            tracing::error!(
326                                "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}.",
327                            );
328                            return Err(TakeDataError::DeserializationError(err));
329                        }
330                    }
331                    // Otherwise, just log the generic deserialization error
332                    tracing::error!("Error deserializing data: {:?}", err);
333                    Err(TakeDataError::DeserializationError(err))
334                }
335            },
336            None => Err(TakeDataError::DataPending),
337        }
338    }
339
340    /// Extend this data with the data from another [`HTMLData`]
341    pub(crate) fn extend(&mut self, other: &Self) {
342        // Make sure this vectors error entry exists even if it is empty
343        if self.data.is_empty() {
344            self.data.push(None);
345            #[cfg(debug_assertions)]
346            {
347                self.debug_types.push(None);
348                self.debug_locations.push(None);
349            }
350        }
351
352        let mut other_data_iter = other.data.iter().cloned();
353        #[cfg(debug_assertions)]
354        let mut other_debug_types_iter = other.debug_types.iter().cloned();
355        #[cfg(debug_assertions)]
356        let mut other_debug_locations_iter = other.debug_locations.iter().cloned();
357
358        // Merge the error entry from the other context
359        if let Some(Some(other_error)) = other_data_iter.next() {
360            self.data[0] = Some(other_error.clone());
361            #[cfg(debug_assertions)]
362            {
363                self.debug_types[0] = other_debug_types_iter.next().unwrap_or(None);
364                self.debug_locations[0] = other_debug_locations_iter.next().unwrap_or(None);
365            }
366        }
367
368        // Don't copy the error from the other context
369        self.data.extend(other_data_iter);
370        #[cfg(debug_assertions)]
371        {
372            self.debug_types.extend(other_debug_types_iter);
373            self.debug_locations.extend(other_debug_locations_iter);
374        }
375    }
376
377    /// Encode data as base64. This is intended to be used in the server to send data to the client.
378    pub(crate) fn serialized(&self) -> SerializedHydrationData {
379        let mut serialized = Vec::new();
380        ciborium::into_writer(&self.data, &mut serialized).unwrap();
381
382        let data = base64::engine::general_purpose::STANDARD.encode(serialized);
383
384        #[cfg(debug_assertions)]
385        let format_js_list_of_strings = |list: &[Option<String>]| {
386            let body = list
387                .iter()
388                .map(|s| match s {
389                    Some(s) => {
390                        // Escape backslashes, quotes, and newlines
391                        let escaped = s
392                            .replace(r#"\"#, r#"\\"#)
393                            .replace("\n", r#"\n"#)
394                            .replace(r#"""#, r#"\""#);
395
396                        format!(r#""{escaped}""#)
397                    }
398                    None => r#""unknown""#.to_string(),
399                })
400                .collect::<Vec<_>>()
401                .join(",");
402            format!("[{}]", body)
403        };
404
405        SerializedHydrationData {
406            data,
407            #[cfg(debug_assertions)]
408            debug_types: format_js_list_of_strings(&self.debug_types),
409            #[cfg(debug_assertions)]
410            debug_locations: format_js_list_of_strings(&self.debug_locations),
411        }
412    }
413}
414
415/// Data that was serialized on the server for hydration on the client. This includes
416/// extra information about the types and sources of the serialized data in debug mode
417pub struct SerializedHydrationData {
418    /// The base64 encoded serialized data
419    pub data: String,
420    /// A list of the types of each serialized data
421    #[cfg(debug_assertions)]
422    pub debug_types: String,
423    /// A list of the locations of each serialized data
424    #[cfg(debug_assertions)]
425    pub debug_locations: String,
426}
427
428/// An error that can occur when trying to take data from the server
429#[derive(Debug)]
430pub enum TakeDataError {
431    /// Deserializing the data failed
432    DeserializationError(ciborium::de::Error<std::io::Error>),
433    /// No data was available
434    DataNotAvailable,
435    /// The server serialized a placeholder for the data, but it isn't available yet
436    DataPending,
437}
438
439impl std::fmt::Display for TakeDataError {
440    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441        match self {
442            Self::DeserializationError(e) => write!(f, "DeserializationError: {}", e),
443            Self::DataNotAvailable => write!(f, "DataNotAvailable"),
444            Self::DataPending => write!(f, "DataPending"),
445        }
446    }
447}
448
449impl std::error::Error for TakeDataError {}
450
451/// Create a new entry in the serialize context for the head element hydration
452pub fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
453    serialize_context().create_entry()
454}
455
456// /// An error that can occur when trying to take data from the server
457// #[derive(thiserror::Error, Debug)]
458// pub enum TakeDataError {
459//     /// Deserializing the data failed
460//     #[error("DeserializationError: {0}")]
461//     DeserializationError(ciborium::de::Error<std::io::Error>),
462
463//     /// No data was available
464//     #[error("DataNotAvailable")]
465//     DataNotAvailable,
466
467//     /// The server serialized a placeholder for the data, but it isn't available yet
468//     #[error("DataPending")]
469//     DataPending,
470// }
471
472// /// A `Transportable` type can be safely transported from the server to the client, and be used for
473// /// hydration. Not all types can sensibly be transported, but many can. This trait makes it possible
474// /// to customize how types are transported which helps for non-serializable types like `dioxus_core::Error`.
475// ///
476// /// By default, all types that implement `Serialize` and `DeserializeOwned` are transportable.
477// ///
478// /// You can also implement `Transportable` for `Result<T, dioxus_core::Error>` where `T` is
479// /// `Serialize` and `DeserializeOwned` to allow transporting results that may contain errors.
480// ///
481// /// Note that transporting a `Result<T, dioxus_core::Error>` will lose various aspects of the original
482// /// `dioxus_core::Error` such as backtraces and source errors, but will preserve the error message.
483// pub trait Transportable<M = ()>: 'static {
484//     /// Serialize the type to a byte vector for transport
485//     fn transport_to_bytes(&self) -> Vec<u8>;
486
487//     /// Deserialize the type from a byte slice
488//     fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
489//     where
490//         Self: Sized;
491// }
492
493// impl<T> Transportable<()> for T
494// where
495//     T: Serialize + DeserializeOwned + 'static,
496// {
497//     fn transport_to_bytes(&self) -> Vec<u8> {
498//         let mut serialized = Vec::new();
499//         ciborium::into_writer(self, &mut serialized).unwrap();
500//         serialized
501//     }
502
503//     fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
504//     where
505//         Self: Sized,
506//     {
507//         ciborium::from_reader(Cursor::new(bytes))
508//     }
509// }
510
511// #[derive(Serialize, Deserialize)]
512// struct TransportResultErr<T> {
513//     error: Result<T, CapturedError>,
514// }
515
516// #[doc(hidden)]
517// pub struct TransportViaErrMarker;
518
519// impl<T> Transportable<TransportViaErrMarker> for Result<T, dioxus_core::Error>
520// where
521//     T: Serialize + DeserializeOwned + 'static,
522// {
523//     fn transport_to_bytes(&self) -> Vec<u8> {
524//         let err = TransportResultErr {
525//             error: self
526//                 .as_ref()
527//                 .map_err(|e| CapturedError::from_display(e.to_string())),
528//         };
529
530//         let mut serialized = Vec::new();
531//         ciborium::into_writer(&err, &mut serialized).unwrap();
532//         serialized
533//     }
534
535//     fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
536//     where
537//         Self: Sized,
538//     {
539//         let err: TransportResultErr<T> = ciborium::from_reader(Cursor::new(bytes))?;
540//         match err.error {
541//             Ok(value) => Ok(Ok(value)),
542//             Err(captured) => Ok(Err(dioxus_core::Error::msg(captured.to_string()))),
543//         }
544//     }
545// }
546
547// #[doc(hidden)]
548// pub struct TransportCapturedError;
549// #[derive(Serialize, Deserialize)]
550// struct TransportError {
551//     error: String,
552// }
553// impl Transportable<TransportCapturedError> for CapturedError {
554//     fn transport_to_bytes(&self) -> Vec<u8> {
555//         let err = TransportError {
556//             error: self.to_string(),
557//         };
558
559//         let mut serialized = Vec::new();
560//         ciborium::into_writer(&err, &mut serialized).unwrap();
561//         serialized
562//     }
563
564//     fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
565//     where
566//         Self: Sized,
567//     {
568//         let err: TransportError = ciborium::from_reader(Cursor::new(bytes))?;
569//         Ok(CapturedError(Arc::new(Error::msg::<String>(err.error))))
570//     }
571// }