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(),
})
}
}
}
}