mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -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 reqwasm;
|
||||||
use serde_json::{from_str, to_string};
|
use serde_json::{from_str, to_string};
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use sycamore_state::Handler;
|
|
||||||
use tracing::{debug, error, info, instrument, warn};
|
use tracing::{debug, error, info, instrument, warn};
|
||||||
|
|
||||||
use client_api::*;
|
use client_api::*;
|
||||||
@ -25,10 +24,11 @@ use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
|
|||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{self, AppState, Message, StateMachine},
|
app_state::{self, AppState},
|
||||||
js_lib,
|
js_lib,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// FIXME(jwall): We should be able to delete this now.
|
||||||
#[instrument]
|
#[instrument]
|
||||||
fn filter_recipes(
|
fn filter_recipes(
|
||||||
recipe_entries: &Option<Vec<RecipeEntry>>,
|
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))]
|
#[instrument(skip(state))]
|
||||||
pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Result<(), String> {
|
pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Result<(), String> {
|
||||||
info!("Synchronizing Recipes");
|
info!("Synchronizing Recipes");
|
||||||
|
@ -13,15 +13,16 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use sycamore::{futures::spawn_local, prelude::*};
|
|
||||||
use tracing::{debug, error, instrument, warn};
|
|
||||||
|
|
||||||
use client_api::UserData;
|
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 sycamore_state::{Handler, MessageMapper};
|
||||||
|
use tracing::{debug, error, info, instrument, warn};
|
||||||
|
|
||||||
use crate::api::HttpStore;
|
use crate::api::HttpStore;
|
||||||
|
use crate::js_lib;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@ -70,13 +71,122 @@ pub enum Message {
|
|||||||
SetUserData(UserData),
|
SetUserData(UserData),
|
||||||
UnsetUserData,
|
UnsetUserData,
|
||||||
SaveState,
|
SaveState,
|
||||||
|
LoadState,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StateMachine(HttpStore);
|
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 {
|
impl MessageMapper<Message, AppState> for StateMachine {
|
||||||
#[instrument(skip_all, fields(?msg))]
|
#[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();
|
let mut original_copy = original.get().as_ref().clone();
|
||||||
match msg {
|
match msg {
|
||||||
Message::InitRecipeCounts(map) => {
|
Message::InitRecipeCounts(map) => {
|
||||||
@ -133,14 +243,21 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
Message::SaveState => {
|
Message::SaveState => {
|
||||||
let store = self.0.clone();
|
let store = self.0.clone();
|
||||||
let original_copy = original_copy.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 {
|
if let Err(e) = store.save_app_state(original_copy).await {
|
||||||
error!(err=?e, "Error saving app state")
|
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
|
// We also need to set recipe in our state
|
||||||
dirty.set(false);
|
dirty.set(false);
|
||||||
if let Ok(recipe) = recipes::parse::as_recipe(text.get_untracked().as_ref()) {
|
if let Ok(recipe) = recipes::parse::as_recipe(text.get_untracked().as_ref()) {
|
||||||
sh.dispatch(Message::SetRecipe(
|
sh.dispatch(
|
||||||
id.get_untracked().as_ref().to_owned(),
|
cx,
|
||||||
recipe,
|
Message::SetRecipe(id.get_untracked().as_ref().to_owned(), recipe),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,12 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use recipes::Recipe;
|
use recipes::Recipe;
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
use sycamore::prelude::*;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::app_state;
|
||||||
use crate::app_state::{Message, StateHandler};
|
use crate::app_state::{Message, StateHandler};
|
||||||
use crate::components::recipe_selection::*;
|
use crate::components::recipe_selection::*;
|
||||||
use crate::{api::*, app_state};
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[instrument(skip_all)]
|
#[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.
|
// FIXME(jwall): We should probably make this a dispatch method instead.
|
||||||
create_effect(cx, move || {
|
create_effect(cx, move || {
|
||||||
refresh_click.track();
|
refresh_click.track();
|
||||||
let store = HttpStore::get_from_context(cx);
|
sh.dispatch(cx, Message::LoadState);
|
||||||
spawn_local_scoped(cx, async move { init_app_state(store.as_ref(), sh).await });
|
|
||||||
});
|
});
|
||||||
create_effect(cx, move || {
|
create_effect(cx, move || {
|
||||||
save_click.track();
|
save_click.track();
|
||||||
sh.dispatch(Message::SaveState);
|
sh.dispatch(cx, Message::SaveState);
|
||||||
});
|
});
|
||||||
view! {cx,
|
view! {cx,
|
||||||
table(class="recipe_selector no-print") {
|
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");
|
debug!("authenticating against ui");
|
||||||
// TODO(jwall): Navigate to plan if the below is successful.
|
// TODO(jwall): Navigate to plan if the below is successful.
|
||||||
if let Some(user_data) = store.authenticate(username, password).await {
|
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 sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
|
use crate::app_state::Message;
|
||||||
use crate::components::{Footer, Header};
|
use crate::components::{Footer, Header};
|
||||||
use crate::{api, routing::Handler as RouteHandler};
|
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();
|
let store = api::HttpStore::get_from_context(cx).as_ref().clone();
|
||||||
info!("Starting UI");
|
info!("Starting UI");
|
||||||
let app_state = crate::app_state::AppState::new();
|
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());
|
let view = create_signal(cx, View::empty());
|
||||||
// FIXME(jwall): We need a way to trigger refreshes when required. Turn this
|
// 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.
|
// into a create_effect with a refresh signal stored as a context.
|
||||||
spawn_local_scoped(cx, {
|
spawn_local_scoped(cx, {
|
||||||
let store = api::HttpStore::get_from_context(cx);
|
let store = api::HttpStore::get_from_context(cx);
|
||||||
async move {
|
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
|
// TODO(jwall): This needs to be moved into the RouteHandler
|
||||||
view.set(view! { cx,
|
view.set(view! { cx,
|
||||||
div(class="app") {
|
div(class="app") {
|
||||||
Header(handler)
|
Header(sh)
|
||||||
RouteHandler(sh=handler)
|
RouteHandler(sh=sh)
|
||||||
Footer { }
|
Footer { }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user