mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
ErrorBoundary SSR and serialization of errors to support hydration
This commit is contained in:
parent
851e1f73fd
commit
42b99dd912
16 changed files with 377 additions and 66 deletions
|
@ -1,6 +1,7 @@
|
|||
use std::{
|
||||
cell::RefCell,
|
||||
error, fmt,
|
||||
error,
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
ops,
|
||||
pin::Pin,
|
||||
|
@ -69,6 +70,18 @@ pub trait ErrorHook: Send + Sync {
|
|||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
|
||||
pub struct ErrorId(usize);
|
||||
|
||||
impl Display for ErrorId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for ErrorId {
|
||||
fn from(value: usize) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static ERROR_HOOK: RefCell<Option<Arc<dyn ErrorHook>>> = RefCell::new(None);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ use leptos::{component, prelude::*, signal, view, ErrorBoundary, IntoView};
|
|||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (value, set_value) = signal("".parse::<i32>());
|
||||
let guard = value.read();
|
||||
|
||||
view! {
|
||||
<h1>"Error Handling"</h1>
|
||||
|
@ -11,42 +10,41 @@ pub fn App() -> impl IntoView {
|
|||
"Type a number (or something that's not a number!)"
|
||||
<input
|
||||
type="text"
|
||||
value=move || value.get().map(|n| n.to_string()).unwrap_or_default()// TODO guard support here
|
||||
value=move || value.get().unwrap_or_default()
|
||||
// when input changes, try to parse a number from the input
|
||||
on:input:target=move |ev| set_value.set(ev.target().value().parse::<i32>())
|
||||
/>
|
||||
|
||||
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
|
||||
/> // If an `Err(_) has been rendered inside the <ErrorBoundary/>,
|
||||
// the fallback will be displayed. Otherwise, the children of the
|
||||
// <ErrorBoundary/> will be displayed.
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|errors| {
|
||||
let errors = errors.clone();
|
||||
view! {
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors
|
||||
// as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
|
||||
// the fallback receives a signal containing current errors
|
||||
<ErrorBoundary fallback=|errors| {
|
||||
let errors = errors.clone();
|
||||
view! {
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors
|
||||
// as strings, if we'd like
|
||||
<ul>
|
||||
{move || {
|
||||
errors
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
}>
|
||||
|
||||
<p>
|
||||
"You entered "
|
||||
// because `value` is `Result<i32, _>`,
|
||||
// it will render the `i32` if it is `Ok`,
|
||||
"You entered " // it will render the `i32` if it is `Ok`,
|
||||
// and render nothing and trigger the error boundary
|
||||
// if it is `Err`. It's a signal, so this will dynamically
|
||||
// update when `value` changes
|
||||
<strong>{move || value.get()}</strong> // TODO render signal directly
|
||||
<strong>{value}</strong>
|
||||
</p>
|
||||
</ErrorBoundary>
|
||||
</label>
|
||||
|
|
Binary file not shown.
|
@ -118,9 +118,7 @@ pub fn Todos() -> impl IntoView {
|
|||
|
||||
view! {
|
||||
<div>
|
||||
|
||||
// fallback=move || view! { <p>"Loading..."</p> }>
|
||||
<Suspense fallback=move || view! { <p>"Loading..."</p> }>
|
||||
<Transition fallback=move || view! { <p>"Loading..."</p> }>
|
||||
<ErrorBoundary fallback=|errors| view! { <ErrorTemplate errors/> }>
|
||||
// {existing_todos}
|
||||
<ul>
|
||||
|
@ -156,7 +154,7 @@ pub fn Todos() -> impl IntoView {
|
|||
// {pending_todos}
|
||||
</ul>
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -70,7 +70,11 @@ use server_fn::{
|
|||
,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet, fmt::Debug, io, pin::Pin, sync::Arc,
|
||||
collections::HashSet,
|
||||
fmt::{Debug, Write},
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
thread::available_parallelism,
|
||||
};
|
||||
use tracing::Instrument;
|
||||
|
@ -739,11 +743,13 @@ where
|
|||
// TODO nonce
|
||||
|
||||
let shared_context = Owner::current_shared_context().unwrap();
|
||||
let shared_context = shared_context
|
||||
.pending_data()
|
||||
.unwrap()
|
||||
.map(|chunk| format!("<script>{chunk}</script>"));
|
||||
futures::stream::select(app_stream, shared_context)
|
||||
let chunks = Box::pin(
|
||||
shared_context
|
||||
.pending_data()
|
||||
.unwrap()
|
||||
.map(|chunk| format!("<script>{chunk}</script>")),
|
||||
);
|
||||
futures::stream::select(app_stream, chunks)
|
||||
});
|
||||
|
||||
let stream = meta_context.inject_meta_context(stream).await;
|
||||
|
|
|
@ -14,7 +14,7 @@ any_error = { workspace = true }
|
|||
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
cfg-if = "1"
|
||||
hydration_context = { workspace = true, optional = true }
|
||||
hydration_context = { workspace = true }
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
|
@ -54,7 +54,6 @@ futures = "0.3.30"
|
|||
default = ["serde"]
|
||||
hydration = [
|
||||
"leptos_server/hydration",
|
||||
"dep:hydration_context",
|
||||
"hydration_context/browser",
|
||||
]
|
||||
csr = ["leptos_macro/csr", "leptos_reactive/csr"]
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
use crate::{children::TypedChildren, IntoView};
|
||||
use any_error::{Error, ErrorHook, ErrorId};
|
||||
use hydration_context::{SerializedDataId, SharedContext};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::Effect;
|
||||
use reactive_graph::{
|
||||
computed::ArcMemo,
|
||||
owner::Owner,
|
||||
signal::ArcRwSignal,
|
||||
traits::{Get, Update, With},
|
||||
traits::{Get, Update, With, WithUntracked},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
use tachys::{
|
||||
hydration::Cursor,
|
||||
renderer::Renderer,
|
||||
renderer::{CastFrom, Renderer},
|
||||
ssr::StreamBuilder,
|
||||
view::{Mountable, Position, PositionState, Render, RenderHtml},
|
||||
};
|
||||
|
||||
|
@ -49,7 +51,15 @@ where
|
|||
Fal: IntoView + 'static,
|
||||
Chil: IntoView + 'static,
|
||||
{
|
||||
let hook = Arc::new(ErrorBoundaryErrorHook::default());
|
||||
let sc = Owner::current_shared_context();
|
||||
let boundary_id = sc.as_ref().map(|sc| sc.next_id()).unwrap_or_default();
|
||||
let initial_errors =
|
||||
sc.map(|sc| sc.errors(&boundary_id)).unwrap_or_default();
|
||||
|
||||
let hook = Arc::new(ErrorBoundaryErrorHook::new(
|
||||
boundary_id.clone(),
|
||||
initial_errors,
|
||||
));
|
||||
let errors = hook.errors.clone();
|
||||
let errors_empty = ArcMemo::new({
|
||||
let errors = errors.clone();
|
||||
|
@ -62,18 +72,22 @@ where
|
|||
let mut children = Some(children.into_inner()());
|
||||
|
||||
move || ErrorBoundaryView {
|
||||
boundary_id: boundary_id.clone(),
|
||||
errors_empty: errors_empty.get(),
|
||||
children: children.take(),
|
||||
fallback: Some((fallback.clone())(&errors)),
|
||||
errors: errors.clone(),
|
||||
rndr: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ErrorBoundaryView<Chil, Fal, Rndr> {
|
||||
boundary_id: SerializedDataId,
|
||||
errors_empty: bool,
|
||||
children: Option<Chil>,
|
||||
fallback: Fal,
|
||||
errors: ArcRwSignal<Errors>,
|
||||
rndr: PhantomData<Rndr>,
|
||||
}
|
||||
|
||||
|
@ -203,9 +217,11 @@ where
|
|||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
let ErrorBoundaryView {
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children,
|
||||
fallback,
|
||||
errors,
|
||||
..
|
||||
} = self;
|
||||
let children = match children {
|
||||
|
@ -213,15 +229,52 @@ where
|
|||
Some(children) => Some(children.resolve().await),
|
||||
};
|
||||
ErrorBoundaryView {
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children,
|
||||
fallback,
|
||||
errors,
|
||||
rndr: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||
todo!()
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let mut new_buf = String::with_capacity(Chil::MIN_LENGTH);
|
||||
let mut new_pos = *position;
|
||||
self.children.to_html_with_buf(&mut new_buf, &mut new_pos);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
if self.errors.with_untracked(|map| map.is_empty()) {
|
||||
buf.push_str(&new_buf);
|
||||
} else {
|
||||
// otherwise, serialize the fallback instead
|
||||
self.fallback.to_html_with_buf(buf, position);
|
||||
}
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
self,
|
||||
buf: &mut StreamBuilder,
|
||||
position: &mut Position,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let mut new_buf = StreamBuilder::new(buf.clone_id());
|
||||
let mut new_pos = *position;
|
||||
self.children
|
||||
.to_html_async_with_buf::<OUT_OF_ORDER>(&mut new_buf, &mut new_pos);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
if self.errors.with_untracked(|map| map.is_empty()) {
|
||||
buf.append(new_buf);
|
||||
} else {
|
||||
// otherwise, serialize the fallback instead
|
||||
let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
|
||||
self.fallback.to_html_with_buf(&mut fallback, position);
|
||||
buf.push_sync(&fallback);
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
|
@ -229,21 +282,75 @@ where
|
|||
cursor: &Cursor<Rndr>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
todo!()
|
||||
let children = self.children.expect(
|
||||
"tried to hydrate ErrorBoundary but children were not present",
|
||||
);
|
||||
let (children, fallback) = if self.errors_empty {
|
||||
(
|
||||
children.hydrate::<FROM_SERVER>(cursor, position),
|
||||
self.fallback.build(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
children.build(),
|
||||
self.fallback.hydrate::<FROM_SERVER>(cursor, position),
|
||||
)
|
||||
};
|
||||
|
||||
cursor.sibling();
|
||||
let placeholder = cursor.current().to_owned();
|
||||
let placeholder = Rndr::Placeholder::cast_from(placeholder).unwrap();
|
||||
position.set(Position::NextChild);
|
||||
|
||||
ErrorBoundaryViewState {
|
||||
showing_fallback: !self.errors_empty,
|
||||
children,
|
||||
fallback,
|
||||
placeholder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
struct ErrorBoundaryErrorHook {
|
||||
errors: ArcRwSignal<Errors>,
|
||||
id: SerializedDataId,
|
||||
shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl ErrorBoundaryErrorHook {
|
||||
pub fn new(
|
||||
id: SerializedDataId,
|
||||
initial_errors: impl IntoIterator<Item = (ErrorId, Error)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
errors: ArcRwSignal::new(Errors(
|
||||
initial_errors.into_iter().collect(),
|
||||
)),
|
||||
id,
|
||||
shared_context: Owner::current_shared_context(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorHook for ErrorBoundaryErrorHook {
|
||||
fn throw(&self, error: Error) -> ErrorId {
|
||||
let key = ErrorId::default();
|
||||
// generate a unique ID
|
||||
let key = ErrorId::default(); // TODO unique ID...
|
||||
|
||||
// register it with the shared context, so that it can be serialized from server to client
|
||||
// as needed
|
||||
if let Some(sc) = &self.shared_context {
|
||||
sc.register_error(self.id.clone(), key.clone(), error.clone());
|
||||
}
|
||||
|
||||
// add it to the reactive map of errors
|
||||
self.errors.update(|map| {
|
||||
map.insert(key.clone(), error);
|
||||
});
|
||||
|
||||
// return the key, which will be owned by the Result being rendered and can be used to
|
||||
// unregister this error if it is rebuilt
|
||||
key
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::{
|
|||
into_view::View,
|
||||
IntoView,
|
||||
};
|
||||
use any_error::ErrorHookFuture;
|
||||
use any_spawner::Executor;
|
||||
use futures::FutureExt;
|
||||
use leptos_macro::component;
|
||||
|
@ -141,7 +142,9 @@ where
|
|||
{
|
||||
buf.next_id();
|
||||
|
||||
let mut fut = Box::pin(self.children.resolve());
|
||||
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
|
||||
self.children.resolve(),
|
||||
)));
|
||||
match fut.as_mut().now_or_never() {
|
||||
Some(resolved) => {
|
||||
Either::<Fal, _>::Right(resolved)
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
html::{
|
||||
attribute::*,
|
||||
class::{class, Class, IntoClass},
|
||||
element::ElementType,
|
||||
element::{ElementType, HasElementType},
|
||||
event::{on, on_target, EventDescriptor, On, Targeted},
|
||||
property::{property, IntoProperty, Property},
|
||||
style::{style, IntoStyle, Style},
|
||||
|
@ -112,13 +112,13 @@ pub trait OnTargetAttribute<E, F, T, Rndr> {
|
|||
|
||||
impl<T, E, F, Rndr> OnTargetAttribute<E, F, Self, Rndr> for T
|
||||
where
|
||||
Self: ElementType,
|
||||
T: AddAnyAttr<Rndr>,
|
||||
T: AddAnyAttr<Rndr> + HasElementType,
|
||||
E: EventDescriptor + Send + 'static,
|
||||
E::EventType: 'static,
|
||||
E::EventType: From<Rndr::Event>,
|
||||
F: FnMut(Targeted<E::EventType, <Self as ElementType>::Output, Rndr>)
|
||||
+ 'static,
|
||||
F: FnMut(
|
||||
Targeted<E::EventType, <Self as HasElementType>::ElementType, Rndr>,
|
||||
) + 'static,
|
||||
Rndr: DomRenderer,
|
||||
{
|
||||
type Output = <Self as AddAnyAttr<Rndr>>::Output<On<Rndr>>;
|
||||
|
|
|
@ -156,12 +156,23 @@ pub trait ElementType: Send {
|
|||
fn tag(&self) -> &str;
|
||||
}
|
||||
|
||||
pub trait HasElementType {
|
||||
type ElementType;
|
||||
}
|
||||
|
||||
pub trait ElementWithChildren {}
|
||||
|
||||
pub trait CreateElement<R: Renderer> {
|
||||
fn create_element(&self) -> R::Element;
|
||||
}
|
||||
|
||||
impl<E, At, Ch, Rndr> HasElementType for HtmlElement<E, At, Ch, Rndr>
|
||||
where
|
||||
E: ElementType,
|
||||
{
|
||||
type ElementType = E::Output;
|
||||
}
|
||||
|
||||
impl<E, At, Ch, Rndr> Render<Rndr> for HtmlElement<E, At, Ch, Rndr>
|
||||
where
|
||||
E: CreateElement<Rndr>,
|
||||
|
|
|
@ -161,8 +161,11 @@ where
|
|||
buf: &mut String,
|
||||
position: &mut super::Position,
|
||||
) {
|
||||
if let Ok(inner) = self {
|
||||
inner.to_html_with_buf(buf, position);
|
||||
match self {
|
||||
Ok(inner) => inner.to_html_with_buf(buf, position),
|
||||
Err(e) => {
|
||||
any_error::throw(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,8 +176,13 @@ where
|
|||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
if let Ok(inner) = self {
|
||||
inner.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position);
|
||||
match self {
|
||||
Ok(inner) => {
|
||||
inner.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position)
|
||||
}
|
||||
Err(e) => {
|
||||
any_error::throw(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue