dev: use indexdb instead of local storage in the storage layer

This commit is contained in:
Jeremy Wall 2024-07-11 23:58:17 -04:00
parent 4767115da6
commit b93edd2701
9 changed files with 1235 additions and 915 deletions

1610
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
serde = "<=1.0.171"
recipes = { path = "../recipes" } recipes = { path = "../recipes" }
chrono = "0.4.22" chrono = "0.4.22"
[dependencies.serde]
version = "1.0.204"
features = ["derive"]
[dependencies.axum] [dependencies.axum]
version = "0.5.16" version = "0.5.16"

View File

@ -18,14 +18,19 @@ async-trait = "0.1.57"
async-session = "3.0.0" async-session = "3.0.0"
ciborium = "0.2.0" ciborium = "0.2.0"
tower = "0.4.13" tower = "0.4.13"
serde = "<=1.0.171"
cookie = "0.17.0" cookie = "0.17.0"
chrono = "0.4.22"
metrics = "0.20.1" metrics = "0.20.1"
metrics-exporter-prometheus = "0.11.0" metrics-exporter-prometheus = "0.11.0"
futures = "0.3" futures = "0.3"
metrics-process = "1.0.8" metrics-process = "1.0.8"
[dependencies.chrono]
version = "0.4.22"
features = ["serde"]
[dependencies.serde]
version = "1.0.204"
[dependencies.argon2] [dependencies.argon2]
version = "0.5.0" version = "0.5.0"

View File

@ -8,8 +8,14 @@ edition = "2021"
[dependencies] [dependencies]
abortable_parser = "~0.2.6" abortable_parser = "~0.2.6"
chrono = "~0.4"
serde = "1.0.144" [dependencies.chrono]
version = "0.4.22"
features = ["serde"]
[dependencies.serde]
version = "1.0.204"
features = ["derive"]
[dependencies.num-rational] [dependencies.num-rational]
version = "~0.4.0" version = "~0.4.0"

View File

