mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
ErrorBoundary SSR and serialization of errors to support hydration
This commit is contained in:
@@ -4,11 +4,13 @@ edition = "2021"
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
any_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[features]
|
||||
browser = ["dep:wasm-bindgen", "dep:js-sys"]
|
||||
|
||||
@@ -41,4 +41,28 @@ impl SharedContext for CsrSharedContext {
|
||||
|
||||
#[inline(always)]
|
||||
fn set_is_hydrating(&self, _is_hydrating: bool) {}
|
||||
|
||||
#[inline(always)]
|
||||
fn errors(
|
||||
&self,
|
||||
_boundary_id: &SerializedDataId,
|
||||
) -> Vec<(any_error::ErrorId, any_error::Error)> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn take_errors(
|
||||
&self,
|
||||
) -> Vec<(SerializedDataId, any_error::ErrorId, any_error::Error)> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn register_error(
|
||||
&self,
|
||||
_error_boundary: SerializedDataId,
|
||||
_error_id: any_error::ErrorId,
|
||||
_error: any_error::Error,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
use super::{SerializedDataId, SharedContext};
|
||||
use crate::{PinnedFuture, PinnedStream};
|
||||
use any_error::{Error, ErrorId};
|
||||
use core::fmt::Debug;
|
||||
use js_sys::Array;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
};
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsCast};
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
static __RESOLVED_RESOURCES: Array;
|
||||
|
||||
static __SERIALIZED_ERRORS: Array;
|
||||
}
|
||||
|
||||
fn serialized_errors() -> Vec<(SerializedDataId, ErrorId, Error)> {
|
||||
__SERIALIZED_ERRORS
|
||||
.iter()
|
||||
.flat_map(|value| {
|
||||
value.dyn_ref::<Array>().map(|value| {
|
||||
let error_boundary_id = value.get(0).as_f64().unwrap() as usize;
|
||||
let error_id = value.get(1).as_f64().unwrap() as usize;
|
||||
let value = value
|
||||
.get(2)
|
||||
.as_string()
|
||||
.expect("Expected a [number, string] tuple");
|
||||
(
|
||||
SerializedDataId(error_boundary_id),
|
||||
ErrorId::from(error_id),
|
||||
Error::from(SerializedError(value)),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// An error that has been serialized across the network boundary.
|
||||
#[derive(Debug, Clone)]
|
||||
struct SerializedError(String);
|
||||
|
||||
impl Display for SerializedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SerializedError {}
|
||||
|
||||
#[derive(Default)]
|
||||
/// The shared context that should be used in the browser while hydrating.
|
||||
pub struct HydrateSharedContext {
|
||||
id: AtomicUsize,
|
||||
is_hydrating: AtomicBool,
|
||||
errors: Lazy<Vec<(SerializedDataId, ErrorId, Error)>>,
|
||||
}
|
||||
|
||||
impl HydrateSharedContext {
|
||||
@@ -23,6 +64,7 @@ impl HydrateSharedContext {
|
||||
Self {
|
||||
id: AtomicUsize::new(0),
|
||||
is_hydrating: AtomicBool::new(true),
|
||||
errors: Lazy::new(serialized_errors),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +76,7 @@ impl HydrateSharedContext {
|
||||
Self {
|
||||
id: AtomicUsize::new(0),
|
||||
is_hydrating: AtomicBool::new(false),
|
||||
errors: Lazy::new(serialized_errors),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +116,32 @@ impl SharedContext for HydrateSharedContext {
|
||||
}
|
||||
|
||||
fn set_is_hydrating(&self, is_hydrating: bool) {
|
||||
self.is_hydrating.store(true, Ordering::Relaxed)
|
||||
self.is_hydrating.store(is_hydrating, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn errors(&self, boundary_id: &SerializedDataId) -> Vec<(ErrorId, Error)> {
|
||||
self.errors
|
||||
.iter()
|
||||
.filter_map(|(boundary, id, error)| {
|
||||
if boundary == boundary_id {
|
||||
Some((id.clone(), error.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn register_error(
|
||||
&self,
|
||||
_error_boundary: SerializedDataId,
|
||||
_error_id: ErrorId,
|
||||
_error: Error,
|
||||
) {
|
||||
}
|
||||
|
||||
fn take_errors(&self) -> Vec<(SerializedDataId, ErrorId, Error)> {
|
||||
self.errors.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ mod csr;
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "browser")))]
|
||||
mod hydrate;
|
||||
mod ssr;
|
||||
use any_error::{Error, ErrorId};
|
||||
#[cfg(feature = "browser")]
|
||||
pub use csr::*;
|
||||
use futures::Stream;
|
||||
@@ -96,4 +97,18 @@ pub trait SharedContext: Debug {
|
||||
/// For example, in an app with "islands," this should be `true` inside islands and
|
||||
/// false elsewhere.
|
||||
fn set_is_hydrating(&self, is_hydrating: bool);
|
||||
|
||||
/// Returns all errors that have been registered, removing them from the list.
|
||||
fn take_errors(&self) -> Vec<(SerializedDataId, ErrorId, Error)>;
|
||||
|
||||
/// Returns the set of errors that have been registered with a particular boundary.
|
||||
fn errors(&self, boundary_id: &SerializedDataId) -> Vec<(ErrorId, Error)>;
|
||||
|
||||
/// Registers an error with the context to be shared from server to client.
|
||||
fn register_error(
|
||||
&self,
|
||||
error_boundary: SerializedDataId,
|
||||
error_id: ErrorId,
|
||||
error: Error,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{SerializedDataId, SharedContext};
|
||||
use crate::{PinnedFuture, PinnedStream};
|
||||
use any_error::{Error, ErrorId};
|
||||
use futures::{
|
||||
stream::{self, FuturesUnordered},
|
||||
StreamExt,
|
||||
@@ -10,7 +11,7 @@ use std::{
|
||||
mem,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
RwLock,
|
||||
Arc, RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +22,7 @@ pub struct SsrSharedContext {
|
||||
is_hydrating: AtomicBool,
|
||||
sync_buf: RwLock<Vec<ResolvedData>>,
|
||||
async_buf: RwLock<Vec<(SerializedDataId, PinnedFuture<String>)>>,
|
||||
errors: Arc<RwLock<Vec<(SerializedDataId, ErrorId, Error)>>>,
|
||||
}
|
||||
|
||||
impl SsrSharedContext {
|
||||
@@ -75,7 +77,7 @@ impl SharedContext for SsrSharedContext {
|
||||
|
||||
// 1) initial, synchronous setup chunk
|
||||
let mut initial_chunk = String::new();
|
||||
// resolved synchronous resources
|
||||
// resolved synchronous resources and errors
|
||||
initial_chunk.push_str("__RESOLVED_RESOURCES=[");
|
||||
for resolved in sync_data {
|
||||
resolved.write_to_buf(&mut initial_chunk);
|
||||
@@ -83,6 +85,16 @@ impl SharedContext for SsrSharedContext {
|
||||
}
|
||||
initial_chunk.push_str("];");
|
||||
|
||||
initial_chunk.push_str("__SERIALIZED_ERRORS=[");
|
||||
for error in mem::take(&mut *self.errors.write().or_poisoned()) {
|
||||
write!(
|
||||
initial_chunk,
|
||||
"[{}, {}, {:?}],",
|
||||
error.0 .0, error.1, error.2
|
||||
);
|
||||
}
|
||||
initial_chunk.push_str("];");
|
||||
|
||||
// pending async resources
|
||||
initial_chunk.push_str("__PENDING_RESOURCES=[");
|
||||
for (id, _) in &async_data {
|
||||
@@ -96,10 +108,24 @@ impl SharedContext for SsrSharedContext {
|
||||
// 2) async resources as they resolve
|
||||
let async_data = async_data
|
||||
.into_iter()
|
||||
.map(|(id, data)| async move {
|
||||
let data = data.await;
|
||||
let data = data.replace('<', "\\u003c");
|
||||
format!("__RESOLVED_RESOURCES[{}] = {:?};", id.0, data)
|
||||
.map(|(id, data)| {
|
||||
let errors = Arc::clone(&self.errors);
|
||||
async move {
|
||||
let data = data.await;
|
||||
let data = data.replace('<', "\\u003c");
|
||||
let mut val =
|
||||
format!("__RESOLVED_RESOURCES[{}] = {:?};", id.0, data);
|
||||
for error in mem::take(&mut *errors.write().or_poisoned()) {
|
||||
write!(
|
||||
val,
|
||||
"__SERIALIZED_ERRORS.push([{}, {}, {:?}]);",
|
||||
error.0 .0,
|
||||
error.1,
|
||||
error.2.to_string()
|
||||
);
|
||||
}
|
||||
val
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
@@ -123,6 +149,38 @@ impl SharedContext for SsrSharedContext {
|
||||
fn set_is_hydrating(&self, is_hydrating: bool) {
|
||||
self.is_hydrating.store(is_hydrating, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn errors(&self, boundary_id: &SerializedDataId) -> Vec<(ErrorId, Error)> {
|
||||
self.errors
|
||||
.read()
|
||||
.or_poisoned()
|
||||
.iter()
|
||||
.filter_map(|(boundary, id, error)| {
|
||||
if boundary == boundary_id {
|
||||
Some((id.clone(), error.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn register_error(
|
||||
&self,
|
||||
error_boundary_id: SerializedDataId,
|
||||
error_id: ErrorId,
|
||||
error: Error,
|
||||
) {
|
||||
self.errors.write().or_poisoned().push((
|
||||
error_boundary_id,
|
||||
error_id,
|
||||
error,
|
||||
));
|
||||
}
|
||||
|
||||
fn take_errors(&self) -> Vec<(SerializedDataId, ErrorId, Error)> {
|
||||
mem::take(&mut *self.errors.write().or_poisoned())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
Reference in New Issue
Block a user