mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-28 20:29:53 -04:00
Compare commits
No commits in common. "aba1e114cf56fac216959ed84daf603ee2a0cce0" and "1c55a315b00d185f2c556966b4edb4f63fcc9e11" have entirely different histories.
aba1e114cf
...
1c55a315b0
1652
Cargo.lock
generated
1652
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,12 +6,10 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = "<=1.0.171"
|
||||
recipes = { path = "../recipes" }
|
||||
chrono = "0.4.22"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.axum]
|
||||
version = "0.5.16"
|
||||
|
@ -18,19 +18,14 @@ async-trait = "0.1.57"
|
||||
async-session = "3.0.0"
|
||||
ciborium = "0.2.0"
|
||||
tower = "0.4.13"
|
||||
serde = "<=1.0.171"
|
||||
cookie = "0.17.0"
|
||||
chrono = "0.4.22"
|
||||
metrics = "0.20.1"
|
||||
metrics-exporter-prometheus = "0.11.0"
|
||||
futures = "0.3"
|
||||
metrics-process = "1.0.8"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.22"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
|
||||
[dependencies.argon2]
|
||||
version = "0.5.0"
|
||||
|
||||
|
@ -99,7 +99,7 @@ impl AsyncFileStore {
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
debug!("adding recipe file {}", file_name);
|
||||
let recipe_contents = read_to_string(entry.path()).await?;
|
||||
entry_vec.push(RecipeEntry::new(file_name, recipe_contents));
|
||||
entry_vec.push(RecipeEntry(file_name, recipe_contents, None, None));
|
||||
} else {
|
||||
warn!(
|
||||
file = %entry.path().to_string_lossy(),
|
||||
@ -119,12 +119,12 @@ impl AsyncFileStore {
|
||||
if recipe_path.exists().await && recipe_path.is_file().await {
|
||||
debug!("Found recipe file {}", recipe_path.to_string_lossy());
|
||||
let recipe_contents = read_to_string(recipe_path).await?;
|
||||
return Ok(Some(RecipeEntry {
|
||||
id: id.as_ref().to_owned(),
|
||||
text: recipe_contents,
|
||||
category: None,
|
||||
serving_count: None,
|
||||
}));
|
||||
return Ok(Some(RecipeEntry(
|
||||
id.as_ref().to_owned(),
|
||||
recipe_contents,
|
||||
None,
|
||||
None,
|
||||
)));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
|
@ -440,12 +440,12 @@ impl APIStore for SqliteStore {
|
||||
.await?
|
||||
.iter()
|
||||
.map(|row| {
|
||||
RecipeEntry {
|
||||
id: row.recipe_id.clone(),
|
||||
text: row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||
category: row.category.clone(),
|
||||
serving_count: row.serving_count.clone(),
|
||||
}
|
||||
RecipeEntry(
|
||||
row.recipe_id.clone(),
|
||||
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||
row.category.clone(),
|
||||
row.serving_count.clone(),
|
||||
)
|
||||
})
|
||||
.nth(0);
|
||||
Ok(entry)
|
||||
@ -460,12 +460,12 @@ impl APIStore for SqliteStore {
|
||||
.await?
|
||||
.iter()
|
||||
.map(|row| {
|
||||
RecipeEntry {
|
||||
id: row.recipe_id.clone(),
|
||||
text: row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||
category: row.category.clone(),
|
||||
serving_count: row.serving_count.clone(),
|
||||
}
|
||||
RecipeEntry(
|
||||
row.recipe_id.clone(),
|
||||
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||
row.category.clone(),
|
||||
row.serving_count.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(Some(rows))
|
||||
|
@ -8,14 +8,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
abortable_parser = "~0.2.6"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.22"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
features = ["derive"]
|
||||
chrono = "~0.4"
|
||||
serde = "1.0.144"
|
||||
|
||||
[dependencies.num-rational]
|
||||
version = "~0.4.0"
|
||||
|
@ -50,49 +50,39 @@ impl Mealplan {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct RecipeEntry {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
pub category: Option<String>,
|
||||
pub serving_count: Option<i64>,
|
||||
}
|
||||
pub struct RecipeEntry(pub String, pub String, pub Option<String>, pub Option<i64>);
|
||||
|
||||
impl RecipeEntry {
|
||||
pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self {
|
||||
Self {
|
||||
id: recipe_id.into(),
|
||||
text: text.into(),
|
||||
category: None,
|
||||
serving_count: None,
|
||||
}
|
||||
Self(recipe_id.into(), text.into(), None, None)
|
||||
}
|
||||
|
||||
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
||||
self.id = id.into();
|
||||
self.0 = id.into();
|
||||
}
|
||||
|
||||
pub fn recipe_id(&self) -> &str {
|
||||
self.id.as_str()
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
pub fn set_recipe_text<S: Into<String>>(&mut self, text: S) {
|
||||
self.text = text.into();
|
||||
self.1 = text.into();
|
||||
}
|
||||
|
||||
pub fn recipe_text(&self) -> &str {
|
||||
self.text.as_str()
|
||||
self.1.as_str()
|
||||
}
|
||||
|
||||
pub fn set_category<S: Into<String>>(&mut self, cat: S) {
|
||||
self.category = Some(cat.into());
|
||||
self.2 = Some(cat.into());
|
||||
}
|
||||
|
||||
pub fn category(&self) -> Option<&String> {
|
||||
self.category.as_ref()
|
||||
self.2.as_ref()
|
||||
}
|
||||
|
||||
|
||||
pub fn serving_count(&self) -> Option<i64> {
|
||||
self.serving_count.clone()
|
||||
self.3.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +210,7 @@ pub struct Step {
|
||||
impl Step {
|
||||
pub fn new<S: Into<String>>(prep_time: Option<std::time::Duration>, instructions: S) -> Self {
|
||||
Self {
|
||||
prep_time,
|
||||
prep_time: prep_time,
|
||||
instructions: instructions.into(),
|
||||
ingredients: Vec::new(),
|
||||
}
|
||||
|
@ -27,12 +27,9 @@ sycamore-router = "0.8"
|
||||
js-sys = "0.3.60"
|
||||
wasm-web-component = { git = "https://github.com/zaphar/wasm-web-components.git", rev = "v0.3.0" }
|
||||
maud = "*"
|
||||
indexed-db = "0.4.1"
|
||||
anyhow = "1.0.86"
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
version = "<=1.0.171"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
|
324
web/src/api.rs
324
web/src/api.rs
@ -16,27 +16,18 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use base64::{self, Engine};
|
||||
use chrono::NaiveDate;
|
||||
use gloo_net;
|
||||
// TODO(jwall): Remove this when we have gone a few migrations past.
|
||||
use serde_json::from_str;
|
||||
use serde_json::{from_str, to_string};
|
||||
use sycamore::prelude::*;
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
use anyhow::Result;
|
||||
use client_api::*;
|
||||
use recipes::{IngredientKey, RecipeEntry};
|
||||
use serde_wasm_bindgen::{from_value, Serializer};
|
||||
use wasm_bindgen::JsValue;
|
||||
// TODO(jwall): Remove this when we have gone a few migrations past.
|
||||
use web_sys::Storage;
|
||||
|
||||
fn to_js<T: serde::ser::Serialize>(value: T) -> Result<JsValue, serde_wasm_bindgen::Error> {
|
||||
let s = Serializer::new().serialize_maps_as_objects(true);
|
||||
value.serialize(&s)
|
||||
}
|
||||
|
||||
use crate::{
|
||||
app_state::{parse_recipes, AppState},
|
||||
js_lib::{self, DBFactory},
|
||||
js_lib,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -85,110 +76,48 @@ impl From<gloo_net::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
fn recipe_key<S: std::fmt::Display>(id: S) -> String {
|
||||
format!("recipe:{}", id)
|
||||
}
|
||||
|
||||
fn token68(user: String, pass: String) -> String {
|
||||
base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass))
|
||||
}
|
||||
|
||||
fn convert_to_io_error<V, E>(res: Result<V, E>) -> Result<V, std::io::Error>
|
||||
where
|
||||
E: Into<Box<dyn std::error::Error>> + std::fmt::Debug,
|
||||
{
|
||||
match res {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("{:?}", e),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LocalStore {
|
||||
// TODO(zaphar): Remove this when it's safe to delete the migration
|
||||
old_store: Storage,
|
||||
store: DBFactory<'static>,
|
||||
store: Storage,
|
||||
}
|
||||
|
||||
const APP_STATE_KEY: &'static str = "app-state";
|
||||
const USER_DATA_KEY: &'static str = "user_data";
|
||||
|
||||
impl LocalStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
store: DBFactory::default(),
|
||||
old_store: js_lib::get_storage(),
|
||||
store: js_lib::get_storage(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn migrate(&self) {
|
||||
// 1. migrate app-state from localstore to indexeddb
|
||||
debug!("Peforming localstorage migration");
|
||||
if let Ok(Some(v)) = self.old_store.get("app_state") {
|
||||
if let Ok(Some(local_state)) = from_str::<Option<AppState>>(&v) {
|
||||
self.store_app_state(&local_state).await;
|
||||
}
|
||||
}
|
||||
let _ = self.old_store.remove_item("app_state");
|
||||
// 2. migrate user-state from localstore to indexeddb
|
||||
if let Ok(Some(v)) = self.old_store.get(USER_DATA_KEY) {
|
||||
if let Ok(local_user_data) = from_str::<Option<UserData>>(&v) {
|
||||
self.set_user_data(local_user_data.as_ref()).await;
|
||||
}
|
||||
}
|
||||
let _ = self.old_store.remove_item(USER_DATA_KEY);
|
||||
// 3. Recipes
|
||||
let store_len = self.old_store.length().unwrap();
|
||||
let mut key_list = Vec::new();
|
||||
for i in 0..store_len {
|
||||
let key = self.old_store.key(i).unwrap().unwrap();
|
||||
if key.starts_with("recipe:") {
|
||||
key_list.push(key);
|
||||
}
|
||||
}
|
||||
for k in key_list {
|
||||
if let Ok(Some(recipe)) = self.old_store.get(&k) {
|
||||
if let Ok(recipe) = from_str::<RecipeEntry>(&recipe) {
|
||||
self.set_recipe_entry(&recipe).await;
|
||||
}
|
||||
}
|
||||
let _ = self.old_store.delete(&k);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn store_app_state(&self, state: &AppState) {
|
||||
let state = match to_js(state) {
|
||||
pub fn store_app_state(&self, state: &AppState) {
|
||||
self.migrate_local_store();
|
||||
let state = match to_string(state) {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
error!(?err, ?state, "Error deserializing app_state");
|
||||
return;
|
||||
}
|
||||
};
|
||||
web_sys::console::log_1(&state);
|
||||
let key = to_js(APP_STATE_KEY).expect("Failed to serialize key");
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
object_store.put_kv(&key, &state).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to store app-state");
|
||||
.set("app_state", &state)
|
||||
.expect("Failed to set our app state");
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn fetch_app_state(&self) -> Option<AppState> {
|
||||
pub fn fetch_app_state(&self) -> Option<AppState> {
|
||||
debug!("Loading state from local store");
|
||||
let recipes = parse_recipes(&self.get_recipes().await).expect("Failed to parse recipes");
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let key = convert_to_io_error(to_js(APP_STATE_KEY))?;
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
let mut app_state: AppState = match object_store.get(&key).await? {
|
||||
Some(s) => convert_to_io_error(from_value(s))?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
self.store.get("app_state").map_or(None, |val| {
|
||||
val.map(|s| {
|
||||
debug!("Found an app_state object");
|
||||
let mut app_state: AppState =
|
||||
from_str(&s).expect("Failed to deserialize app state");
|
||||
let recipes = parse_recipes(&self.get_recipes()).expect("Failed to parse recipes");
|
||||
if let Some(recipes) = recipes {
|
||||
debug!("Populating recipes");
|
||||
for (id, recipe) in recipes {
|
||||
@ -196,173 +125,122 @@ impl LocalStore {
|
||||
app_state.recipes.insert(id, recipe);
|
||||
}
|
||||
}
|
||||
Ok(Some(app_state))
|
||||
app_state
|
||||
})
|
||||
.await
|
||||
.expect("Failed to fetch app-state")
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Gets user data from local storage.
|
||||
pub async fn get_user_data(&self) -> Option<UserData> {
|
||||
pub fn get_user_data(&self) -> Option<UserData> {
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let key = to_js(USER_DATA_KEY).expect("Failed to serialize key");
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
let user_data: UserData = match object_store.get(&key).await? {
|
||||
Some(s) => convert_to_io_error(from_value(s))?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
Ok(Some(user_data))
|
||||
})
|
||||
.await
|
||||
.expect("Failed to fetch user_data")
|
||||
.get("user_data")
|
||||
.map_or(None, |val| val.map(|val| from_str(&val).unwrap_or(None)))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
// Set's user data to local storage.
|
||||
pub async fn set_user_data(&self, data: Option<&UserData>) {
|
||||
let key = to_js(USER_DATA_KEY).expect("Failed to serialize key");
|
||||
pub fn set_user_data(&self, data: Option<&UserData>) {
|
||||
if let Some(data) = data {
|
||||
let data = data.clone();
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
object_store
|
||||
.put_kv(&key, &convert_to_io_error(to_js(&data))?)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.set(
|
||||
"user_data",
|
||||
&to_string(data).expect("Failed to desrialize user_data"),
|
||||
)
|
||||
.expect("Failed to set user_data");
|
||||
} else {
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
object_store.delete(&key).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.delete("user_data")
|
||||
.expect("Failed to delete user_data");
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let mut keys = Vec::new();
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
let key_vec = object_store.get_all_keys(None).await?;
|
||||
for k in key_vec {
|
||||
if let Ok(v) = from_value(k) {
|
||||
keys.push(v);
|
||||
fn get_storage_keys(&self) -> Vec<String> {
|
||||
let mut keys = Vec::new();
|
||||
for idx in 0..self.store.length().unwrap() {
|
||||
if let Some(k) = self.store.key(idx).expect("Failed to get storage key") {
|
||||
keys.push(k)
|
||||
}
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn migrate_local_store(&self) {
|
||||
for k in self.get_storage_keys().into_iter().filter(|k| {
|
||||
k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples"
|
||||
}) {
|
||||
// Deleting old local store key
|
||||
debug!("Deleting old local store key {}", k);
|
||||
self.store.delete(&k).expect("Failed to delete storage key");
|
||||
}
|
||||
}
|
||||
|
||||
fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
||||
self.get_storage_keys()
|
||||
.into_iter()
|
||||
.filter(|k| k.starts_with("recipe:"))
|
||||
}
|
||||
|
||||
/// Gets all the recipes from local storage.
|
||||
pub fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
|
||||
let mut recipe_list = Vec::new();
|
||||
for recipe_key in self.get_recipe_keys() {
|
||||
if let Some(entry) = self
|
||||
.store
|
||||
.get(&recipe_key)
|
||||
.expect(&format!("Failed to get recipe: {}", recipe_key))
|
||||
{
|
||||
match from_str(&entry) {
|
||||
Ok(entry) => {
|
||||
recipe_list.push(entry);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(recipe_key, err = ?e, "Failed to parse recipe entry");
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to get storage keys")
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
if recipe_list.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(recipe_list)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Gets all the recipes from local storage.
|
||||
pub async fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
|
||||
pub fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
|
||||
let key = recipe_key(id);
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let mut recipe_list = Vec::new();
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
let mut c = object_store.cursor().open().await?;
|
||||
while let Some(value) = c.value() {
|
||||
recipe_list.push(convert_to_io_error(from_value(value))?);
|
||||
c.advance(1).await?;
|
||||
}
|
||||
if recipe_list.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(recipe_list))
|
||||
})
|
||||
.await
|
||||
.expect("Failed to get recipes")
|
||||
.get(&key)
|
||||
.expect(&format!("Failed to get recipe {}", key))
|
||||
.map(|entry| from_str(&entry).expect(&format!("Failed to get recipe {}", key)))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
|
||||
let key = to_js(id).expect("Failed to serialize key");
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
let entry: Option<RecipeEntry> = match object_store.get(&key).await? {
|
||||
Some(v) => convert_to_io_error(from_value(v))?,
|
||||
None => None,
|
||||
};
|
||||
Ok(entry)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to get recipes")
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Sets the set of recipes to the entries passed in. Deletes any recipes not
|
||||
/// in the list.
|
||||
pub async fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
|
||||
for recipe_key in self.get_recipe_keys().await {
|
||||
let key = to_js(&recipe_key).expect("Failed to serialize key");
|
||||
pub fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
|
||||
for recipe_key in self.get_recipe_keys() {
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
object_store.delete(&key).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to delete user_data");
|
||||
.delete(&recipe_key)
|
||||
.expect(&format!("Failed to get recipe {}", recipe_key));
|
||||
}
|
||||
for entry in entries {
|
||||
let entry = entry.clone();
|
||||
let key = to_js(entry.recipe_id()).expect("Failed to serialize recipe key");
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
object_store
|
||||
.put_kv(&key, &convert_to_io_error(to_js(&entry))?)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to store recipe entry");
|
||||
self.set_recipe_entry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Set recipe entry in local storage.
|
||||
pub async fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
||||
let entry = entry.clone();
|
||||
let key = to_js(entry.recipe_id()).expect("Failed to serialize recipe key");
|
||||
pub fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
object_store
|
||||
.put_kv(&key, &convert_to_io_error(to_js(&entry))?)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to store recipe entry");
|
||||
.set(
|
||||
&recipe_key(entry.recipe_id()),
|
||||
&to_string(&entry).expect(&format!("Failed to get recipe {}", entry.recipe_id())),
|
||||
)
|
||||
.expect(&format!("Failed to store recipe {}", entry.recipe_id()))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Delete recipe entry from local storage.
|
||||
pub async fn delete_recipe_entry(&self, recipe_id: &str) {
|
||||
let key = to_js(recipe_id).expect("Failed to serialize key");
|
||||
pub fn delete_recipe_entry(&self, recipe_id: &str) {
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
object_store.delete(&key).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to delete user_data");
|
||||
.delete(&recipe_key(recipe_id))
|
||||
.expect(&format!("Failed to delete recipe {}", recipe_id))
|
||||
}
|
||||
}
|
||||
|
||||
@ -487,7 +365,7 @@ impl HttpStore {
|
||||
Ok(resp) => resp,
|
||||
Err(gloo_net::Error::JsError(err)) => {
|
||||
error!(path, ?err, "Error hitting api");
|
||||
return Ok(self.local_store.get_recipes().await);
|
||||
return Ok(self.local_store.get_recipes());
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err)?;
|
||||
@ -517,7 +395,7 @@ impl HttpStore {
|
||||
Ok(resp) => resp,
|
||||
Err(gloo_net::Error::JsError(err)) => {
|
||||
error!(path, ?err, "Error hitting api");
|
||||
return Ok(self.local_store.get_recipe_entry(id.as_ref()).await);
|
||||
return Ok(self.local_store.get_recipe_entry(id.as_ref()));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err)?;
|
||||
@ -537,7 +415,7 @@ impl HttpStore {
|
||||
.as_success()
|
||||
.unwrap();
|
||||
if let Some(ref entry) = entry {
|
||||
self.local_store.set_recipe_entry(entry).await;
|
||||
self.local_store.set_recipe_entry(entry);
|
||||
}
|
||||
Ok(entry)
|
||||
}
|
||||
|
@ -37,14 +37,12 @@ fn bool_true() -> bool {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AppState {
|
||||
pub recipe_counts: BTreeMap<String, u32>,
|
||||
pub recipe_counts: BTreeMap<String, usize>,
|
||||
pub recipe_categories: BTreeMap<String, String>,
|
||||
pub extras: Vec<(String, String)>,
|
||||
// FIXME(jwall): This should really be storable I think?
|
||||
#[serde(skip_deserializing,skip_serializing)]
|
||||
#[serde(skip)] // FIXME(jwall): This should really be storable I think?
|
||||
pub staples: Option<BTreeSet<Ingredient>>,
|
||||
// FIXME(jwall): This should really be storable I think?
|
||||
#[serde(skip_deserializing,skip_serializing)]
|
||||
#[serde(skip)] // FIXME(jwall): This should really be storable I think?
|
||||
pub recipes: BTreeMap<String, Recipe>,
|
||||
pub category_map: BTreeMap<String, String>,
|
||||
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
@ -77,7 +75,7 @@ impl AppState {
|
||||
|
||||
pub enum Message {
|
||||
ResetRecipeCounts,
|
||||
UpdateRecipeCount(String, u32),
|
||||
UpdateRecipeCount(String, usize),
|
||||
AddExtra(String, String),
|
||||
RemoveExtra(usize),
|
||||
UpdateExtra(usize, String, String),
|
||||
@ -171,7 +169,6 @@ impl StateMachine {
|
||||
Self { store, local_store }
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn load_state(
|
||||
store: &HttpStore,
|
||||
local_store: &LocalStore,
|
||||
@ -181,7 +178,7 @@ impl StateMachine {
|
||||
// call set on the signal once. When the LinearSignal get's dropped it
|
||||
// will call set on the contained Signal.
|
||||
let mut original: LinearSignal<AppState> = original.into();
|
||||
if let Some(state) = local_store.fetch_app_state().await {
|
||||
if let Some(state) = local_store.fetch_app_state() {
|
||||
original = original.update(state);
|
||||
}
|
||||
let mut state = original.get().as_ref().clone();
|
||||
@ -204,7 +201,7 @@ impl StateMachine {
|
||||
|
||||
info!("Synchronizing recipe");
|
||||
if let Some(recipe_entries) = recipe_entries {
|
||||
local_store.set_all_recipes(recipe_entries).await;
|
||||
local_store.set_all_recipes(recipe_entries);
|
||||
state.recipe_categories = recipe_entries
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
@ -239,7 +236,7 @@ impl StateMachine {
|
||||
// set the counts.
|
||||
let mut plan_map = BTreeMap::new();
|
||||
for (id, count) in plan {
|
||||
plan_map.insert(id, count as u32);
|
||||
plan_map.insert(id, count as usize);
|
||||
}
|
||||
state.recipe_counts = plan_map;
|
||||
for (id, _) in state.recipes.iter() {
|
||||
@ -258,11 +255,11 @@ impl StateMachine {
|
||||
info!("Checking for user account data");
|
||||
if let Some(user_data) = store.fetch_user_data().await {
|
||||
debug!("Successfully got account data from server");
|
||||
local_store.set_user_data(Some(&user_data)).await;
|
||||
local_store.set_user_data(Some(&user_data));
|
||||
state.auth = Some(user_data);
|
||||
} else {
|
||||
debug!("Using account data from local store");
|
||||
let user_data = local_store.get_user_data().await;
|
||||
let user_data = local_store.get_user_data();
|
||||
state.auth = user_data;
|
||||
}
|
||||
info!("Synchronizing categories");
|
||||
@ -296,7 +293,7 @@ impl StateMachine {
|
||||
}
|
||||
}
|
||||
// Finally we store all of this app state back to our localstore
|
||||
local_store.store_app_state(&state).await;
|
||||
local_store.store_app_state(&state);
|
||||
original.update(state);
|
||||
Ok(())
|
||||
}
|
||||
@ -352,9 +349,8 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
.or_insert(cat);
|
||||
}
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
self.local_store.set_recipe_entry(&entry);
|
||||
spawn_local_scoped(cx, async move {
|
||||
local_store.set_recipe_entry(&entry).await;
|
||||
if let Err(e) = store.store_recipes(vec![entry]).await {
|
||||
// FIXME(jwall): We should have a global way to trigger error messages
|
||||
error!(err=?e, "Unable to save Recipe");
|
||||
@ -367,10 +363,9 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
Message::RemoveRecipe(recipe, callback) => {
|
||||
original_copy.recipe_counts.remove(&recipe);
|
||||
original_copy.recipes.remove(&recipe);
|
||||
self.local_store.delete_recipe_entry(&recipe);
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
local_store.delete_recipe_entry(&recipe).await;
|
||||
if let Err(err) = store.delete_recipe(&recipe).await {
|
||||
error!(?err, "Failed to delete recipe");
|
||||
}
|
||||
@ -401,11 +396,8 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
original_copy.modified_amts.insert(key, amt);
|
||||
}
|
||||
Message::SetUserData(user_data) => {
|
||||
let local_store = self.local_store.clone();
|
||||
original_copy.auth = Some(user_data.clone());
|
||||
spawn_local_scoped(cx, async move {
|
||||
local_store.set_user_data(Some(&user_data)).await;
|
||||
});
|
||||
self.local_store.set_user_data(Some(&user_data));
|
||||
original_copy.auth = Some(user_data);
|
||||
}
|
||||
Message::SaveState(f) => {
|
||||
let mut original_copy = original_copy.clone();
|
||||
@ -425,7 +417,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
if let Err(e) = store.store_app_state(&original_copy).await {
|
||||
error!(err=?e, "Error saving app state");
|
||||
};
|
||||
local_store.store_app_state(&original_copy).await;
|
||||
local_store.store_app_state(&original_copy);
|
||||
original.set(original_copy);
|
||||
f.map(|f| f());
|
||||
});
|
||||
@ -471,7 +463,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
// Note(jwall): This is a little unusual but because this
|
||||
// is async code we can't rely on the set below.
|
||||
original_copy.recipe_counts =
|
||||
BTreeMap::from_iter(plan.drain(0..).map(|(k, v)| (k, v as u32)));
|
||||
BTreeMap::from_iter(plan.drain(0..).map(|(k, v)| (k, v as usize)));
|
||||
}
|
||||
let (filtered, modified, extras) = store
|
||||
.fetch_inventory_for_date(&date)
|
||||
@ -486,7 +478,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
.store_plan_for_date(vec![], &date)
|
||||
.await
|
||||
.expect("Failed to init meal plan for date");
|
||||
local_store.store_app_state(&original_copy).await;
|
||||
local_store.store_app_state(&original_copy);
|
||||
original.set(original_copy);
|
||||
|
||||
callback.map(|f| f());
|
||||
@ -509,7 +501,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
original_copy.filtered_ingredients = BTreeSet::new();
|
||||
original_copy.modified_amts = BTreeMap::new();
|
||||
original_copy.extras = Vec::new();
|
||||
local_store.store_app_state(&original_copy).await;
|
||||
local_store.store_app_state(&original_copy);
|
||||
original.set(original_copy);
|
||||
|
||||
callback.map(|f| f());
|
||||
@ -521,13 +513,8 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
return;
|
||||
}
|
||||
}
|
||||
spawn_local_scoped(cx, {
|
||||
let local_store = self.local_store.clone();
|
||||
async move {
|
||||
local_store.store_app_state(&original_copy).await;
|
||||
original.set(original_copy);
|
||||
}
|
||||
});
|
||||
self.local_store.store_app_state(&original_copy);
|
||||
original.set(original_copy);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,19 +42,19 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View
|
||||
} else {
|
||||
Some(category)
|
||||
};
|
||||
RecipeEntry {
|
||||
id: recipe_title
|
||||
RecipeEntry(
|
||||
recipe_title
|
||||
.get()
|
||||
.as_ref()
|
||||
.to_lowercase()
|
||||
.replace(" ", "_")
|
||||
.replace("\n", ""),
|
||||
text: STARTER_RECIPE
|
||||
STARTER_RECIPE
|
||||
.replace("TITLE_PLACEHOLDER", recipe_title.get().as_str())
|
||||
.replace("\r", ""),
|
||||
category,
|
||||
serving_count: None,
|
||||
}
|
||||
None,
|
||||
)
|
||||
});
|
||||
|
||||
view! {cx,
|
||||
|
@ -115,12 +115,12 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>)
|
||||
} else {
|
||||
Some(category.as_ref().clone())
|
||||
};
|
||||
let recipe_entry = RecipeEntry {
|
||||
id: id.get_untracked().as_ref().clone(),
|
||||
text: text.get_untracked().as_ref().clone(),
|
||||
let recipe_entry = RecipeEntry(
|
||||
id.get_untracked().as_ref().clone(),
|
||||
text.get_untracked().as_ref().clone(),
|
||||
category,
|
||||
serving_count: None,
|
||||
};
|
||||
None,
|
||||
);
|
||||
sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None));
|
||||
dirty.set(false);
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ pub fn RecipeSelection<'ctx, G: Html>(
|
||||
label(for=for_id, class="flex-item-grow") { a(href=href) { (*title) } }
|
||||
NumberField(name=name, class="flex-item-shrink".to_string(), counter=count, min=0.0, on_change=Some(move |_| {
|
||||
debug!(idx=%id, count=%(*count.get_untracked()), "setting recipe count");
|
||||
sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as u32));
|
||||
sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as usize));
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -11,123 +11,17 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use anyhow::{Context, Result};
|
||||
use indexed_db::{self, Database, Factory, Transaction};
|
||||
use js_sys::Date;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use tracing::error;
|
||||
use web_sys::{window, Window};
|
||||
use web_sys::{window, Storage, Window};
|
||||
|
||||
pub fn get_storage() -> web_sys::Storage {
|
||||
pub fn get_storage() -> Storage {
|
||||
get_window()
|
||||
.local_storage()
|
||||
.expect("Failed to get storage")
|
||||
.expect("No storage available")
|
||||
}
|
||||
|
||||
pub const STATE_STORE_NAME: &'static str = "state-store";
|
||||
pub const RECIPE_STORE_NAME: &'static str = "recipe-store";
|
||||
pub const SERVING_COUNT_IDX: &'static str = "recipe-serving-count";
|
||||
pub const CATEGORY_IDX: &'static str = "recipe-category";
|
||||
pub const DB_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DBFactory<'name> {
|
||||
name: &'name str,
|
||||
version: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for DBFactory<'static> {
|
||||
fn default() -> Self {
|
||||
DBFactory {
|
||||
name: STATE_STORE_NAME,
|
||||
version: Some(DB_VERSION),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn version1_setup<'db>(
|
||||
stores: &HashSet<String>,
|
||||
db: &'db Database<std::io::Error>,
|
||||
) -> Result<(), indexed_db::Error<std::io::Error>> {
|
||||
// We use out of line keys for this object store
|
||||
if !stores.contains(STATE_STORE_NAME) {
|
||||
db.build_object_store(STATE_STORE_NAME).create()?;
|
||||
}
|
||||
if !stores.contains(RECIPE_STORE_NAME) {
|
||||
let recipe_store = db.build_object_store(RECIPE_STORE_NAME).create()?;
|
||||
recipe_store
|
||||
.build_index(CATEGORY_IDX, "category")
|
||||
.create()?;
|
||||
recipe_store
|
||||
.build_index(SERVING_COUNT_IDX, "serving_count")
|
||||
.create()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'name> DBFactory<'name> {
|
||||
pub async fn get_indexed_db(&self) -> Result<Database<std::io::Error>> {
|
||||
let factory = Factory::<std::io::Error>::get().context("opening IndexedDB")?;
|
||||
let db = factory
|
||||
.open(self.name, self.version.unwrap_or(0), |evt| async move {
|
||||
// NOTE(zaphar): This is the on upgradeneeded handler. It get's called on new databases or
|
||||
// databases with an older version than the one we requested to build.
|
||||
let db = evt.database();
|
||||
let stores = db
|
||||
.object_store_names()
|
||||
.into_iter()
|
||||
.collect::<HashSet<String>>();
|
||||
// NOTE(jwall): This needs to be somewhat clever in handling version upgrades.
|
||||
if db.version() > 0 {
|
||||
version1_setup(&stores, db).await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context(format!("Opening or creating the database {}", self.name))?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
pub async fn rw_transaction<Fun, RetFut, Ret>(
|
||||
&self,
|
||||
stores: &[&str],
|
||||
transaction: Fun,
|
||||
) -> indexed_db::Result<Ret, std::io::Error>
|
||||
where
|
||||
Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut,
|
||||
RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>,
|
||||
Ret: 'static,
|
||||
{
|
||||
self.get_indexed_db()
|
||||
.await
|
||||
.expect("Failed to open database")
|
||||
.transaction(stores)
|
||||
.rw()
|
||||
.run(transaction)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn ro_transaction<Fun, RetFut, Ret>(
|
||||
&self,
|
||||
stores: &[&str],
|
||||
transaction: Fun,
|
||||
) -> indexed_db::Result<Ret, std::io::Error>
|
||||
where
|
||||
Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut,
|
||||
RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>,
|
||||
Ret: 'static,
|
||||
{
|
||||
self.get_indexed_db()
|
||||
.await
|
||||
.expect("Failed to open database")
|
||||
.transaction(stores)
|
||||
.run(transaction)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ms_timestamp() -> u32 {
|
||||
Date::new_0().get_milliseconds()
|
||||
}
|
||||
|
@ -20,22 +20,20 @@ use crate::{api, routing::Handler as RouteHandler};
|
||||
#[instrument]
|
||||
#[component]
|
||||
pub fn UI<G: Html>(cx: Scope) -> View<G> {
|
||||
let view = create_signal(cx, View::empty());
|
||||
api::HttpStore::provide_context(cx, "/api".to_owned());
|
||||
let store = api::HttpStore::get_from_context(cx).as_ref().clone();
|
||||
info!("Starting UI");
|
||||
let local_store = api::LocalStore::new();
|
||||
let app_state = if let Some(app_state) = local_store.fetch_app_state() {
|
||||
app_state
|
||||
} else {
|
||||
crate::app_state::AppState::new()
|
||||
};
|
||||
debug!(?app_state, "Loaded app state from local storage");
|
||||
let sh = crate::app_state::get_state_handler(cx, app_state, store);
|
||||
let view = create_signal(cx, View::empty());
|
||||
spawn_local_scoped(cx, {
|
||||
async move {
|
||||
let local_store = api::LocalStore::new();
|
||||
// TODO(jwall): At some point we can drop this potentially?
|
||||
local_store.migrate().await;
|
||||
let app_state = if let Some(app_state) = local_store.fetch_app_state().await {
|
||||
app_state
|
||||
} else {
|
||||
crate::app_state::AppState::new()
|
||||
};
|
||||
debug!(?app_state, "Loaded app state from local storage");
|
||||
let sh = crate::app_state::get_state_handler(cx, app_state, store);
|
||||
sh.dispatch(cx, Message::LoadState(None));
|
||||
view.set(view! { cx,
|
||||
RouteHandler(sh=sh)
|
||||
|
Loading…
x
Reference in New Issue
Block a user