perseus/reactor/error.rs
1use super::Reactor;
2use crate::{
3 error_views::{ErrorContext, ErrorPosition, ErrorViews},
4 errors::ClientError,
5 template::BrowserNodeType,
6 utils::{render_or_hydrate, replace_head},
7};
8#[cfg(engine)]
9use std::rc::Rc;
10use std::{panic::PanicInfo, sync::Arc};
11use sycamore::{
12 prelude::{create_scope_immediate, try_use_context, view, Scope, ScopeDisposer},
13 view::View,
14 web::SsrNode,
15};
16
17impl Reactor<BrowserNodeType> {
18 /// This reports an error to the failsafe mechanism, which will handle it
19 /// appropriately. This will determine the capabilities the error view
20 /// will have access to from the scope provided.
21 ///
22 /// This returns the disposer for the underlying error scope, which must be
23 /// handled appropriately, or a memory leak will occur. Leaking an error
24 /// scope is never permissible. A boolean of whether or not the error took
25 /// up the whole page or not is also returned, which can be used to guide
26 /// what should be done with the disposer.
27 ///
28 /// Obviously, since this is a method on a reactor, this does not handle
29 /// critical errors caused by not being able to create a reactor.
30 ///
31 /// This **does not** handle widget errors (unless they're popups).
32 #[must_use]
33 pub(crate) fn report_err<'a>(
34 &self,
35 cx: Scope<'a>,
36 err: ClientError,
37 ) -> (ScopeDisposer<'a>, bool) {
38 // Determine where this should be placed
39 let pos = match self.is_first.get() {
40 // On an initial load, we'll use a popup, unless it's a server-given error
41 true => match err {
42 ClientError::ServerError { .. } => ErrorPosition::Page,
43 _ => ErrorPosition::Popup,
44 },
45 // On a subsequent load, this is the responsibility of the user
46 false => match self.error_views.subsequent_err_should_be_popup(&err) {
47 true => ErrorPosition::Popup,
48 false => ErrorPosition::Page,
49 },
50 };
51
52 let (head_str, body_view, disposer) = self.error_views.handle(cx, err, pos);
53
54 match pos {
55 // For page-wide errors, we need to set the head
56 ErrorPosition::Page => {
57 replace_head(&head_str);
58 self.current_view.set(body_view);
59 (disposer, true)
60 }
61 ErrorPosition::Popup => {
62 self.popup_error_view.set(body_view);
63 (disposer, false)
64 }
65 // We don't handle widget errors in this function
66 ErrorPosition::Widget => unreachable!(),
67 }
68 }
69
70 /// Creates the infrastructure necessary to handle a critical error, and
71 /// then displays it. This is intended for use if the reactor cannot be
72 /// instantiated, and it takes the app-level context to verify this.
73 ///
74 /// # Panics
75 /// This will panic if given a scope in which a reactor exists.
76 ///
77 /// # Visibility
78 /// This is broadly part of Perseus implementation details, and is exposed
79 /// only for those foregoing `#[perseus::main]` or
80 /// `#[perseus::browser_main]` to build their own custom browser-side
81 /// entrypoint (do not do this unless you really need to).
82 pub fn handle_critical_error(
83 cx: Scope,
84 err: ClientError,
85 error_views: &ErrorViews<BrowserNodeType>,
86 ) {
87 // We do NOT want this called if there is a reactor (but, if it is, we have no
88 // clue about the calling situation, so it's safest to just panic)
89 assert!(try_use_context::<Reactor<BrowserNodeType>>(cx).is_none(), "attempted to handle 'critical' error, but a reactor was found (this is a programming error)");
90
91 let popup_error_root = Self::get_popup_err_elem();
92 // This will determine the `Static` error context (we guaranteed there's no
93 // reactor above). We don't care about the head in a popup.
94 let (_, err_view, disposer) = error_views.handle(cx, err, ErrorPosition::Popup);
95 render_or_hydrate(
96 cx,
97 view! { cx,
98 // This is not reactive, as there's no point in making it so
99 (err_view)
100 },
101 popup_error_root,
102 true, // Browser-side-only error, so force a full render
103 );
104 // SAFETY: We're outside the child scope
105 unsafe {
106 disposer.dispose();
107 }
108 }
109 /// Creates the infrastructure necessary to handle a panic, and then
110 /// displays an error created by the user's [`ErrorViews`]. This
111 /// function will only panic if certain fundamental functions of the web
112 /// APIs are not defined, in which case no error message could ever be
113 /// displayed to the user anyway.
114 ///
115 /// A handler is manually provided to this, because the [`ErrorViews`]
116 /// are typically not thread-safe once extracted from `PerseusApp`.
117 ///
118 /// # Visibility
119 /// Under absolutely no circumstances should this function **ever** be
120 /// called outside a Perseus panic handler set in the entrypoint! It is
121 /// exposed for custom entrypoints only.
122 #[allow(clippy::type_complexity)]
123 pub fn handle_panic(
124 panic_info: &PanicInfo,
125 handler: Arc<
126 dyn Fn(
127 Scope,
128 ClientError,
129 ErrorContext,
130 ErrorPosition,
131 ) -> (View<SsrNode>, View<BrowserNodeType>)
132 + Send
133 + Sync,
134 >,
135 ) {
136 let popup_error_root = Self::get_popup_err_elem();
137
138 // The standard library handles all the hard parts here
139 let msg = panic_info.to_string();
140 // The whole app is about to implode, we are not keeping this scope
141 // around
142 create_scope_immediate(|cx| {
143 let (_head, body) = handler(
144 cx,
145 ClientError::Panic(msg),
146 ErrorContext::Static,
147 ErrorPosition::Popup,
148 );
149 render_or_hydrate(
150 cx,
151 view! { cx,
152 // This is not reactive, as there's no point in making it so
153 (body)
154 },
155 popup_error_root,
156 true, // Browser-side-only error, so force a full render
157 );
158 });
159 }
160}