Compare commits

...

8 Commits

Author SHA1 Message Date
aba1e114cf maid: cleanup warnings 2024-07-13 20:11:11 -04:00
9126d434d5 fix: Turns out that indexed db doesn't support Map
Had to modify the wasm-bindgen serialization to use objects instead
of the Map type.
2024-07-12 21:54:26 -04:00
548f336e1a dev: use better recipe keys
Having a dedicated object store makes a number of things simpler.
2024-07-12 19:18:24 -04:00
9f3b11a01f dev: migrate recipes from localstorage to indexeddb 2024-07-12 19:12:15 -04:00
f173204d2d dev: use cursor for the get_all_recipes 2024-07-12 18:48:25 -04:00
1f90cc2ef6 feat: migrate user_data and app_state
from localstorage to indexeddb
2024-07-12 18:26:13 -04:00
ed44e929f4 refactor: cleanup and make our upgrade logic more robust 2024-07-12 18:05:20 -04:00
51d165a50b dev: indexeddb indexes on recipe category and serving_count 2024-07-12 17:51:25 -04:00
10 changed files with 287 additions and 295 deletions

View File

@ -99,7 +99,7 @@ impl AsyncFileStore {
let file_name = entry.file_name().to_string_lossy().to_string(); let file_name = entry.file_name().to_string_lossy().to_string();
debug!("adding recipe file {}", file_name); debug!("adding recipe file {}", file_name);
let recipe_contents = read_to_string(entry.path()).await?; let recipe_contents = read_to_string(entry.path()).await?;
entry_vec.push(RecipeEntry(file_name, recipe_contents, None, None)); entry_vec.push(RecipeEntry::new(file_name, recipe_contents));
} else { } else {
warn!( warn!(
file = %entry.path().to_string_lossy(), file = %entry.path().to_string_lossy(),
@ -119,12 +119,12 @@ impl AsyncFileStore {
if recipe_path.exists().await && recipe_path.is_file().await { if recipe_path.exists().await && recipe_path.is_file().await {
debug!("Found recipe file {}", recipe_path.to_string_lossy()); debug!("Found recipe file {}", recipe_path.to_string_lossy());
let recipe_contents = read_to_string(recipe_path).await?; let recipe_contents = read_to_string(recipe_path).await?;
return Ok(Some(RecipeEntry( return Ok(Some(RecipeEntry {
id.as_ref().to_owned(), id: id.as_ref().to_owned(),
recipe_contents, text: recipe_contents,
None, category: None,
None, serving_count: None,
))); }));
} else { } else {
return Ok(None); return Ok(None);
} }

View File

@ -440,12 +440,12 @@ impl APIStore for SqliteStore {
.await? .await?
.iter() .iter()
.map(|row| { .map(|row| {
RecipeEntry( RecipeEntry {
row.recipe_id.clone(), id: row.recipe_id.clone(),
row.recipe_text.clone().unwrap_or_else(|| String::new()), text: row.recipe_text.clone().unwrap_or_else(|| String::new()),
row.category.clone(), category: row.category.clone(),
row.serving_count.clone(), serving_count: row.serving_count.clone(),
) }
}) })
.nth(0); .nth(0);
Ok(entry) Ok(entry)
@ -460,12 +460,12 @@ impl APIStore for SqliteStore {
.await? .await?
.iter() .iter()
.map(|row| { .map(|row| {
RecipeEntry( RecipeEntry {
row.recipe_id.clone(), id: row.recipe_id.clone(),
row.recipe_text.clone().unwrap_or_else(|| String::new()), text: row.recipe_text.clone().unwrap_or_else(|| String::new()),
row.category.clone(), category: row.category.clone(),
row.serving_count.clone(), serving_count: row.serving_count.clone(),
) }
}) })
.collect(); .collect();
Ok(Some(rows)) Ok(Some(rows))

View File

@ -50,39 +50,49 @@ impl Mealplan {
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RecipeEntry(pub String, pub String, pub Option<String>, pub Option<i64>); pub struct RecipeEntry {
pub id: String,
pub text: String,
pub category: Option<String>,
pub serving_count: Option<i64>,
}
impl RecipeEntry { impl RecipeEntry {
pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self { pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self {
Self(recipe_id.into(), text.into(), None, None) Self {
id: recipe_id.into(),
text: text.into(),
category: None,
serving_count: None,
}
} }
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) { pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
self.0 = id.into(); self.id = id.into();
} }
pub fn recipe_id(&self) -> &str { pub fn recipe_id(&self) -> &str {
self.0.as_str() self.id.as_str()
} }
pub fn set_recipe_text<S: Into<String>>(&mut self, text: S) { pub fn set_recipe_text<S: Into<String>>(&mut self, text: S) {
self.1 = text.into(); self.text = text.into();
} }
pub fn recipe_text(&self) -> &str { pub fn recipe_text(&self) -> &str {
self.1.as_str() self.text.as_str()
} }
pub fn set_category<S: Into<String>>(&mut self, cat: S) { pub fn set_category<S: Into<String>>(&mut self, cat: S) {
self.2 = Some(cat.into()); self.category = Some(cat.into());
} }
pub fn category(&self) -> Option<&String> { pub fn category(&self) -> Option<&String> {
self.2.as_ref() self.category.as_ref()
} }
pub fn serving_count(&self) -> Option<i64> { pub fn serving_count(&self) -> Option<i64> {
self.3.clone() self.serving_count.clone()
} }
} }
@ -210,7 +220,7 @@ pub struct Step {
impl Step { impl Step {
pub fn new<S: Into<String>>(prep_time: Option<std::time::Duration>, instructions: S) -> Self { pub fn new<S: Into<String>>(prep_time: Option<std::time::Duration>, instructions: S) -> Self {
Self { Self {
prep_time: prep_time, prep_time,
instructions: instructions.into(), instructions: instructions.into(),
ingredients: Vec::new(), ingredients: Vec::new(),
} }

View File

@ -17,17 +17,22 @@ use base64::{self, Engine};
use chrono::NaiveDate; use chrono::NaiveDate;
use gloo_net; use gloo_net;
// TODO(jwall): Remove this when we have gone a few migrations past. // TODO(jwall): Remove this when we have gone a few migrations past.
//use serde_json::{from_str, to_string}; use serde_json::from_str;
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, error, instrument}; use tracing::{debug, error, instrument};
use anyhow::Result; use anyhow::Result;
use client_api::*; use client_api::*;
use recipes::{IngredientKey, RecipeEntry}; use recipes::{IngredientKey, RecipeEntry};
use serde_wasm_bindgen::{from_value, to_value}; use serde_wasm_bindgen::{from_value, Serializer};
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
// TODO(jwall): Remove this when we have gone a few migrations past. // TODO(jwall): Remove this when we have gone a few migrations past.
//use web_sys::Storage; 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::{ use crate::{
app_state::{parse_recipes, AppState}, app_state::{parse_recipes, AppState},
@ -80,75 +85,107 @@ 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 { fn token68(user: String, pass: String) -> String {
base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass)) 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)] #[derive(Clone, Debug)]
pub struct LocalStore { pub struct LocalStore {
// FIXME(zaphar): Migration from local storage to indexed db // TODO(zaphar): Remove this when it's safe to delete the migration
//old_store: Storage, old_store: Storage,
store: DBFactory<'static>, store: DBFactory<'static>,
} }
const APP_STATE_KEY: &'static str = "app-state"; const APP_STATE_KEY: &'static str = "app-state";
const USER_DATA_KEY: &'static str = "user_data";
impl LocalStore { impl LocalStore {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
store: DBFactory::default(), store: DBFactory::default(),
//old_store: js_lib::get_storage(), old_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) { pub async fn store_app_state(&self, state: &AppState) {
//self.migrate_local_store().await; let state = match to_js(state) {
let state = match to_value(state) {
Ok(state) => state, Ok(state) => state,
Err(err) => { Err(err) => {
error!(?err, ?state, "Error deserializing app_state"); error!(?err, ?state, "Error deserializing app_state");
return; return;
} }
}; };
let key = to_value(APP_STATE_KEY).expect("Failed to serialize key"); web_sys::console::log_1(&state);
let key = to_js(APP_STATE_KEY).expect("Failed to serialize key");
self.store self.store
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move { .rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
let object_store = trx let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
.object_store(js_lib::STATE_STORE_NAME) object_store.put_kv(&key, &state).await?;
.expect("Failed to get object store");
object_store
.put_kv(
&key,
&state,
)
.await
.expect("Failed to write to object store");
Ok(()) Ok(())
}) })
.await .await
.expect("Failed to store app-state"); .expect("Failed to store app-state");
// FIXME(zaphar): Migration from local storage to indexed db
//self.store
// .set("app_state", &state)
// .expect("Failed to set our app state");
} }
#[instrument]
pub async fn fetch_app_state(&self) -> Option<AppState> { pub async fn fetch_app_state(&self) -> Option<AppState> {
debug!("Loading state from local store"); debug!("Loading state from local store");
let recipes = parse_recipes(&self.get_recipes().await).expect("Failed to parse recipes"); let recipes = parse_recipes(&self.get_recipes().await).expect("Failed to parse recipes");
self.store self.store
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move { .ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
let key = to_value(APP_STATE_KEY).expect("Failed to serialize key"); let key = convert_to_io_error(to_js(APP_STATE_KEY))?;
let object_store = trx let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
.object_store(js_lib::STATE_STORE_NAME) let mut app_state: AppState = match object_store.get(&key).await? {
.expect("Failed to get object store"); Some(s) => convert_to_io_error(from_value(s))?,
let mut app_state: AppState =
match object_store.get(&key).await.expect("Failed to read from") {
Some(s) => from_value(s).expect("Failed to deserialize app state"),
None => return Ok(None), None => return Ok(None),
}; };
@ -163,112 +200,60 @@ impl LocalStore {
}) })
.await .await
.expect("Failed to fetch app-state") .expect("Failed to fetch app-state")
// FIXME(zaphar): Migration from local storage to indexed db
//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 {
// debug!(id, "Adding recipe from local storage");
// app_state.recipes.insert(id, recipe);
// }
// }
// app_state
// })
//})
} }
#[instrument]
/// Gets user data from local storage. /// Gets user data from local storage.
pub async fn get_user_data(&self) -> Option<UserData> { pub async fn get_user_data(&self) -> Option<UserData> {
self.store self.store
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move { .ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
let key = to_value("user_data").expect("Failed to serialize key"); let key = to_js(USER_DATA_KEY).expect("Failed to serialize key");
let object_store = trx let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
.object_store(js_lib::STATE_STORE_NAME) let user_data: UserData = match object_store.get(&key).await? {
.expect("Failed to get object store"); Some(s) => convert_to_io_error(from_value(s))?,
let user_data: UserData = match object_store
.get(&key)
.await
.expect("Failed to read from object store")
{
Some(s) => from_value(s).expect("Failed to deserialize app state"),
None => return Ok(None), None => return Ok(None),
}; };
Ok(Some(user_data)) Ok(Some(user_data))
}) })
.await .await
.expect("Failed to fetch user_data") .expect("Failed to fetch user_data")
// FIXME(zaphar): Migration from local storage to indexed db
//self.store
// .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. // Set's user data to local storage.
pub async fn set_user_data(&self, data: Option<&UserData>) { pub async fn set_user_data(&self, data: Option<&UserData>) {
let key = to_value("user_data").expect("Failed to serialize key"); let key = to_js(USER_DATA_KEY).expect("Failed to serialize key");
if let Some(data) = data { if let Some(data) = data {
let data = data.clone(); let data = data.clone();
self.store self.store
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move { .rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
let object_store = trx let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
.object_store(js_lib::STATE_STORE_NAME)
.expect("Failed to get object store");
object_store object_store
.put_kv( .put_kv(&key, &convert_to_io_error(to_js(&data))?)
&key, .await?;
&to_value(&data).expect("failed to serialize UserData"),
)
.await
.expect("Failed to store user_data");
Ok(()) Ok(())
}) })
.await .await
.expect("Failed to set user_data"); .expect("Failed to set user_data");
// FIXME(zaphar): Migration from local storage to indexed db
//self.store
// .set(
// "user_data",
// &to_string(data).expect("Failed to desrialize user_data"),
// )
// .expect("Failed to set user_data");
} else { } else {
self.store self.store
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move { .rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
let object_store = trx let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
.object_store(js_lib::STATE_STORE_NAME) object_store.delete(&key).await?;
.expect("Failed to get object store");
object_store
.delete(&key)
.await
.expect("Failed to delete user_data");
Ok(()) Ok(())
}) })
.await .await
.expect("Failed to delete user_data"); .expect("Failed to delete user_data");
// FIXME(zaphar): Migration from local storage to indexed db
//self.store
// .delete("user_data")
// .expect("Failed to delete user_data");
} }
} }
#[instrument]
async fn get_recipe_keys(&self) -> impl Iterator<Item = String> { async fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
self.store self.store
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move { .ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
let mut keys = Vec::new(); let mut keys = Vec::new();
let object_store = trx let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
.object_store(js_lib::RECIPE_STORE_NAME) let key_vec = object_store.get_all_keys(None).await?;
.expect("Failed to get object store");
let key_vec = object_store
.get_all_keys(None)
.await
.expect("Failed to get keys from object_store");
for k in key_vec { for k in key_vec {
if let Ok(v) = from_value(k) { if let Ok(v) = from_value(k) {
keys.push(v); keys.push(v);
@ -277,158 +262,107 @@ impl LocalStore {
Ok(keys) Ok(keys)
}) })
.await .await
.expect("Failed to get storage keys").into_iter() .expect("Failed to get storage keys")
.into_iter()
} }
#[instrument]
/// Gets all the recipes from local storage. /// Gets all the recipes from local storage.
pub async fn get_recipes(&self) -> Option<Vec<RecipeEntry>> { pub async fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
let mut recipe_list = Vec::new();
for recipe_key in self.get_recipe_keys().await {
let key = to_value(&recipe_key).expect("Failed to serialize key");
let entry = self
.store
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
let object_store = trx
.object_store(js_lib::RECIPE_STORE_NAME)
.expect("Failed to get object store");
let entry: Option<RecipeEntry> = match object_store
.get(&key)
.await
.expect("Failed to get recipe from key")
{
Some(v) => from_value(v).expect("Failed to deserialize entry"),
None => None,
};
Ok(entry)
})
.await
.expect("Failed to get recipes");
if let Some(entry) = entry {
recipe_list.push(entry);
}
}
if recipe_list.is_empty() {
return None;
}
Some(recipe_list)
}
pub async fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
let key = to_value(&recipe_key(id)).expect("Failed to serialize key");
self.store self.store
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move { .ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
let object_store = trx let mut recipe_list = Vec::new();
.object_store(js_lib::RECIPE_STORE_NAME) let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
.expect("Failed to get object store"); let mut c = object_store.cursor().open().await?;
let entry: Option<RecipeEntry> = match object_store while let Some(value) = c.value() {
.get(&key) 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 .await
.expect("Failed to get recipe from key") .expect("Failed to get recipes")
{ }
Some(v) => from_value(v).expect("Failed to deserialize entry"),
#[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, None => None,
}; };
Ok(entry) Ok(entry)
}) })
.await .await
.expect("Failed to get recipes") .expect("Failed to get recipes")
// FIXME(zaphar): Migration from local storage to indexed db
//self.store
// .get(&key)
// .expect(&format!("Failed to get recipe {}", key))
// .map(|entry| from_str(&entry).expect(&format!("Failed to get recipe {}", key)))
} }
#[instrument]
/// Sets the set of recipes to the entries passed in. Deletes any recipes not /// Sets the set of recipes to the entries passed in. Deletes any recipes not
/// in the list. /// in the list.
pub async fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) { pub async fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
// FIXME(zaphar): Migration from local storage to indexed db
for recipe_key in self.get_recipe_keys().await { for recipe_key in self.get_recipe_keys().await {
let key = to_value(&recipe_key).expect("Failed to serialize key"); let key = to_js(&recipe_key).expect("Failed to serialize key");
self.store self.store
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move { .rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
let object_store = trx let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
.object_store(js_lib::STATE_STORE_NAME) object_store.delete(&key).await?;
.expect("Failed to get object store");
object_store
.delete(&key)
.await
.expect("Failed to delete user_data");
Ok(()) Ok(())
}) })
.await .await
.expect("Failed to delete user_data"); .expect("Failed to delete user_data");
//self.store
// .delete(&recipe_key)
// .expect(&format!("Failed to get recipe {}", recipe_key));
} }
for entry in entries { for entry in entries {
let entry = entry.clone(); let entry = entry.clone();
let key = let key = to_js(entry.recipe_id()).expect("Failed to serialize recipe key");
to_value(&recipe_key(entry.recipe_id())).expect("Failed to serialize recipe key"); self.store
self.store.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move { .rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
let object_store = trx let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
.object_store(js_lib::RECIPE_STORE_NAME)
.expect("Failed to get object store");
object_store object_store
.put_kv( .put_kv(&key, &convert_to_io_error(to_js(&entry))?)
&key, .await?;
&to_value(&entry).expect("Failed to serialize recipe entry"),
)
.await
.expect("Failed to store recipe_entry");
Ok(()) Ok(())
}).await.expect("Failed to store recipe entry"); })
//self.set_recipe_entry(entry).await; .await
.expect("Failed to store recipe entry");
} }
} }
#[instrument]
/// Set recipe entry in local storage. /// Set recipe entry in local storage.
pub async fn set_recipe_entry(&self, entry: &RecipeEntry) { pub async fn set_recipe_entry(&self, entry: &RecipeEntry) {
let entry = entry.clone(); let entry = entry.clone();
let key = to_value(&recipe_key(entry.recipe_id())).expect("Failed to serialize recipe key"); 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)
.expect("Failed to get object store");
object_store
.put_kv(
&key,
&to_value(&entry).expect("Failed to serialize recipe entry"),
)
.await
.expect("Failed to store recipe_entry");
Ok(())
}).await.expect("Failed to store recipe entry");
// FIXME(zaphar): Migration from local storage to indexed db
//self.store
// .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()))
}
/// Delete recipe entry from local storage.
pub async fn delete_recipe_entry(&self, recipe_id: &str) {
let key = to_value(recipe_id).expect("Failed to serialize key");
self.store self.store
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move { .rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
let object_store = trx let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
.object_store(js_lib::RECIPE_STORE_NAME)
.expect("Failed to get object store");
object_store object_store
.delete(&key) .put_kv(&key, &convert_to_io_error(to_js(&entry))?)
.await?;
Ok(())
})
.await .await
.expect("Failed to delete user_data"); .expect("Failed to store recipe entry");
}
#[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");
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(()) Ok(())
}) })
.await .await
.expect("Failed to delete user_data"); .expect("Failed to delete user_data");
// FIXME(zaphar): Migration from local storage to indexed db
//self.store
// .delete(&recipe_key(recipe_id))
// .expect(&format!("Failed to delete recipe {}", recipe_id))
} }
} }

