Introduce a save state message

This commit is contained in:
Jeremy Wall 2022-12-28 11:54:24 -06:00
parent fbb4e4ceeb
commit e77fe40d75
5 changed files with 62 additions and 34 deletions

View File

@ -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::<Vec<(String, String)>>(),
)
.await
}
#[instrument]
pub async fn save_state(&self, state: std::rc::Rc<app_state::State>) -> Result<(), Error> {
let mut plan = Vec::new();

View File

@ -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<String, usize>,
@ -48,6 +50,7 @@ impl AppState {
}
}
#[derive(Debug)]
pub enum Message {
InitRecipeCounts(BTreeMap<String, usize>),
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<Message, AppState> for StateMachine {
#[instrument(skip_all, fields(?msg))]
fn map(&self, msg: Message, original: &ReadSignal<AppState>) -> AppState {
let mut original_copy = original.get().as_ref().clone();
match msg {
@ -126,6 +130,15 @@ impl MessageMapper<Message, AppState> 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<Message, AppState> 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<BTreeMap<String, RcSignal<usize>>>,

View File

@ -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<G: Html>(cx: Scope) -> View<G> {
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<G> {
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::<Vec<&Signal<(String, Recipe)>>>()
@ -39,27 +38,15 @@ pub fn RecipePlan<G: Html>(cx: Scope) -> View<G> {
});
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") {

View File

@ -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) }
}
}

View File

@ -21,15 +21,18 @@ use crate::{api, routing::Handler as RouteHandler};
#[component]
pub fn UI<G: Html>(cx: Scope) -> View<G> {
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") {