fix: unique IDs and correct hydration for <ErrorBoundary/> (closes #2704)

This commit is contained in:
Greg Johnston 2024-07-25 12:20:01 -04:00
parent 25bfc27544
commit d9b590b8e0
5 changed files with 58 additions and 16 deletions

View file

@ -1,7 +1,7 @@
[package]
name = "throw_error"
edition = "2021"
version = "0.1.0"
version = "0.2.0-beta"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View file

@ -9,7 +9,7 @@ use std::{
error,
fmt::{self, Display},
future::Future,
ops,
mem, ops,
pin::Pin,
sync::Arc,
task::{Context, Poll},
@ -92,9 +92,25 @@ thread_local! {
static ERROR_HOOK: RefCell<Option<Arc<dyn ErrorHook>>> = RefCell::new(None);
}
/// Resets the error hook to its previous state when dropped.
pub struct ResetErrorHookOnDrop(Option<Arc<dyn ErrorHook>>);
impl Drop for ResetErrorHookOnDrop {
fn drop(&mut self) {
ERROR_HOOK.with_borrow_mut(|this| *this = self.0.take())
}
}
/// Returns the current error hook.
pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
ERROR_HOOK.with_borrow(Clone::clone)
}
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) {
ERROR_HOOK.with_borrow_mut(|this| *this = Some(hook))
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
ResetErrorHookOnDrop(
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
)
}
/// Invokes the error hook set by [`set_error_hook`] with the given error.
@ -140,9 +156,10 @@ where
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
if let Some(hook) = &this.hook {
set_error_hook(Arc::clone(hook))
}
let _hook = this
.hook
.as_ref()
.map(|hook| set_error_hook(Arc::clone(hook)));
this.inner.poll(cx)
}
}

View file

@ -44,6 +44,12 @@ pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
/// from the server to the client.
pub struct SerializedDataId(usize);
impl From<SerializedDataId> for ErrorId {
fn from(value: SerializedDataId) -> Self {
value.0.into()
}
}
/// Information that will be shared between the server and the client.
pub trait SharedContext: Debug {
/// Whether the application is running in the browser.

View file

@ -9,7 +9,7 @@ use reactive_graph::{
traits::{Get, Update, With, WithUntracked},
};
use rustc_hash::FxHashMap;
use std::{marker::PhantomData, sync::Arc};
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
@ -72,12 +72,11 @@ where
});
let hook = hook as Arc<dyn ErrorHook>;
// provide the error hook and render children
// TODO unset this outside the ErrorBoundary
throw_error::set_error_hook(Arc::clone(&hook));
let _guard = throw_error::set_error_hook(Arc::clone(&hook));
let children = children.into_inner()();
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
@ -87,8 +86,8 @@ where
}
}
#[derive(Debug)]
struct ErrorBoundaryView<Chil, FalFn, Rndr> {
hook: Arc<dyn ErrorHook>,
boundary_id: SerializedDataId,
errors_empty: ArcMemo<bool>,
children: Chil,
@ -146,10 +145,12 @@ where
fn build(mut self) -> Self::State {
let mut children = Some(self.children.build());
let hook = Arc::clone(&self.hook);
RenderEffect::new(
move |prev: Option<
ErrorBoundaryViewState<Chil::State, Fal::State>,
>| {
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
if let Some(mut state) = prev {
match (self.errors_empty.get(), &mut state.fallback) {
// no errors, and was showing fallback
@ -216,6 +217,7 @@ where
Self::Output<NewAttr>: RenderHtml<Rndr>,
{
let ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
@ -224,6 +226,7 @@ where
rndr,
} = self;
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children: children.add_any_attr(attr.into_cloneable_owned()),
@ -252,6 +255,7 @@ where
async fn resolve(self) -> Self::AsyncOutput {
let ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
@ -260,6 +264,7 @@ where
..
} = self;
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children: children.resolve().await,
@ -277,6 +282,7 @@ where
mark_branches: bool,
) {
// first, attempt to serialize the children to HTML, then check for errors
let _hook = throw_error::set_error_hook(self.hook);
let mut new_buf = String::with_capacity(Chil::MIN_LENGTH);
let mut new_pos = *position;
self.children.to_html_with_buf(
@ -309,6 +315,7 @@ where
) where
Self: Sized,
{
let hook = throw_error::set_error_hook(self.hook);
// 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;
@ -345,12 +352,14 @@ where
position: &PositionState,
) -> Self::State {
let mut children = Some(self.children);
let hook = Arc::clone(&self.hook);
let cursor = cursor.to_owned();
let position = position.to_owned();
RenderEffect::new(
move |prev: Option<
ErrorBoundaryViewState<Chil::State, Fal::State>,
>| {
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
if let Some(mut state) = prev {
match (self.errors_empty.get(), &mut state.fallback) {
// no errors, and was showing fallback
@ -424,7 +433,10 @@ impl ErrorBoundaryErrorHook {
impl ErrorHook for ErrorBoundaryErrorHook {
fn throw(&self, error: Error) -> ErrorId {
// generate a unique ID
let key = ErrorId::default(); // TODO unique ID...
let key: ErrorId = Owner::current_shared_context()
.map(|sc| sc.next_id())
.unwrap_or_default()
.into();
// register it with the shared context, so that it can be serialized from server to client
// as needed

View file

@ -6,7 +6,8 @@ use crate::{
view::{iterators::OptionState, Mountable, Render, Renderer},
};
use either_of::Either;
use throw_error::Error as AnyError;
use std::sync::Arc;
use throw_error::{Error as AnyError, ErrorHook};
impl<R, T, E> Render<R> for Result<T, E>
where
@ -17,6 +18,7 @@ where
type State = ResultState<T, R>;
fn build(self) -> Self::State {
let hook = throw_error::get_error_hook();
let (state, error) = match self {
Ok(view) => (Either::Left(view.build()), None),
Err(e) => (
@ -24,10 +26,11 @@ where
Some(throw_error::throw(e.into())),
),
};
ResultState { state, error }
ResultState { state, error, hook }
}
fn rebuild(self, state: &mut Self::State) {
let _guard = state.hook.clone().map(throw_error::set_error_hook);
match (&mut state.state, self) {
// both errors: throw the new error and replace
(Either::Right(_), Err(new)) => {
@ -68,6 +71,7 @@ where
/// The view state.
state: OptionState<T, R>,
error: Option<throw_error::ErrorId>,
hook: Option<Arc<dyn ErrorHook>>,
}
impl<T, R> Drop for ResultState<T, R>
@ -164,6 +168,7 @@ where
inner.to_html_with_buf(buf, position, escape, mark_branches)
}
Err(e) => {
buf.push_str("<!>");
throw_error::throw(e);
}
}
@ -186,6 +191,7 @@ where
mark_branches,
),
Err(e) => {
buf.push_sync("<!>");
throw_error::throw(e);
}
}
@ -196,6 +202,7 @@ where
cursor: &Cursor<R>,
position: &PositionState,
) -> Self::State {
let hook = throw_error::get_error_hook();
let (state, error) = match self {
Ok(view) => (
Either::Left(view.hydrate::<FROM_SERVER>(cursor, position)),
@ -210,6 +217,6 @@ where
(Either::Right(state), Some(throw_error::throw(e.into())))
}
};
ResultState { state, error }
ResultState { state, error, hook }
}
}