Allow async using spawn_local_scoped

This commit is contained in:
Jeremy Wall 2022-12-28 12:59:11 -06:00
parent e77fe40d75
commit 8bcafc385d
7 changed files with 482 additions and 437 deletions

674
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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");

View File

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

View File

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

View File

@ -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") {

View File

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

View File

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