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}