Skip to content

Commit

Permalink
feat: added global state
Browse files Browse the repository at this point in the history
This shouldn't break anything for anyone using the macros and not
digging into the internals of Perseus.

Closes #119.
  • Loading branch information
arctic-hen7 committed Jan 21, 2022
1 parent 4c9c1be commit a5fcc56
Show file tree
Hide file tree
Showing 32 changed files with 741 additions and 223 deletions.
16 changes: 14 additions & 2 deletions examples/basic/.perseus/builder/src/bin/build.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use fmterr::fmt_err;
use perseus::{internal::build::build_app, PluginAction, SsrNode};
use perseus_engine::app::{
get_immutable_store, get_locales, get_mutable_store, get_plugins, get_templates_map,
get_translations_manager,
get_global_state_creator, get_immutable_store, get_locales, get_mutable_store, get_plugins,
get_templates_map, get_translations_manager,
};

#[tokio::main]
Expand All @@ -27,6 +27,17 @@ async fn real_main() -> i32 {
// We can't proceed without a translations manager
let translations_manager = get_translations_manager().await;
let locales = get_locales(&plugins);
// Generate the global state
let gsc = get_global_state_creator(&plugins);
let global_state = match gsc.get_build_state().await {
Ok(global_state) => global_state,
Err(err) => {
let err_msg = fmt_err(&err);
// TODO Functional action here
eprintln!("{}", err_msg);
return 1;
}
};

// Build the site for all the common locales (done in parallel)
// All these parameters can be modified by `define_app!` and plugins, so there's no point in having a plugin opportunity here
Expand All @@ -36,6 +47,7 @@ async fn real_main() -> i32 {
&locales,
(&immutable_store, &mutable_store),
&translations_manager,
&global_state,
// We use another binary to handle exporting
false,
)
Expand Down
17 changes: 15 additions & 2 deletions examples/basic/.perseus/builder/src/bin/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use perseus::{
PluginAction, SsrNode,
};
use perseus_engine::app::{
get_app_root, get_immutable_store, get_locales, get_mutable_store, get_plugins,
get_static_aliases, get_templates_map, get_translations_manager,
get_app_root, get_global_state_creator, get_immutable_store, get_locales, get_mutable_store,
get_plugins, get_static_aliases, get_templates_map, get_translations_manager,
};
use std::fs;
use std::path::PathBuf;
Expand Down Expand Up @@ -58,6 +58,17 @@ async fn build_and_export() -> i32 {
let mutable_store = get_mutable_store();
let translations_manager = get_translations_manager().await;
let locales = get_locales(&plugins);
// Generate the global state
let gsc = get_global_state_creator(&plugins);
let global_state = match gsc.get_build_state().await {
Ok(global_state) => global_state,
Err(err) => {
let err_msg = fmt_err(&err);
// TODO Functional action here
eprintln!("{}", err_msg);
return 1;
}
};

// Build the site for all the common locales (done in parallel), denying any non-exportable features
// We need to build and generate those artifacts before we can proceed on to exporting
Expand All @@ -67,6 +78,7 @@ async fn build_and_export() -> i32 {
&locales,
(&immutable_store, &mutable_store),
&translations_manager,
&global_state,
// We use another binary to handle normal building
true,
)
Expand Down Expand Up @@ -97,6 +109,7 @@ async fn build_and_export() -> i32 {
&immutable_store,
&translations_manager,
get_path_prefix_server(),
&global_state,
)
.await;
if let Err(err) = export_res {
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/.perseus/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ tokio = { version = "1", optional = true, features = [ "macros", "rt-multi-threa
integration-actix-web = [ "perseus-actix-web", "actix-web", "actix-http" ]
integration-warp = [ "perseus-warp", "warp", "tokio" ]

default = [ "integration-warp" ]
default = [ "integration-actix-web" ]

# This makes the binary work on its own, and is enabled by `perseus deploy` (do NOT invoke this manually!)
standalone = [ "perseus/standalone", "perseus-engine/standalone" ]
11 changes: 8 additions & 3 deletions examples/basic/.perseus/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use perseus::plugins::PluginAction;
use perseus::stores::MutableStore;
use perseus::SsrNode;
use perseus_engine::app::{
get_app_root, get_error_pages_contained, get_immutable_store, get_locales, get_mutable_store,
get_plugins, get_static_aliases, get_templates_map_atomic_contained, get_translations_manager,
get_app_root, get_error_pages_contained, get_global_state_creator, get_immutable_store,
get_locales, get_mutable_store, get_plugins, get_static_aliases,
get_templates_map_atomic_contained, get_translations_manager,
};
use std::env;
use std::fs;
Expand All @@ -26,6 +27,7 @@ async fn main() -> std::io::Result<()> {

let is_standalone = get_standalone_and_act();
let (host, port) = get_host_and_port();

HttpServer::new(move || App::new().configure(block_on(configurer(get_props(is_standalone)))))
.bind((host, port))?
.run()
Expand All @@ -40,7 +42,7 @@ async fn main() {
use std::net::SocketAddr;

let is_standalone = get_standalone_and_act();
let props = get_props(is_standalone);
let props = get_props(is_standalone).await;
let (host, port) = get_host_and_port();
let addr: SocketAddr = format!("{}:{}", host, port)
.parse()
Expand Down Expand Up @@ -105,6 +107,8 @@ fn get_props(is_standalone: bool) -> ServerProps<impl MutableStore, impl Transla
let locales = get_locales(&plugins);
let app_root = get_app_root(&plugins);
let static_aliases = get_static_aliases(&plugins);
// Generate the global state
let global_state_creator = get_global_state_creator(&plugins);

let opts = ServerOptions {
// We don't support setting some attributes from `wasm-pack` through plugins/`define_app!` because that would require CLI changes as well (a job for an alternative engine)
Expand Down Expand Up @@ -134,5 +138,6 @@ fn get_props(is_standalone: bool) -> ServerProps<impl MutableStore, impl Transla
immutable_store,
mutable_store: get_mutable_store(),
translations_manager: block_on(get_translations_manager()),
global_state_creator,
}
}
6 changes: 6 additions & 0 deletions examples/basic/.perseus/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
pub use app::get_plugins;
use perseus::{
internal::i18n::Locales,
state::GlobalStateCreator,
stores::ImmutableStore,
templates::{ArcTemplateMap, TemplateMap},
ErrorPages, Html, PluginAction, Plugins,
Expand Down Expand Up @@ -165,3 +166,8 @@ pub fn get_error_pages_contained<G: Html>() -> ErrorPages<G> {
let plugins = get_plugins::<G>();
get_error_pages(&plugins)
}

pub fn get_global_state_creator<G: Html>(plugins: &Plugins<G>) -> GlobalStateCreator {
// TODO Control action to override
app::get_global_state_creator()
}
10 changes: 7 additions & 3 deletions examples/basic/.perseus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use perseus::{
templates::{RouterState, TemplateNodeType},
DomNode,
};
use std::cell::RefCell;
use std::rc::Rc;
use std::{any::Any, cell::RefCell};
use sycamore::prelude::{cloned, create_effect, view, NodeRef, ReadSignal};
use sycamore_router::{HistoryIntegration, Router, RouterProps};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
Expand Down Expand Up @@ -67,6 +67,9 @@ pub fn run() -> Result<(), JsValue> {
let router_state = RouterState::default();
// Create a page state store to use
let pss = PageStateStore::default();
// Create a new global state set to `None`, which will be updated and handled entirely by the template macro from here on
let global_state: Rc<RefCell<Box<dyn Any>>> =
Rc::new(RefCell::new(Box::new(Option::<()>::None)));

// Create the router we'll use for this app, based on the user's app definition
create_app_route! {
Expand All @@ -90,7 +93,7 @@ pub fn run() -> Result<(), JsValue> {
// Sycamore's reactivity is broken by a future, so we need to explicitly add the route to the reactive dependencies here
// We do need the future though (otherwise `container_rx` doesn't link to anything until it's too late)
let _ = route.get();
wasm_bindgen_futures::spawn_local(cloned!((locales, route, container_rx, router_state, pss, translations_manager, error_pages, initial_container) => async move {
wasm_bindgen_futures::spawn_local(cloned!((locales, route, container_rx, router_state, pss, global_state, translations_manager, error_pages, initial_container) => async move {
let container_rx_elem = container_rx.get::<DomNode>().unchecked_into::<web_sys::Element>();
checkpoint("router_entry");
match &route.get().as_ref().0 {
Expand All @@ -113,7 +116,8 @@ pub fn run() -> Result<(), JsValue> {
error_pages: error_pages.clone(),
initial_container: initial_container.unwrap().clone(),
container_rx_elem: container_rx_elem.clone(),
page_state_store: pss.clone()
page_state_store: pss.clone(),
global_state: global_state.clone()
}
).await,
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale
Expand Down
12 changes: 6 additions & 6 deletions examples/rx_state/src/about.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use crate::global_state::AppState;
use crate::index::IndexPropsRx;
use perseus::{get_render_ctx, Html, Template};
use sycamore::prelude::{component, view, Signal};
use sycamore::prelude::{view, Signal};
use sycamore::view::View;

// This template doesn't have any properties, so there's no point in using the special `template_with_rx_state` macro (but we could)
#[perseus::template(AboutPage)]
#[component(AboutPage<G>)]
#[perseus::template_with_rx_state(component = "AboutPage", global_state = "AppState")]
pub fn about_page() -> View<G> {
// Get the page state store manually
let pss = get_render_ctx!().page_state_store;
// The index page is just an empty string
let index_props_rx = get_render_ctx!().page_state_store.get::<IndexPropsRx>("");
// Get the state from the index page
// If the user hasn't visited there yet, this won't exist
// The index page is just an empty string
let username = match pss.get::<IndexPropsRx>("") {
let username = match index_props_rx {
Some(IndexPropsRx { username }) => username,
None => Signal::new("".to_string()),
};
Expand Down
17 changes: 17 additions & 0 deletions examples/rx_state/src/global_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use perseus::{state::GlobalStateCreator, RenderFnResult};

pub fn get_global_state_creator() -> GlobalStateCreator {
GlobalStateCreator::new().build_state_fn(get_build_state)
}

#[perseus::make_rx(AppStateRx)]
pub struct AppState {
pub test: String,
}

#[perseus::autoserde(global_build_state)]
pub async fn get_build_state() -> RenderFnResult<AppState> {
Ok(AppState {
test: "Hello from the global state build process!".to_string(),
})
}
12 changes: 9 additions & 3 deletions examples/rx_state/src/index.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use perseus::{Html, RenderFnResultWithCause, Template};
use sycamore::prelude::*;

use crate::global_state::{AppState, AppStateRx};

// We define a normal `struct` and then use `make_rx` (which derives `Serialize`, `Deserialize`, and `Clone` automatically)
// This will generate a new `struct` called `IndexPropsRx` (as we asked it to), in which every field is made reactive with a `Signal`
#[perseus::make_rx(IndexPropsRx)]
Expand All @@ -11,13 +13,17 @@ pub struct IndexProps {
// This special macro (normally we'd use `template(IndexProps)`) converts the state we generate elsewhere to a reactive version
// We need to tell it the name of the unreactive properties we created to start with (unfortunately the compiler isn't smart enough to figure that out yet)
// This will also add our reactive properties to the global state store, and, if they're already there, it'll use the existing one
#[perseus::template_with_rx_state(IndexPage, IndexProps)]
#[component(IndexPage<G>)]
pub fn index_page(IndexPropsRx { username }: IndexPropsRx) -> View<G> {
#[perseus::template_with_rx_state(
component = "IndexPage",
unrx_props = "IndexProps",
global_state = "AppState"
)]
pub fn index_page(IndexPropsRx { username }: IndexPropsRx, global_state: AppStateRx) -> View<G> {
let username_2 = username.clone(); // This is necessary until Sycamore's new reactive primitives are released
view! {
p { (format!("Greetings, {}!", username.get())) }
input(bind:value = username_2, placeholder = "Username")
p { (global_state.test.get()) }

// When the user visits this and then comes back, they'll still be able to see their username (the previous state will be retrieved from the global state automatically)
a(href = "about") { "About" }
Expand Down
5 changes: 4 additions & 1 deletion examples/rx_state/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod about;
mod global_state;
mod index;

use perseus::define_app;

define_app! {
templates: [
index::get_template::<G>(),
Expand All @@ -11,5 +13,6 @@ define_app! {
sycamore::view! {
p { (format!("An error with HTTP code {} occurred at '{}': '{}'.", status, url, err)) }
}
})
}),
global_state_creator: global_state::get_global_state_creator()
}
4 changes: 1 addition & 3 deletions examples/showcase/src/templates/router_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ use sycamore::prelude::{cloned, component, create_memo, view, View};
#[perseus::template(RouterStatePage)]
#[component(RouterStatePage<G>)]
pub fn router_state_page() -> View<G> {
let load_state = sycamore::context::use_context::<perseus::templates::RenderCtx>()
.router
.get_load_state();
let load_state = perseus::get_render_ctx!().router.get_load_state();
let load_state_str = create_memo(
cloned!(load_state => move || match (*load_state.get()).clone() {
RouterLoadState::Loaded(name) => format!("Loaded {}.", name),
Expand Down
8 changes: 8 additions & 0 deletions packages/perseus-actix-web/src/configurer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub async fn configurer<M: MutableStore + 'static, T: TranslationsManager + 'sta
immutable_store,
mutable_store,
translations_manager,
global_state_creator,
}: ServerProps<M, T>,
) -> impl FnOnce(&mut actix_web::web::ServiceConfig) {
let opts = Rc::new(opts); // TODO Find a more efficient way of doing this
Expand All @@ -56,6 +57,12 @@ pub async fn configurer<M: MutableStore + 'static, T: TranslationsManager + 'sta
&render_cfg,
&get_path_prefix_server(),
);
// Generate the global state
// The user will get a more detailed error message in the build process
let global_state = global_state_creator
.get_build_state()
.await
.expect("Couldn't generate global state.");

move |cfg: &mut web::ServiceConfig| {
cfg
Expand All @@ -66,6 +73,7 @@ pub async fn configurer<M: MutableStore + 'static, T: TranslationsManager + 'sta
.app_data(web::Data::new(translations_manager.clone()))
.app_data(web::Data::new(opts.clone()))
.app_data(web::Data::new(index_with_render_cfg.clone()))
.app_data(web::Data::new(global_state.clone()))
// TODO chunk JS and Wasm bundles
// These allow getting the basic app code (not including the static data)
// This contains everything in the spirit of a pseudo-SPA
Expand Down
5 changes: 4 additions & 1 deletion packages/perseus-actix-web/src/initial_load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fn return_error_page(

/// The handler for calls to any actual pages (first-time visits), which will render the appropriate HTML and then interpolate it into
/// the app shell.
#[allow(clippy::too_many_arguments)]
pub async fn initial_load<M: MutableStore, T: TranslationsManager>(
req: HttpRequest,
opts: web::Data<Rc<ServerOptions>>,
Expand All @@ -44,6 +45,7 @@ pub async fn initial_load<M: MutableStore, T: TranslationsManager>(
immutable_store: web::Data<ImmutableStore>,
mutable_store: web::Data<M>,
translations_manager: web::Data<T>,
global_state: web::Data<Option<String>>,
) -> HttpResponse {
let templates = &opts.templates_map;
let error_pages = &opts.error_pages;
Expand Down Expand Up @@ -81,6 +83,7 @@ pub async fn initial_load<M: MutableStore, T: TranslationsManager>(
template,
was_incremental_match,
http_req,
&global_state,
(immutable_store.get_ref(), mutable_store.get_ref()),
translations_manager.get_ref(),
)
Expand All @@ -96,7 +99,7 @@ pub async fn initial_load<M: MutableStore, T: TranslationsManager>(
let final_html = html_shell
.get_ref()
.clone()
.page_data(&page_data)
.page_data(&page_data, &global_state)
.to_string();

let mut http_res = HttpResponse::Ok();
Expand Down
3 changes: 3 additions & 0 deletions packages/perseus-actix-web/src/page_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ pub struct PageDataReq {
}

/// The handler for calls to `.perseus/page/*`. This will manage returning errors and the like.
#[allow(clippy::too_many_arguments)]
pub async fn page_data<M: MutableStore, T: TranslationsManager>(
req: HttpRequest,
opts: web::Data<Rc<ServerOptions>>,
immutable_store: web::Data<ImmutableStore>,
mutable_store: web::Data<M>,
translations_manager: web::Data<T>,
global_state: web::Data<Option<String>>,
web::Query(query_params): web::Query<PageDataReq>,
) -> HttpResponse {
let templates = &opts.templates_map;
Expand Down Expand Up @@ -60,6 +62,7 @@ pub async fn page_data<M: MutableStore, T: TranslationsManager>(
template,
was_incremental_match,
http_req,
&global_state,
(immutable_store.get_ref(), mutable_store.get_ref()),
translations_manager.get_ref(),
)
Expand Down
Loading

0 comments on commit a5fcc56

Please sign in to comment.