use super::Reactor;
use crate::{
error_views::{ErrorContext, ErrorPosition, ErrorViews},
errors::ClientError,
template::BrowserNodeType,
utils::{render_or_hydrate, replace_head},
};
#[cfg(engine)]
use std::rc::Rc;
use std::{panic::PanicInfo, sync::Arc};
use sycamore::{
prelude::{create_scope_immediate, try_use_context, view, Scope, ScopeDisposer},
view::View,
web::SsrNode,
};
impl Reactor<BrowserNodeType> {
/// This reports an error to the failsafe mechanism, which will handle it
/// appropriately. This will determine the capabilities the error view
/// will have access to from the scope provided.
///
/// This returns the disposer for the underlying error scope, which must be
/// handled appropriately, or a memory leak will occur. Leaking an error
/// scope is never permissible. A boolean of whether or not the error took
/// up the whole page or not is also returned, which can be used to guide
/// what should be done with the disposer.
///
/// Obviously, since this is a method on a reactor, this does not handle
/// critical errors caused by not being able to create a reactor.
///
/// This **does not** handle widget errors (unless they're popups).
#[must_use]
pub(crate) fn report_err<'a>(
&self,
cx: Scope<'a>,
err: ClientError,
) -> (ScopeDisposer<'a>, bool) {
// Determine where this should be placed
let pos = match self.is_first.get() {
// On an initial load, we'll use a popup, unless it's a server-given error
true => match err {
ClientError::ServerError { .. } => ErrorPosition::Page,
_ => ErrorPosition::Popup,
},
// On a subsequent load, this is the responsibility of the user
false => match self.error_views.subsequent_err_should_be_popup(&err) {
true => ErrorPosition::Popup,
false => ErrorPosition::Page,
},
};
let (head_str, body_view, disposer) = self.error_views.handle(cx, err, pos);
match pos {
// For page-wide errors, we need to set the head
ErrorPosition::Page => {
replace_head(&head_str);
self.current_view.set(body_view);
(disposer, true)
}
ErrorPosition::Popup => {
self.popup_error_view.set(body_view);
(disposer, false)
}
// We don't handle widget errors in this function
ErrorPosition::Widget => unreachable!(),
}
}
/// Creates the infrastructure necessary to handle a critical error, and
/// then displays it. This is intended for use if the reactor cannot be
/// instantiated, and it takes the app-level context to verify this.
///
/// # Panics
/// This will panic if given a scope in which a reactor exists.
///
/// # Visibility
/// This is broadly part of Perseus implementation details, and is exposed
/// only for those foregoing `#[perseus::main]` or
/// `#[perseus::browser_main]` to build their own custom browser-side
/// entrypoint (do not do this unless you really need to).
pub fn handle_critical_error(
cx: Scope,
err: ClientError,
error_views: &ErrorViews<BrowserNodeType>,
) {
// We do NOT want this called if there is a reactor (but, if it is, we have no
// clue about the calling situation, so it's safest to just panic)
assert!(try_use_context::<Reactor<BrowserNodeType>>(cx).is_none(), "attempted to handle 'critical' error, but a reactor was found (this is a programming error)");
let popup_error_root = Self::get_popup_err_elem();
// This will determine the `Static` error context (we guaranteed there's no
// reactor above). We don't care about the head in a popup.
let (_, err_view, disposer) = error_views.handle(cx, err, ErrorPosition::Popup);
render_or_hydrate(
cx,
view! { cx,
// This is not reactive, as there's no point in making it so
(err_view)
},
popup_error_root,
true, // Browser-side-only error, so force a full render
);
// SAFETY: We're outside the child scope
unsafe {
disposer.dispose();
}
}
/// Creates the infrastructure necessary to handle a panic, and then
/// displays an error created by the user's [`ErrorViews`]. This
/// function will only panic if certain fundamental functions of the web
/// APIs are not defined, in which case no error message could ever be
/// displayed to the user anyway.
///
/// A handler is manually provided to this, because the [`ErrorViews`]
/// are typically not thread-safe once extracted from `PerseusApp`.
///
/// # Visibility
/// Under absolutely no circumstances should this function **ever** be
/// called outside a Perseus panic handler set in the entrypoint! It is
/// exposed for custom entrypoints only.
pub fn handle_panic(
panic_info: &PanicInfo,
handler: Arc<
dyn Fn(
Scope,
ClientError,
ErrorContext,
ErrorPosition,
) -> (View<SsrNode>, View<BrowserNodeType>)
+ Send
+ Sync,
>,
) {
let popup_error_root = Self::get_popup_err_elem();
// The standard library handles all the hard parts here
let msg = panic_info.to_string();
// The whole app is about to implode, we are not keeping this scope
// around
create_scope_immediate(|cx| {
let (_head, body) = handler(
cx,
ClientError::Panic(msg),
ErrorContext::Static,
ErrorPosition::Popup,
);
render_or_hydrate(
cx,
view! { cx,
// This is not reactive, as there's no point in making it so
(body)
},
popup_error_root,
true, // Browser-side-only error, so force a full render
);
});
}
}