1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
use serde_json::Value;
use sycamore::{
    prelude::{create_scope, Scope, ScopeDisposer},
    view::View,
    web::Html,
};

use crate::{
    errors::{AssetType, ClientError, ClientInvariantError},
    i18n::detect_locale,
    page_data::PageDataPartial,
    path::PathMaybeWithLocale,
    router::{FullRouteInfo, FullRouteVerdict, RouteVerdict, RouterLoadState},
    state::{PssContains, TemplateState},
    utils::{checkpoint, fetch, get_path_prefix_client, replace_head},
};

use super::Reactor;

impl<G: Html> Reactor<G> {
    /// Gets the subsequent view, based on the given verdict.
    ///
    /// Note that 'server errors' as constructed by this function are
    /// constructed here, not deserialized from provided data.
    ///
    /// # Panics
    /// This function will panic on a locale redirection if a router has not
    /// been created on the given scope.
    ///
    /// This function will also panic if the given route verdict stores a
    /// template name that is not known to this reactor (i.e. it must have
    /// been generated by `match_route`).
    pub(crate) async fn get_subsequent_view<'a>(
        &self,
        cx: Scope<'a>,
        verdict: RouteVerdict,
    ) -> Result<(View<G>, ScopeDisposer<'a>), ClientError> {
        checkpoint("router_entry");
        // We'll need this for setting the router load state later
        let slim_verdict = verdict.clone();

        match &verdict.into_full(&self.entities) {
            FullRouteVerdict::Found(FullRouteInfo {
                path,
                entity,
                locale,
                was_incremental_match,
            }) => {
                let full_path = PathMaybeWithLocale::new(&path, &locale);
                // Update the router state
                self.router_state.set_load_state(RouterLoadState::Loading {
                    template_name: entity.get_path(),
                    path: full_path.clone(),
                });
                self.router_state.set_last_verdict(slim_verdict);

                // Before we fetch anything, first check if there's an entry in the PSS already
                // (if there is, we can avoid a network request)
                let page_data = match self.state_store.contains(&full_path) {
                    // We only have one part of the puzzle (or nothing at all), and no guarantee
                    // that the other doesn't exist, so we'll have to check with
                    // the server to be safe. Remember that this function
                    // can't be used with widgets!
                    PssContains::State | PssContains::Head | PssContains::None => {
                        // Get the static page data (head and state)
                        let asset_url = format!(
                            "{}/.perseus/page/{}/{}.json?entity_name={}&was_incremental_match={}",
                            get_path_prefix_client(),
                            locale,
                            **path,
                            entity.get_path(),
                            was_incremental_match
                        );
                        // If this doesn't exist, then it's a 404 (we went here by explicit
                        // navigation, but it may be an unservable ISR page
                        // or the like)
                        let page_data_str = fetch(&asset_url, AssetType::Page).await?;
                        match &page_data_str {
                            Some(page_data_str) => {
                                // All good, deserialize the page data
                                let page_data =
                                    serde_json::from_str::<PageDataPartial>(&page_data_str);
                                match page_data {
                                    Ok(page_data) => {
                                        // Add the head to the PSS for future use (we make
                                        // absolutely no
                                        // assumptions about state and leave that to the macros)
                                        self.state_store.add_head(
                                            &full_path,
                                            page_data.head.to_string(),
                                            false,
                                        );
                                        page_data
                                    }
                                    // If the page failed to serialize, it's a server error
                                    Err(err) => {
                                        return Err(ClientInvariantError::InvalidState {
                                            source: err,
                                        }
                                        .into())
                                    }
                                }
                            }
                            // This indicates the fetch found a 404 (any other errors were
                            // propagated by `?`)
                            None => {
                                return Err(ClientError::ServerError {
                                    status: 404,
                                    message: "page not found".to_string(),
                                })
                            }
                        }
                    }
                    // We have everything locally, so we can move right ahead!
                    PssContains::All => PageDataPartial {
                        // This will never be parsed, because the template closures use the active
                        // state preferentially, whose existence we verified
                        // by getting here
                        state: Value::Null,
                        head: self.state_store.get_head(&full_path).unwrap(),
                    },
                    // We only have document metadata, but the page definitely takes no state, so
                    // we're fine
                    PssContains::HeadNoState => PageDataPartial {
                        state: Value::Null,
                        head: self.state_store.get_head(&full_path).unwrap(),
                    },
                    // The page's data has been preloaded at some other time
                    PssContains::Preloaded => {
                        let page_data = self.state_store.get_preloaded(&full_path).unwrap();
                        // Register the head, otherwise it will never be registered and the page
                        // will never properly show up in the PSS (meaning
                        // future preload calls will go through, creating
                        // unnecessary network requests)
                        self.state_store
                            .add_head(&full_path, page_data.head.to_string(), false);
                        page_data
                    }
                };
                // Interpolate the metadata directly into the document's `<head>`
                replace_head(&page_data.head);

                // Now update the translator (this will do nothing if the user hasn't switched
                // locales). Importantly, if this returns an error, the error
                // views will almost certainly get the old translator. Because
                // this will be registered as an internal error as well,
                // that means we'll probably get a popup, which is much better UX than an error
                // page on `/fr-FR/foo` in Spanish.
                self.translations_manager
                    .set_translator_for_locale(&locale)
                    .await?;

                let template_name = entity.get_path();
                // Pre-emptively update the router state
                checkpoint("page_interactive");
                // Update the router state
                self.router_state.set_load_state(RouterLoadState::Loaded {
                    template_name,
                    path: full_path.clone(),
                });
                // Now return the view that should be rendered
                let (view, disposer) = entity.render_for_template_client(
                    full_path,
                    TemplateState::from_value(page_data.state),
                    cx,
                )?;

                Ok((view, disposer))
            }
            // For subsequent loads, this should only be possible if the dev forgot `link!()`
            // TODO Debug assertion that this doesn't happen perhaps?
            FullRouteVerdict::LocaleDetection(path) => {
                let dest = detect_locale(path.clone(), &self.locales);
                // Since this is only for subsequent loads, we know the router is instantiated
                // This shouldn't be a replacement navigation, since the user has deliberately
                // navigated here
                sycamore_router::navigate(&dest);
                // We'll ever get here
                Ok((View::empty(), create_scope(|_| {})))
            }
            FullRouteVerdict::NotFound { .. } => {
                checkpoint("not_found");

                // Neatly return a `ClientError::ServerError`, which will be displayed somehow
                // by the caller (hopefully as a full-page view, but that will depend on the
                // user's error view logic)
                Err(ClientError::ServerError {
                    status: 404,
                    message: "page not found".to_string(),
                })
            }
        }
    }
}