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
//! The internals of Perseus' state generation platform. This is not responsible
//! for the reactivity of state, or any other browser-side work. This is
//! responsible for the actual *generation* of state on the engine-side, at both
//! build-time and request-time.
//!
//! If you wanted to isolate the core of engine-side Perseus, it would be this
//! module.
mod build;
mod build_error_page;
mod export;
mod export_error_page;
mod serve;
/// This has the actual API endpoints.
mod server;
mod tinker;
pub use server::{ApiResponse, SubsequentLoadQueryParams};
use crate::{
error_views::ErrorViews,
errors::*,
i18n::{Locales, TranslationsManager},
init::{PerseusAppBase, Tm},
plugins::Plugins,
server::HtmlShell,
state::{GlobalStateCreator, TemplateState},
stores::{ImmutableStore, MutableStore},
template::EntityMap,
};
use futures::executor::block_on;
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use sycamore::web::SsrNode;
/// The Perseus state generator.
#[derive(Debug)]
pub struct Turbine<M: MutableStore, T: TranslationsManager> {
/// All the templates and capsules in the app.
entities: EntityMap<SsrNode>,
/// The app's error views.
error_views: Arc<ErrorViews<SsrNode>>,
/// The app's locales data.
locales: Locales,
/// An immutable store.
immutable_store: ImmutableStore,
/// A mutable store.
mutable_store: M,
/// A translations manager.
translations_manager: T,
/// The global state creator.
global_state_creator: Arc<GlobalStateCreator>,
plugins: Arc<Plugins>,
index_view_str: String,
root_id: String,
/// This is stored as a `PathBuf` so we can easily check whether or not it
/// exists.
pub static_dir: PathBuf,
/// The app's static aliases.
pub static_aliases: HashMap<String, String>,
// --- These may not be populated at creation ---
/// The app's render configuration, a map of paths in the app to the names
/// of the templates that generated them. (Since templates can have
/// multiple `/` delimiters in their names.)
///
/// Since the paths are not actually valid paths, we leave them typed as
/// `String`s, but these keys are in effect `PathWithoutLocale` instances.
render_cfg: HashMap<String, String>,
/// The app's global state, kept cached throughout the build process because
/// every template we build will need access to it through context.
global_state: TemplateState,
/// The HTML shell that can be used for constructing the full pages this app
/// returns.
html_shell: Option<HtmlShell>,
}
// We want to be able to create a turbine straight from an app base
impl<M: MutableStore, T: TranslationsManager> TryFrom<PerseusAppBase<SsrNode, M, T>>
for Turbine<M, T>
{
type Error = PluginError;
fn try_from(app: PerseusAppBase<SsrNode, M, T>) -> Result<Self, Self::Error> {
let locales = app.get_locales()?;
let immutable_store = app.get_immutable_store()?;
let index_view_str = app.get_index_view_str();
let root_id = app.get_root()?;
let static_aliases = app.get_static_aliases()?;
Ok(Self {
entities: app.entities,
locales,
immutable_store,
mutable_store: app.mutable_store,
global_state_creator: app.global_state_creator,
plugins: app.plugins,
index_view_str,
root_id,
static_dir: PathBuf::from(&app.static_dir),
static_aliases,
#[cfg(debug_assertions)]
error_views: app.error_views.unwrap_or_default(),
#[cfg(not(debug_assertions))]
error_views: app
.error_views
.expect("you must provide your own error pages in production"),
// This consumes the app
// Note that we can't do anything in parallel with this anyway
translations_manager: match app.translations_manager {
Tm::Dummy(tm) => tm,
Tm::Full(tm) => block_on(tm),
},
// If we're going from a `PerseusApp`, these will be filled in later
render_cfg: HashMap::new(),
// This will be immediately overriden
global_state: TemplateState::empty(),
html_shell: None,
})
}
}
impl<M: MutableStore, T: TranslationsManager> Turbine<M, T> {
/// Updates some internal fields of the turbine by assuming the app has been
/// built in the past. This expects a number of things to exist in the
/// filesystem. Note that calling `.build()` will automatically perform
/// this population.
pub async fn populate_after_build(&mut self) -> Result<(), ServerError> {
// Get the render config
let render_cfg_str = self.immutable_store.read("render_conf.json").await?;
let render_cfg = serde_json::from_str::<HashMap<String, String>>(&render_cfg_str)
.map_err(|err| ServerError::BuildError(BuildError::RenderCfgInvalid { source: err }))?;
self.render_cfg = render_cfg;
// Get the global state
let global_state = self.immutable_store.read("static/global_state.json").await;
self.global_state = match global_state {
Ok(state) => TemplateState::from_str(&state)
.map_err(|err| ServerError::InvalidPageState { source: err })?,
Err(StoreError::NotFound { .. }) => TemplateState::empty(),
Err(err) => return Err(err.into()),
};
let html_shell = PerseusAppBase::<SsrNode, M, T>::get_html_shell(
self.index_view_str.to_string(),
&self.root_id,
&self.render_cfg,
&self.plugins,
)
.await?;
self.html_shell = Some(html_shell);
Ok(())
}
}