View File

@ -37,7 +37,7 @@ fn bool_true() -> bool {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AppState { pub struct AppState {
pub recipe_counts: BTreeMap<String, usize>, pub recipe_counts: BTreeMap<String, u32>,
pub recipe_categories: BTreeMap<String, String>, pub recipe_categories: BTreeMap<String, String>,
pub extras: Vec<(String, String)>, pub extras: Vec<(String, String)>,
// FIXME(jwall): This should really be storable I think? // FIXME(jwall): This should really be storable I think?
@ -77,7 +77,7 @@ impl AppState {
pub enum Message { pub enum Message {
ResetRecipeCounts, ResetRecipeCounts,
UpdateRecipeCount(String, usize), UpdateRecipeCount(String, u32),
AddExtra(String, String), AddExtra(String, String),
RemoveExtra(usize), RemoveExtra(usize),
UpdateExtra(usize, String, String), UpdateExtra(usize, String, String),
@ -171,6 +171,7 @@ impl StateMachine {
Self { store, local_store } Self { store, local_store }
} }
#[instrument(skip_all)]
async fn load_state( async fn load_state(
store: &HttpStore, store: &HttpStore,
local_store: &LocalStore, local_store: &LocalStore,
@ -238,7 +239,7 @@ impl StateMachine {
// set the counts. // set the counts.
let mut plan_map = BTreeMap::new(); let mut plan_map = BTreeMap::new();
for (id, count) in plan { for (id, count) in plan {
plan_map.insert(id, count as usize); plan_map.insert(id, count as u32);
} }
state.recipe_counts = plan_map; state.recipe_counts = plan_map;
for (id, _) in state.recipes.iter() { for (id, _) in state.recipes.iter() {
@ -470,7 +471,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
// Note(jwall): This is a little unusual but because this // Note(jwall): This is a little unusual but because this
// is async code we can't rely on the set below. // is async code we can't rely on the set below.
original_copy.recipe_counts = original_copy.recipe_counts =
BTreeMap::from_iter(plan.drain(0..).map(|(k, v)| (k, v as usize))); BTreeMap::from_iter(plan.drain(0..).map(|(k, v)| (k, v as u32)));
} }
let (filtered, modified, extras) = store let (filtered, modified, extras) = store
.fetch_inventory_for_date(&date) .fetch_inventory_for_date(&date)

View File

@ -42,19 +42,19 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View
} else { } else {
Some(category) Some(category)
}; };
RecipeEntry( RecipeEntry {
recipe_title id: recipe_title
.get() .get()
.as_ref() .as_ref()
.to_lowercase() .to_lowercase()
.replace(" ", "_") .replace(" ", "_")
.replace("\n", ""), .replace("\n", ""),
STARTER_RECIPE text: STARTER_RECIPE
.replace("TITLE_PLACEHOLDER", recipe_title.get().as_str()) .replace("TITLE_PLACEHOLDER", recipe_title.get().as_str())
.replace("\r", ""), .replace("\r", ""),
category, category,
None, serving_count: None,
) }
}); });
view! {cx, view! {cx,

View File

@ -115,12 +115,12 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>)
} else { } else {
Some(category.as_ref().clone()) Some(category.as_ref().clone())
}; };
let recipe_entry = RecipeEntry( let recipe_entry = RecipeEntry {
id.get_untracked().as_ref().clone(), id: id.get_untracked().as_ref().clone(),
text.get_untracked().as_ref().clone(), text: text.get_untracked().as_ref().clone(),
category, category,
None, serving_count: None,
); };
sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None)); sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None));
dirty.set(false); dirty.set(false);
} }