@ -29,9 +29,10 @@ wasm-web-component = { git = "https://github.com/zaphar/wasm-web-components.git"
maud = "*" maud = "*"
indexed-db = "0.4.1" indexed-db = "0.4.1"
anyhow = "1.0.86" anyhow = "1.0.86"
serde-wasm-bindgen = "0.6.5"
[dependencies.serde] [dependencies.serde]
version = "<=1.0.171" version = "1.0.204"
features = ["derive"] features = ["derive"]
[dependencies.tracing-subscriber] [dependencies.tracing-subscriber]

View File

@ -16,18 +16,22 @@ use std::collections::{BTreeMap, BTreeSet};
use base64::{self, Engine}; use base64::{self, Engine};
use chrono::NaiveDate; use chrono::NaiveDate;
use gloo_net; use gloo_net;
use serde_json::{from_str, to_string}; // TODO(jwall): Remove this when we have gone a few migrations past.
//use serde_json::{from_str, to_string};
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, error, instrument}; use tracing::{debug, error, instrument};
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 wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
use web_sys::Storage; // TODO(jwall): Remove this when we have gone a few migrations past.
//use web_sys::Storage;
use crate::{ use crate::{
app_state::{parse_recipes, AppState}, app_state::{parse_recipes, AppState},
js_lib, js_lib::{self, DBFactory},
}; };
#[allow(dead_code)] #[allow(dead_code)]
@ -86,38 +90,68 @@ fn token68(user: String, pass: String) -> String {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct LocalStore { pub struct LocalStore {
store: Storage, // FIXME(zaphar): Migration from local storage to indexed db
//old_store: Storage,
store: DBFactory<'static>,
} }
const APP_STATE_KEY: &'static str = "app-state";
impl LocalStore { impl LocalStore {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
store: js_lib::get_storage(), store: DBFactory::new("app-state", Some(0)),
//old_store: js_lib::get_storage(),
} }
} }
pub fn store_app_state(&self, state: &AppState) { pub async fn store_app_state(&self, state: &AppState) {
self.migrate_local_store(); self.migrate_local_store().await;
let state = match to_string(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");
self.store self.store
.set("app_state", &state) .rw_transaction(|trx| async move {
.expect("Failed to set our app state"); let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
object_store
.put_kv(
&key,
&state,
)
.await
.expect("Failed to write to object store");
Ok(())
})
.await
.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");
} }
pub 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");
self.store.get("app_state").map_or(None, |val| { let recipes = parse_recipes(&self.get_recipes().await).expect("Failed to parse recipes");
val.map(|s| { self.store
debug!("Found an app_state object"); .ro_transaction(|trx| async move {
let key = to_value(APP_STATE_KEY).expect("Failed to serialize key");
let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
let mut app_state: AppState = let mut app_state: AppState =
from_str(&s).expect("Failed to deserialize app state"); match object_store.get(&key).await.expect("Failed to read from") {
let recipes = parse_recipes(&self.get_recipes()).expect("Failed to parse recipes"); Some(s) => from_value(s).expect("Failed to deserialize app state"),
None => return Ok(None),
};
if let Some(recipes) = recipes { if let Some(recipes) = recipes {
debug!("Populating recipes"); debug!("Populating recipes");
for (id, recipe) in recipes { for (id, recipe) in recipes {
@ -125,79 +159,185 @@ impl LocalStore {
app_state.recipes.insert(id, recipe); app_state.recipes.insert(id, recipe);
} }
} }
app_state Ok(Some(app_state))
})
}) })
.await
.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
// })
//})
} }
/// Gets user data from local storage. /// Gets user data from local storage.
pub fn get_user_data(&self) -> Option<UserData> { pub async fn get_user_data(&self) -> Option<UserData> {
self.store self.store
.get("user_data") .ro_transaction(|trx| async move {
.map_or(None, |val| val.map(|val| from_str(&val).unwrap_or(None))) let key = to_value("user_data").expect("Failed to serialize key");
.flatten() let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
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),
};
Ok(Some(user_data))
})
.await
.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()
} }
// Set's user data to local storage. // Set's user data to local storage.
pub 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");
if let Some(data) = data { if let Some(data) = data {
let data = data.clone();
self.store self.store
.set( .rw_transaction(|trx| async move {
"user_data", let object_store = trx
&to_string(data).expect("Failed to desrialize user_data"), .object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
object_store
.put_kv(
&key,
&to_value(&data).expect("failed to serialize UserData"),
) )
.await
.expect("Failed to store user_data");
Ok(())
})
.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
.delete("user_data") .rw_transaction(|trx| async move {
let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
object_store
.delete(&key)
.await
.expect("Failed to delete user_data"); .expect("Failed to delete user_data");
Ok(())
})
.await
.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");
} }
} }
fn get_storage_keys(&self) -> Vec<String> { async fn get_storage_keys(&self) -> Vec<String> {
self.store
.ro_transaction(|trx| async move {
let mut keys = Vec::new(); let mut keys = Vec::new();
for idx in 0..self.store.length().unwrap() { let object_store = trx
if let Some(k) = self.store.key(idx).expect("Failed to get storage key") { .object_store(js_lib::STORE_NAME)
keys.push(k) .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 {
if let Ok(v) = from_value(k) {
keys.push(v);
} }
} }
keys Ok(keys)
})
.await
.expect("Failed to get storage keys")
} }
fn migrate_local_store(&self) { async fn migrate_local_store(&self) {
for k in self.get_storage_keys().into_iter().filter(|k| { // FIXME(zaphar): Migration from local storage to indexed db
for k in self.get_storage_keys().await.into_iter().filter(|k| {
k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples" k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples"
}) { }) {
// Deleting old local store key // Deleting old local store key
debug!("Deleting old local store key {}", k); let key = to_value(&k).expect("Failed to serialize key");
self.store.delete(&k).expect("Failed to delete storage key"); self.store
.rw_transaction(|trx| async move {
let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
object_store
.delete(&key)
.await
.expect("Failed to delete user_data");
Ok(())
})
.await
.expect("Failed to delete user_data");
//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> { async fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
self.get_storage_keys() self.get_storage_keys()
.await
.into_iter() .into_iter()
.filter(|k| k.starts_with("recipe:")) .filter(|k| k.starts_with("recipe:"))
} }
/// Gets all the recipes from local storage. /// Gets all the recipes from local storage.
pub fn get_recipes(&self) -> Option<Vec<RecipeEntry>> { pub async fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
let mut recipe_list = Vec::new(); let mut recipe_list = Vec::new();
for recipe_key in self.get_recipe_keys() { for recipe_key in self.get_recipe_keys().await {
if let Some(entry) = self let key = to_value(&recipe_key).expect("Failed to serialize key");
let entry = self
.store .store
.get(&recipe_key) .ro_transaction(|trx| async move {
.expect(&format!("Failed to get recipe: {}", recipe_key)) let object_store = trx
.object_store(js_lib::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")
{ {
match from_str(&entry) { Some(v) => from_value(v).expect("Failed to deserialize entry"),
Ok(entry) => { None => None,
};
Ok(entry)
})
.await
.expect("Failed to get recipes");
if let Some(entry) = entry {
recipe_list.push(entry); recipe_list.push(entry);
} }
Err(e) => {
error!(recipe_key, err = ?e, "Failed to parse recipe entry");
}
}
}
} }
if recipe_list.is_empty() { if recipe_list.is_empty() {
return None; return None;
@ -205,42 +345,122 @@ impl LocalStore {
Some(recipe_list) Some(recipe_list)
} }
pub fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> { pub async fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
let key = recipe_key(id); let key = to_value(&recipe_key(id)).expect("Failed to serialize key");
self.store self.store
.ro_transaction(|trx| async move {
let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
let entry: Option<RecipeEntry> = match object_store
.get(&key) .get(&key)
.expect(&format!("Failed to get recipe {}", key)) .await
.map(|entry| from_str(&entry).expect(&format!("Failed to get recipe {}", key))) .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")
// 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)))
} }
/// 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 fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) { pub async fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
for recipe_key in self.get_recipe_keys() { // FIXME(zaphar): Migration from local storage to indexed db
for recipe_key in self.get_recipe_keys().await {
let key = to_value(&recipe_key).expect("Failed to serialize key");
self.store self.store
.delete(&recipe_key) .rw_transaction(|trx| async move {
.expect(&format!("Failed to get recipe {}", recipe_key)); let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
object_store
.delete(&key)
.await
.expect("Failed to delete user_data");
Ok(())
})
.await
.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 {
self.set_recipe_entry(entry); let entry = entry.clone();
let key =
to_value(&recipe_key(entry.recipe_id())).expect("Failed to serialize recipe key");
self.store.rw_transaction(|trx| async move {
let object_store = trx
.object_store(js_lib::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");
//self.set_recipe_entry(entry).await;
} }
} }
/// Set recipe entry in local storage. /// Set recipe entry in local storage.
pub fn set_recipe_entry(&self, entry: &RecipeEntry) { pub async fn set_recipe_entry(&self, entry: &RecipeEntry) {
self.store let entry = entry.clone();
.set( let key = to_value(&recipe_key(entry.recipe_id())).expect("Failed to serialize recipe key");
&recipe_key(entry.recipe_id()), self.store.rw_transaction(|trx| async move {
&to_string(&entry).expect(&format!("Failed to get recipe {}", entry.recipe_id())), let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
object_store
.put_kv(
&key,
&to_value(&entry).expect("Failed to serialize recipe entry"),
) )
.expect(&format!("Failed to store recipe {}", entry.recipe_id())) .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. /// Delete recipe entry from local storage.
pub fn delete_recipe_entry(&self, recipe_id: &str) { 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
.delete(&recipe_key(recipe_id)) .rw_transaction(|trx| async move {
.expect(&format!("Failed to delete recipe {}", recipe_id)) let object_store = trx
.object_store(js_lib::STORE_NAME)
.expect("Failed to get object store");
object_store
.delete(&key)
.await
.expect("Failed to delete user_data");
Ok(())
})
.await
.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))
} }
} }
@ -365,7 +585,7 @@ impl HttpStore {
Ok(resp) => resp, Ok(resp) => resp,
Err(gloo_net::Error::JsError(err)) => { Err(gloo_net::Error::JsError(err)) => {
error!(path, ?err, "Error hitting api"); error!(path, ?err, "Error hitting api");
return Ok(self.local_store.get_recipes()); return Ok(self.local_store.get_recipes().await);
} }
Err(err) => { Err(err) => {
return Err(err)?; return Err(err)?;
@ -395,7 +615,7 @@ impl HttpStore {
Ok(resp) => resp, Ok(resp) => resp,
Err(gloo_net::Error::JsError(err)) => { Err(gloo_net::Error::JsError(err)) => {
error!(path, ?err, "Error hitting api"); error!(path, ?err, "Error hitting api");
return Ok(self.local_store.get_recipe_entry(id.as_ref())); return Ok(self.local_store.get_recipe_entry(id.as_ref()).await);
} }
Err(err) => { Err(err) => {
return Err(err)?; return Err(err)?;
@ -415,7 +635,7 @@ impl HttpStore {
.as_success() .as_success()
.unwrap(); .unwrap();
if let Some(ref entry) = entry { if let Some(ref entry) = entry {
self.local_store.set_recipe_entry(entry); self.local_store.set_recipe_entry(entry).await;
} }
Ok(entry) Ok(entry)
} }

View File

@ -40,9 +40,11 @@ pub struct AppState {
pub recipe_counts: BTreeMap<String, usize>, pub recipe_counts: BTreeMap<String, usize>,
pub recipe_categories: BTreeMap<String, String>, pub recipe_categories: BTreeMap<String, String>,
pub extras: Vec<(String, String)>, pub extras: Vec<(String, String)>,
#[serde(skip)] // FIXME(jwall): This should really be storable I think? // FIXME(jwall): This should really be storable I think?
#[serde(skip_deserializing,skip_serializing)]
pub staples: Option<BTreeSet<Ingredient>>, pub staples: Option<BTreeSet<Ingredient>>,
#[serde(skip)] // FIXME(jwall): This should really be storable I think? // FIXME(jwall): This should really be storable I think?
#[serde(skip_deserializing,skip_serializing)]
pub recipes: BTreeMap<String, Recipe>, pub recipes: BTreeMap<String, Recipe>,
pub category_map: BTreeMap<String, String>, pub category_map: BTreeMap<String, String>,
pub filtered_ingredients: BTreeSet<IngredientKey>, pub filtered_ingredients: BTreeSet<IngredientKey>,
@ -178,7 +180,7 @@ impl StateMachine {
// call set on the signal once. When the LinearSignal get's dropped it // call set on the signal once. When the LinearSignal get's dropped it
// will call set on the contained Signal. // will call set on the contained Signal.
let mut original: LinearSignal<AppState> = original.into(); let mut original: LinearSignal<AppState> = original.into();
if let Some(state) = local_store.fetch_app_state() { if let Some(state) = local_store.fetch_app_state().await {
original = original.update(state); original = original.update(state);
} }
let mut state = original.get().as_ref().clone(); let mut state = original.get().as_ref().clone();
@ -201,7 +203,7 @@ impl StateMachine {
info!("Synchronizing recipe"); info!("Synchronizing recipe");
if let Some(recipe_entries) = recipe_entries { if let Some(recipe_entries) = recipe_entries {
local_store.set_all_recipes(recipe_entries); local_store.set_all_recipes(recipe_entries).await;
state.recipe_categories = recipe_entries state.recipe_categories = recipe_entries
.iter() .iter()
.map(|entry| { .map(|entry| {
@ -255,11 +257,11 @@ impl StateMachine {
info!("Checking for user account data"); info!("Checking for user account data");
if let Some(user_data) = store.fetch_user_data().await { if let Some(user_data) = store.fetch_user_data().await {
debug!("Successfully got account data from server"); debug!("Successfully got account data from server");
local_store.set_user_data(Some(&user_data)); local_store.set_user_data(Some(&user_data)).await;
state.auth = Some(user_data); state.auth = Some(user_data);
} else { } else {
debug!("Using account data from local store"); debug!("Using account data from local store");
let user_data = local_store.get_user_data(); let user_data = local_store.get_user_data().await;
state.auth = user_data; state.auth = user_data;
} }
info!("Synchronizing categories"); info!("Synchronizing categories");
@ -293,7 +295,7 @@ impl StateMachine {
} }
} }
// Finally we store all of this app state back to our localstore // Finally we store all of this app state back to our localstore
local_store.store_app_state(&state); local_store.store_app_state(&state).await;
original.update(state); original.update(state);
Ok(()) Ok(())
} }
@ -349,8 +351,9 @@ impl MessageMapper<Message, AppState> for StateMachine {
.or_insert(cat); .or_insert(cat);
} }
let store = self.store.clone(); let store = self.store.clone();
self.local_store.set_recipe_entry(&entry); let local_store = self.local_store.clone();
spawn_local_scoped(cx, async move { spawn_local_scoped(cx, async move {
local_store.set_recipe_entry(&entry).await;
if let Err(e) = store.store_recipes(vec![entry]).await { if let Err(e) = store.store_recipes(vec![entry]).await {
// FIXME(jwall): We should have a global way to trigger error messages // FIXME(jwall): We should have a global way to trigger error messages
error!(err=?e, "Unable to save Recipe"); error!(err=?e, "Unable to save Recipe");
@ -363,9 +366,10 @@ impl MessageMapper<Message, AppState> for StateMachine {
Message::RemoveRecipe(recipe, callback) => { Message::RemoveRecipe(recipe, callback) => {
original_copy.recipe_counts.remove(&recipe); original_copy.recipe_counts.remove(&recipe);
original_copy.recipes.remove(&recipe); original_copy.recipes.remove(&recipe);
self.local_store.delete_recipe_entry(&recipe);
let store = self.store.clone(); let store = self.store.clone();
let local_store = self.local_store.clone();
spawn_local_scoped(cx, async move { spawn_local_scoped(cx, async move {
local_store.delete_recipe_entry(&recipe).await;
if let Err(err) = store.delete_recipe(&recipe).await { if let Err(err) = store.delete_recipe(&recipe).await {
error!(?err, "Failed to delete recipe"); error!(?err, "Failed to delete recipe");
} }
@ -396,8 +400,11 @@ impl MessageMapper<Message, AppState> for StateMachine {
original_copy.modified_amts.insert(key, amt); original_copy.modified_amts.insert(key, amt);
} }
Message::SetUserData(user_data) => { Message::SetUserData(user_data) => {
self.local_store.set_user_data(Some(&user_data)); let local_store = self.local_store.clone();
original_copy.auth = Some(user_data); original_copy.auth = Some(user_data.clone());
spawn_local_scoped(cx, async move {
local_store.set_user_data(Some(&user_data)).await;
});
} }
Message::SaveState(f) => { Message::SaveState(f) => {
let mut original_copy = original_copy.clone(); let mut original_copy = original_copy.clone();
@ -417,7 +424,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
if let Err(e) = store.store_app_state(&original_copy).await { if let Err(e) = store.store_app_state(&original_copy).await {
error!(err=?e, "Error saving app state"); error!(err=?e, "Error saving app state");
}; };
local_store.store_app_state(&original_copy); local_store.store_app_state(&original_copy).await;
original.set(original_copy); original.set(original_copy);
f.map(|f| f()); f.map(|f| f());
}); });
@ -478,7 +485,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
.store_plan_for_date(vec![], &date) .store_plan_for_date(vec![], &date)
.await .await
.expect("Failed to init meal plan for date"); .expect("Failed to init meal plan for date");
local_store.store_app_state(&original_copy); local_store.store_app_state(&original_copy).await;
original.set(original_copy); original.set(original_copy);
callback.map(|f| f()); callback.map(|f| f());
@ -501,7 +508,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
original_copy.filtered_ingredients = BTreeSet::new(); original_copy.filtered_ingredients = BTreeSet::new();
original_copy.modified_amts = BTreeMap::new(); original_copy.modified_amts = BTreeMap::new();
original_copy.extras = Vec::new(); original_copy.extras = Vec::new();
local_store.store_app_state(&original_copy); local_store.store_app_state(&original_copy).await;
original.set(original_copy); original.set(original_copy);
callback.map(|f| f()); callback.map(|f| f());
@ -513,9 +520,14 @@ impl MessageMapper<Message, AppState> for StateMachine {
return; return;
} }
} }
self.local_store.store_app_state(&original_copy); 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); original.set(original_copy);
} }
});
}
} }
pub type StateHandler<'ctx> = &'ctx Handler<'ctx, StateMachine, AppState, Message>; pub type StateHandler<'ctx> = &'ctx Handler<'ctx, StateMachine, AppState, Message>;

View File

@ -13,32 +13,68 @@
// limitations under the License. // limitations under the License.
use js_sys::Date; use js_sys::Date;
use tracing::error; use tracing::error;
use web_sys::{window, Storage, Window}; use web_sys::{window, Window};
use indexed_db::{self, Factory, Database}; use indexed_db::{self, Factory, Database, Transaction};
use anyhow::{Result, Context}; use anyhow::{Result, Context};
use std::future::Future;
pub async fn get_indexed_db(name: &str, version: Option<u32>) -> Result<Database<std::io::Error>> { pub fn get_storage() -> web_sys::Storage {
let version = version.unwrap_or(0); get_window()
.local_storage()
.expect("Failed to get storage")
.expect("No storage available")
}
pub const STORE_NAME: &'static str = "state-store";
#[derive(Clone, Debug)]
pub struct DBFactory<'name> {
name: &'name str,
version: Option<u32>,
}
impl<'name> DBFactory<'name> {
pub fn new(name: &'name str, version: Option<u32>) -> Self {
Self { name, version }
}
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(name, version, |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. // database with an older version than the one we requested to build.
let db = evt.database(); let db = evt.database();
// 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() == 0 { if db.version() == 0 {
// We use out of line keys for this object store // We use out of line keys for this object store
db.build_object_store("store").create()?; db.build_object_store(STORE_NAME).create()?;
} }
Ok(()) Ok(())
}).await.context(format!("Openong or creating the database {}", name))?; }).await.context(format!("Opening or creating the database {}", self.name))?;
Ok(db) Ok(db)
} }
pub fn get_storage() -> Storage { pub async fn rw_transaction<Fun, RetFut, Ret>(&self, transaction: Fun) -> indexed_db::Result<Ret, std::io::Error>
get_window() where
.local_storage() Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut,
.expect("Failed to get storage") RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>,
.expect("No storage available") Ret: 'static,
{
self.get_indexed_db().await.expect("Failed to open database")
.transaction(&[STORE_NAME]).rw()
.run(transaction).await
}
pub async fn ro_transaction<Fun, RetFut, Ret>(&self, 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(&[STORE_NAME])
.run(transaction).await
}
} }
pub fn get_ms_timestamp() -> u32 { pub fn get_ms_timestamp() -> u32 {

View File

@ -20,20 +20,20 @@ use crate::{api, routing::Handler as RouteHandler};
#[instrument] #[instrument]
#[component] #[component]
pub fn UI<G: Html>(cx: Scope) -> View<G> { pub fn UI<G: Html>(cx: Scope) -> View<G> {
let view = create_signal(cx, View::empty());
api::HttpStore::provide_context(cx, "/api".to_owned()); api::HttpStore::provide_context(cx, "/api".to_owned());
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");
spawn_local_scoped(cx, {
async move {
let local_store = api::LocalStore::new(); let local_store = api::LocalStore::new();
let app_state = if let Some(app_state) = local_store.fetch_app_state() { let app_state = if let Some(app_state) = local_store.fetch_app_state().await {
app_state app_state
} else { } else {
crate::app_state::AppState::new() crate::app_state::AppState::new()
}; };
debug!(?app_state, "Loaded app state from local storage"); debug!(?app_state, "Loaded app state from local storage");
let sh = 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());
spawn_local_scoped(cx, {
async move {
sh.dispatch(cx, Message::LoadState(None)); sh.dispatch(cx, Message::LoadState(None));
view.set(view! { cx, view.set(view! { cx,
RouteHandler(sh=sh) RouteHandler(sh=sh)