From e77fe40d752506690eaa0bf31ce845c8d40fc914 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 28 Dec 2022 11:54:24 -0600 Subject: [PATCH] Introduce a save state message --- web/src/api.rs | 26 +++++++++++++++++++++++--- web/src/app_state.rs | 30 ++++++++++++++++++++++++------ web/src/components/recipe_plan.rs | 31 +++++++++---------------------- web/src/pages/planning/plan.rs | 2 +- web/src/web.rs | 7 +++++-- 5 files changed, 62 insertions(+), 34 deletions(-) diff --git a/web/src/api.rs b/web/src/api.rs index 6863e2d..e73c88a 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -58,11 +58,10 @@ fn filter_recipes( } pub async fn init_app_state<'ctx>( - cx: Scope<'ctx>, + store: &HttpStore, h: &'ctx Handler<'ctx, StateMachine, AppState, Message>, ) { info!("Synchronizing Recipes"); - let store = HttpStore::get_from_context(cx); // TODO(jwall): Make our caching logic using storage more robust. let recipe_entries = match store.get_recipes().await { Ok(recipe_entries) => { @@ -270,7 +269,7 @@ pub struct HttpStore { } impl HttpStore { - fn new(root: String) -> Self { + pub fn new(root: String) -> Self { Self { root } } @@ -493,6 +492,27 @@ impl HttpStore { } } + #[instrument(skip_all)] + pub async fn save_app_state(&self, state: AppState) -> Result<(), Error> { + let mut plan = Vec::new(); + for (key, count) in state.recipe_counts.iter() { + plan.push((key.clone(), *count as i32)); + } + debug!("Saving plan data"); + self.save_plan(plan).await?; + debug!("Saving inventory data"); + self.save_inventory_data( + state.filtered_ingredients, + state.modified_amts, + state + .extras + .iter() + .cloned() + .collect::>(), + ) + .await + } + #[instrument] pub async fn save_state(&self, state: std::rc::Rc) -> Result<(), Error> { let mut plan = Vec::new(); diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 63f2322..057bbc7 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -13,14 +13,16 @@ // limitations under the License. use std::collections::{BTreeMap, BTreeSet}; -use sycamore::prelude::*; -use tracing::{debug, instrument, warn}; +use sycamore::{futures::spawn_local, prelude::*}; +use tracing::{debug, error, instrument, warn}; use client_api::UserData; use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe}; use sycamore_state::{Handler, MessageMapper}; +use crate::api::HttpStore; + #[derive(Debug, Clone, PartialEq)] pub struct AppState { pub recipe_counts: BTreeMap, @@ -48,6 +50,7 @@ impl AppState { } } +#[derive(Debug)] pub enum Message { InitRecipeCounts(BTreeMap), UpdateRecipeCount(String, usize), @@ -66,12 +69,13 @@ pub enum Message { UpdateAmt(IngredientKey, String), SetUserData(UserData), UnsetUserData, + SaveState, } -// TODO(jwall): Add HttpStore here and do the proper things for each event. -pub struct StateMachine(); +pub struct StateMachine(HttpStore); impl MessageMapper for StateMachine { + #[instrument(skip_all, fields(?msg))] fn map(&self, msg: Message, original: &ReadSignal) -> AppState { let mut original_copy = original.get().as_ref().clone(); match msg { @@ -126,6 +130,15 @@ impl MessageMapper for StateMachine { Message::UnsetUserData => { original_copy.auth = None; } + Message::SaveState => { + let store = self.0.clone(); + let original_copy = original_copy.clone(); + spawn_local(async move { + if let Err(e) = store.save_app_state(original_copy).await { + error!(err=?e, "Error saving app state") + }; + }); + } } original_copy } @@ -133,9 +146,14 @@ impl MessageMapper for StateMachine { pub type StateHandler<'ctx> = &'ctx Handler<'ctx, StateMachine, AppState, Message>; -pub fn get_state_handler<'ctx>(cx: Scope<'ctx>, initial: AppState) -> StateHandler<'ctx> { - Handler::new(cx, initial, StateMachine()) +pub fn get_state_handler<'ctx>( + cx: Scope<'ctx>, + initial: AppState, + store: HttpStore, +) -> StateHandler<'ctx> { + Handler::new(cx, initial, StateMachine(store)) } + #[derive(Debug)] pub struct State { pub recipe_counts: RcSignal>>, diff --git a/web/src/components/recipe_plan.rs b/web/src/components/recipe_plan.rs index ab409fb..3633c0d 100644 --- a/web/src/components/recipe_plan.rs +++ b/web/src/components/recipe_plan.rs @@ -13,21 +13,20 @@ // limitations under the License. use recipes::Recipe; use sycamore::{futures::spawn_local_scoped, prelude::*}; -use tracing::{error, instrument}; +use tracing::instrument; +use crate::app_state::{Message, StateHandler}; use crate::components::recipe_selection::*; use crate::{api::*, app_state}; #[allow(non_snake_case)] -#[instrument] -pub fn RecipePlan(cx: Scope) -> View { - let rows = create_memo(cx, move || { - let state = app_state::State::get_from_context(cx); +#[instrument(skip_all)] +pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + let rows = sh.get_selector(cx, move |state| { let mut rows = Vec::new(); for row in state - .recipes .get() - .as_ref() + .recipes .iter() .map(|(k, v)| create_signal(cx, (k.clone(), v.clone()))) .collect::>>() @@ -39,27 +38,15 @@ pub fn RecipePlan(cx: Scope) -> View { }); let refresh_click = create_signal(cx, false); let save_click = create_signal(cx, false); + // FIXME(jwall): We should probably make this a dispatch method instead. create_effect(cx, move || { refresh_click.track(); let store = HttpStore::get_from_context(cx); - let state = app_state::State::get_from_context(cx); - spawn_local_scoped(cx, { - async move { - if let Err(err) = init_page_state(store.as_ref(), state.as_ref()).await { - error!(?err); - }; - } - }); + spawn_local_scoped(cx, async move { init_app_state(store.as_ref(), sh).await }); }); create_effect(cx, move || { save_click.track(); - let store = HttpStore::get_from_context(cx); - let state = app_state::State::get_from_context(cx); - spawn_local_scoped(cx, { - async move { - store.save_state(state).await.expect("Failed to save plan"); - } - }) + sh.dispatch(Message::SaveState); }); view! {cx, table(class="recipe_selector no-print") { diff --git a/web/src/pages/planning/plan.rs b/web/src/pages/planning/plan.rs index 838eb76..f3f4975 100644 --- a/web/src/pages/planning/plan.rs +++ b/web/src/pages/planning/plan.rs @@ -21,6 +21,6 @@ pub fn PlanPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View< view! {cx, PlanningPage( selected=Some("Plan".to_owned()), - ) { RecipePlan() } + ) { RecipePlan(sh) } } } diff --git a/web/src/web.rs b/web/src/web.rs index a16932d..58b0e8a 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -21,15 +21,18 @@ use crate::{api, routing::Handler as RouteHandler}; #[component] pub fn UI(cx: Scope) -> View { api::HttpStore::provide_context(cx, "/api".to_owned()); + // FIXME(jwall): We shouldn't need to get the store from a context anymore. + let store = api::HttpStore::get_from_context(cx).as_ref().clone(); info!("Starting UI"); let app_state = crate::app_state::AppState::new(); - let handler = crate::app_state::get_state_handler(cx, app_state); + let handler = crate::app_state::get_state_handler(cx, app_state, store); let view = create_signal(cx, View::empty()); // FIXME(jwall): We need a way to trigger refreshes when required. Turn this // into a create_effect with a refresh signal stored as a context. spawn_local_scoped(cx, { + let store = api::HttpStore::get_from_context(cx); async move { - api::init_app_state(cx, handler).await; + api::init_app_state(store.as_ref(), handler).await; // TODO(jwall): This needs to be moved into the RouteHandler view.set(view! { cx, div(class="app") {