View File

@ -68,7 +68,7 @@ pub fn RecipeSelection<'ctx, G: Html>(
label(for=for_id, class="flex-item-grow") { a(href=href) { (*title) } } 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 |_| { 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"); debug!(idx=%id, count=%(*count.get_untracked()), "setting recipe count");
sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as usize)); sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as u32));
})) }))
} }
} }

View File

@ -11,12 +11,13 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// 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 anyhow::{Context, Result};
use indexed_db::{self, Database, Factory, Transaction};
use js_sys::Date; use js_sys::Date;
use std::collections::HashSet;
use std::future::Future;
use tracing::error; use tracing::error;
use web_sys::{window, Window}; use web_sys::{window, Window};
use indexed_db::{self, Factory, Database, Transaction};
use anyhow::{Result, Context};
use std::future::Future;
pub fn get_storage() -> web_sys::Storage { pub fn get_storage() -> web_sys::Storage {
get_window() get_window()
@ -27,6 +28,8 @@ pub fn get_storage() -> web_sys::Storage {
pub const STATE_STORE_NAME: &'static str = "state-store"; pub const STATE_STORE_NAME: &'static str = "state-store";
pub const RECIPE_STORE_NAME: &'static str = "recipe-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; pub const DB_VERSION: u32 = 1;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -37,49 +40,91 @@ pub struct DBFactory<'name> {
impl Default for DBFactory<'static> { impl Default for DBFactory<'static> {
fn default() -> Self { fn default() -> Self {
DBFactory { name: STATE_STORE_NAME, version: Some(DB_VERSION) } 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> { impl<'name> DBFactory<'name> {
pub async fn get_indexed_db(&self) -> Result<Database<std::io::Error>> { pub async fn get_indexed_db(&self) -> Result<Database<std::io::Error>> {
let factory = Factory::<std::io::Error>::get().context("opening IndexedDB")?; let factory = Factory::<std::io::Error>::get().context("opening IndexedDB")?;
let db = factory.open(self.name, self.version.unwrap_or(0), |evt| async move { 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 // NOTE(zaphar): This is the on upgradeneeded handler. It get's called on new databases or
// database with an older version than the one we requested to build. // databases with an older version than the one we requested to build.
let db = evt.database(); 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. // NOTE(jwall): This needs to be somewhat clever in handling version upgrades.
if db.version() == 1 { if db.version() > 0 {
// We use out of line keys for this object store version1_setup(&stores, db).await?;
db.build_object_store(STATE_STORE_NAME).create()?;
db.build_object_store(RECIPE_STORE_NAME).create()?;
// TODO(jwall): Do we need indexes?
} }
Ok(()) Ok(())
}).await.context(format!("Opening or creating the database {}", self.name))?; })
.await
.context(format!("Opening or creating the database {}", self.name))?;
Ok(db) Ok(db)
} }
pub async fn rw_transaction<Fun, RetFut, Ret>(&self, stores: &[&str], transaction: Fun) -> indexed_db::Result<Ret, std::io::Error> pub async fn rw_transaction<Fun, RetFut, Ret>(
&self,
stores: &[&str],
transaction: Fun,
) -> indexed_db::Result<Ret, std::io::Error>
where where
Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut, Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut,
RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>, RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>,
Ret: 'static, Ret: 'static,
{ {
self.get_indexed_db().await.expect("Failed to open database") self.get_indexed_db()
.transaction(stores).rw() .await
.run(transaction).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> pub async fn ro_transaction<Fun, RetFut, Ret>(
&self,
stores: &[&str],
transaction: Fun,
) -> indexed_db::Result<Ret, std::io::Error>
where where
Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut, Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut,
RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>, RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>,
Ret: 'static, Ret: 'static,
{ {
self.get_indexed_db().await.expect("Failed to open database") self.get_indexed_db()
.await
.expect("Failed to open database")
.transaction(stores) .transaction(stores)
.run(transaction).await .run(transaction)
.await
} }
} }

View File

@ -27,6 +27,8 @@ pub fn UI<G: Html>(cx: Scope) -> View<G> {
spawn_local_scoped(cx, { spawn_local_scoped(cx, {
async move { async move {
let local_store = api::LocalStore::new(); 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 { let app_state = if let Some(app_state) = local_store.fetch_app_state().await {
app_state app_state
} else { } else {