dioxus_fullstack_protocol/
lib.rs1#![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#[derive(Default, Clone)]
17pub struct HydrationContext {
18 #[cfg(feature = "web")]
19 suspense_finished: bool,
21 data: Rc<RefCell<HTMLData>>,
22}
23
24impl HydrationContext {
25 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 pub fn serialized(&self) -> SerializedHydrationData {
44 self.data.borrow().serialized()
45 }
46
47 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 pub fn error_entry(&self) -> SerializeContextEntry<Option<CapturedError>> {
60 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 pub fn extend(&self, other: &Self) {
72 self.data.borrow_mut().extend(&other.data.borrow());
73 }
74
75 #[cfg(feature = "web")]
76 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 #[cfg(feature = "web")]
102 if self.suspense_finished {
103 return Err(TakeDataError::DataNotAvailable);
104 }
105 self.data.borrow().get(id)
106 }
107}
108
109pub struct SerializeContextEntry<T> {
112 index: usize,
114 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 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 pub fn get(&self) -> Result<T, TakeDataError>
140 where
141 T: serde::de::DeserializeOwned,
142 {
143 self.context.get(self.index)
144 }
145}
146
147pub fn is_hydrating() -> bool {
149 #[cfg(feature = "web")]
150 {
151 CONTEXT.with(|context| context.borrow().is_some())
153 }
154 #[cfg(not(feature = "web"))]
155 {
156 true
157 }
158}
159
160pub fn serialize_context() -> HydrationContext {
163 #[cfg(feature = "web")]
164 if let Some(current_context) = CONTEXT.with(|context| context.borrow().clone()) {
166 current_context
167 } else {
168 HydrationContext {
170 suspense_finished: true,
171 ..Default::default()
172 }
173 }
174 #[cfg(not(feature = "web"))]
175 {
176 dioxus_core::has_context()
178 .unwrap_or_else(|| dioxus_core::provide_context(HydrationContext::default()))
179 }
180}
181
182pub(crate) struct HTMLData {
183 pub(crate) cursor: usize,
185 pub data: Vec<Option<Vec<u8>>>,
187 #[cfg(debug_assertions)]
193 pub debug_types: Vec<Option<String>>,
194 #[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 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 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 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 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 tracing::error!("Error deserializing data: {:?}", err);
307 Err(TakeDataError::DeserializationError(err))
308 }
309 },
310 None => Err(TakeDataError::DataPending),
311 }
312 }
313
314 pub(crate) fn extend(&mut self, other: &Self) {
316 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 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 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 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 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
388pub struct SerializedHydrationData {
391 pub data: String,
393 #[cfg(debug_assertions)]
395 pub debug_types: String,
396 #[cfg(debug_assertions)]
398 pub debug_locations: String,
399}
400
401#[derive(Debug)]
403pub enum TakeDataError {
404 DeserializationError(ciborium::de::Error<std::io::Error>),
406 DataNotAvailable,
408 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
424pub fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
426 serialize_context().create_entry()
427}