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// }