mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-21 19:29:49 -04:00
Allow async using spawn_local_scoped
This commit is contained in:
parent
e77fe40d75
commit
8bcafc385d
674
Cargo.lock
generated
674
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,6 @@ use base64;
|
||||
use reqwasm;
|
||||
use serde_json::{from_str, to_string};
|
||||
use sycamore::prelude::*;
|
||||
use sycamore_state::Handler;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use client_api::*;
|
||||
@ -25,10 +24,11 @@ use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use crate::{
|
||||
app_state::{self, AppState, Message, StateMachine},
|
||||
app_state::{self, AppState},
|
||||
js_lib,
|
||||
};
|
||||
|
||||
// FIXME(jwall): We should be able to delete this now.
|
||||
#[instrument]
|
||||
fn filter_recipes(
|
||||
recipe_entries: &Option<Vec<RecipeEntry>>,
|
||||
@ -57,86 +57,6 @@ fn filter_recipes(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init_app_state<'ctx>(
|
||||
store: &HttpStore,
|
||||
h: &'ctx Handler<'ctx, StateMachine, AppState, Message>,
|
||||
) {
|
||||
info!("Synchronizing Recipes");
|
||||
// TODO(jwall): Make our caching logic using storage more robust.
|
||||
let recipe_entries = match store.get_recipes().await {
|
||||
Ok(recipe_entries) => {
|
||||
if let Ok((staples, recipes)) = filter_recipes(&recipe_entries) {
|
||||
h.dispatch(Message::SetStaples(staples));
|
||||
if let Some(recipes) = recipes {
|
||||
h.dispatch(Message::InitRecipes(recipes));
|
||||
}
|
||||
}
|
||||
recipe_entries
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(Some(plan)) = store.get_plan().await {
|
||||
// set the counts.
|
||||
let mut plan_map = BTreeMap::new();
|
||||
for (id, count) in plan {
|
||||
plan_map.insert(id, count as usize);
|
||||
}
|
||||
h.dispatch(Message::InitRecipeCounts(plan_map));
|
||||
} else {
|
||||
// Initialize things to zero
|
||||
if let Some(rs) = recipe_entries {
|
||||
for r in rs {
|
||||
h.dispatch(Message::UpdateRecipeCount(r.recipe_id().to_owned(), 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Checking for user_data in local storage");
|
||||
let storage = js_lib::get_storage();
|
||||
let user_data = storage
|
||||
.get("user_data")
|
||||
.expect("Couldn't read from storage");
|
||||
if let Some(data) = user_data {
|
||||
if let Ok(user_data) = from_str(&data) {
|
||||
h.dispatch(Message::SetUserData(user_data));
|
||||
}
|
||||
}
|
||||
info!("Synchronizing categories");
|
||||
match store.get_categories().await {
|
||||
Ok(Some(categories_content)) => {
|
||||
debug!(categories=?categories_content);
|
||||
match recipes::parse::as_categories(&categories_content) {
|
||||
Ok(category_map) => {
|
||||
h.dispatch(Message::SetCategoryMap(category_map));
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err)
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!("There is no category file");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
info!("Synchronizing inventory data");
|
||||
match store.get_inventory_data().await {
|
||||
Ok((filtered_ingredients, modified_amts, extra_items)) => {
|
||||
h.dispatch(Message::InitAmts(modified_amts));
|
||||
h.dispatch(Message::InitFilteredIngredient(filtered_ingredients));
|
||||
h.dispatch(Message::InitExtras(BTreeSet::from_iter(extra_items)));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Result<(), String> {
|
||||
info!("Synchronizing Recipes");
|
||||
|
@ -13,15 +13,16 @@
|
||||
// limitations under the License.
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use sycamore::{futures::spawn_local, prelude::*};
|
||||
use tracing::{debug, error, instrument, warn};
|
||||
|
||||
use client_api::UserData;
|
||||
use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe};
|
||||
|
||||
use recipes::{parse, Ingredient, IngredientAccumulator, IngredientKey, Recipe, RecipeEntry};
|
||||
use serde_json::from_str;
|
||||
use sycamore::futures::spawn_local_scoped;
|
||||
use sycamore::{futures::spawn_local, prelude::*};
|
||||
use sycamore_state::{Handler, MessageMapper};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use crate::api::HttpStore;
|
||||
use crate::js_lib;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AppState {
|
||||
@ -70,13 +71,122 @@ pub enum Message {
|
||||
SetUserData(UserData),
|
||||
UnsetUserData,
|
||||
SaveState,
|
||||
LoadState,
|
||||
}
|
||||
|
||||
pub struct StateMachine(HttpStore);
|
||||
|
||||
#[instrument]
|
||||
fn filter_recipes(
|
||||
recipe_entries: &Option<Vec<RecipeEntry>>,
|
||||
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> {
|
||||
match recipe_entries {
|
||||
Some(parsed) => {
|
||||
let mut staples = None;
|
||||
let mut parsed_map = BTreeMap::new();
|
||||
for r in parsed {
|
||||
let recipe = match parse::as_recipe(&r.recipe_text()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error parsing recipe {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if recipe.title == "Staples" {
|
||||
staples = Some(recipe);
|
||||
} else {
|
||||
parsed_map.insert(r.recipe_id().to_owned(), recipe);
|
||||
}
|
||||
}
|
||||
Ok((staples, Some(parsed_map)))
|
||||
}
|
||||
None => Ok((None, None)),
|
||||
}
|
||||
}
|
||||
impl StateMachine {
|
||||
async fn load_state(store: HttpStore, original: &Signal<AppState>) {
|
||||
let mut state = original.get().as_ref().clone();
|
||||
info!("Synchronizing Recipes");
|
||||
// TODO(jwall): Make our caching logic using storage more robust.
|
||||
let recipe_entries = match store.get_recipes().await {
|
||||
Ok(recipe_entries) => {
|
||||
if let Ok((staples, recipes)) = filter_recipes(&recipe_entries) {
|
||||
state.staples = staples;
|
||||
if let Some(recipes) = recipes {
|
||||
state.recipes = recipes;
|
||||
}
|
||||
}
|
||||
recipe_entries
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(Some(plan)) = store.get_plan().await {
|
||||
// set the counts.
|
||||
let mut plan_map = BTreeMap::new();
|
||||
for (id, count) in plan {
|
||||
plan_map.insert(id, count as usize);
|
||||
}
|
||||
state.recipe_counts = plan_map;
|
||||
} else {
|
||||
// Initialize things to zero
|
||||
if let Some(rs) = recipe_entries {
|
||||
for r in rs {
|
||||
state.recipe_counts.insert(r.recipe_id().to_owned(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Checking for user_data in local storage");
|
||||
let storage = js_lib::get_storage();
|
||||
let user_data = storage
|
||||
.get("user_data")
|
||||
.expect("Couldn't read from storage");
|
||||
if let Some(data) = user_data {
|
||||
if let Ok(user_data) = from_str(&data) {
|
||||
state.auth = Some(user_data);
|
||||
}
|
||||
}
|
||||
info!("Synchronizing categories");
|
||||
match store.get_categories().await {
|
||||
Ok(Some(categories_content)) => {
|
||||
debug!(categories=?categories_content);
|
||||
match recipes::parse::as_categories(&categories_content) {
|
||||
Ok(category_map) => {
|
||||
state.category_map = category_map;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err)
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!("There is no category file");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
info!("Synchronizing inventory data");
|
||||
match store.get_inventory_data().await {
|
||||
Ok((filtered_ingredients, modified_amts, extra_items)) => {
|
||||
state.modified_amts = modified_amts;
|
||||
state.filtered_ingredients = filtered_ingredients;
|
||||
state.extras = BTreeSet::from_iter(extra_items);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
original.set(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageMapper<Message, AppState> for StateMachine {
|
||||
#[instrument(skip_all, fields(?msg))]
|
||||
fn map(&self, msg: Message, original: &ReadSignal<AppState>) -> AppState {
|
||||
fn map<'ctx>(&self, cx: Scope<'ctx>, msg: Message, original: &'ctx Signal<AppState>) {
|
||||
let mut original_copy = original.get().as_ref().clone();
|
||||
match msg {
|
||||
Message::InitRecipeCounts(map) => {
|
||||
@ -133,14 +243,21 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
Message::SaveState => {
|
||||
let store = self.0.clone();
|
||||
let original_copy = original_copy.clone();
|
||||
spawn_local(async move {
|
||||
spawn_local_scoped(cx, async move {
|
||||
if let Err(e) = store.save_app_state(original_copy).await {
|
||||
error!(err=?e, "Error saving app state")
|
||||
};
|
||||
});
|
||||
}
|
||||
Message::LoadState => {
|
||||
let store = self.0.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
Self::load_state(store, original).await;
|
||||
});
|
||||
return;
|
||||
}
|
||||
original_copy
|
||||
}
|
||||
original.set(original_copy);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,10 +95,10 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>)
|
||||
// We also need to set recipe in our state
|
||||
dirty.set(false);
|
||||
if let Ok(recipe) = recipes::parse::as_recipe(text.get_untracked().as_ref()) {
|
||||
sh.dispatch(Message::SetRecipe(
|
||||
id.get_untracked().as_ref().to_owned(),
|
||||
recipe,
|
||||
));
|
||||
sh.dispatch(
|
||||
cx,
|
||||
Message::SetRecipe(id.get_untracked().as_ref().to_owned(), recipe),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -12,12 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use recipes::Recipe;
|
||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||
use sycamore::prelude::*;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::{Message, StateHandler};
|
||||
use crate::components::recipe_selection::*;
|
||||
use crate::{api::*, app_state};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[instrument(skip_all)]
|
||||
@ -41,12 +41,11 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie
|
||||
// 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);
|
||||
spawn_local_scoped(cx, async move { init_app_state(store.as_ref(), sh).await });
|
||||
sh.dispatch(cx, Message::LoadState);
|
||||
});
|
||||
create_effect(cx, move || {
|
||||
save_click.track();
|
||||
sh.dispatch(Message::SaveState);
|
||||
sh.dispatch(cx, Message::SaveState);
|
||||
});
|
||||
view! {cx,
|
||||
table(class="recipe_selector no-print") {
|
||||
|
@ -29,7 +29,7 @@ pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View
|
||||
debug!("authenticating against ui");
|
||||
// TODO(jwall): Navigate to plan if the below is successful.
|
||||
if let Some(user_data) = store.authenticate(username, password).await {
|
||||
sh.dispatch(Message::SetUserData(user_data));
|
||||
sh.dispatch(cx, Message::SetUserData(user_data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use crate::app_state::Message;
|
||||
use crate::components::{Footer, Header};
|
||||
use crate::{api, routing::Handler as RouteHandler};
|
||||
|
||||
@ -25,19 +26,19 @@ pub fn UI<G: Html>(cx: Scope) -> View<G> {
|
||||
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, store);
|
||||
let sh = 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(store.as_ref(), handler).await;
|
||||
sh.dispatch(cx, Message::LoadState);
|
||||
// TODO(jwall): This needs to be moved into the RouteHandler
|
||||
view.set(view! { cx,
|
||||
div(class="app") {
|
||||
Header(handler)
|
||||
RouteHandler(sh=handler)
|
||||
Header(sh)
|
||||
RouteHandler(sh=sh)
|
||||
Footer { }
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user