feat: migrate user_data and app_state

from localstorage to indexeddb
This commit is contained in:
Jeremy Wall 2024-07-12 18:14:28 -04:00
parent ed44e929f4
commit 1f90cc2ef6
3 changed files with 103 additions and 91 deletions

View File

@ -17,7 +17,7 @@ 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, to_string};
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, error, instrument}; use tracing::{debug, error, instrument};
@ -26,6 +26,7 @@ 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, 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. // TODO(jwall): Remove this when we have gone a few migrations past.
//use web_sys::Storage; //use web_sys::Storage;
@ -90,21 +91,38 @@ fn token68(user: String, pass: String) -> String {
#[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
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;
}
}
// 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;
}
}
// 3. Recipes?
}
pub async fn store_app_state(&self, state: &AppState) { pub async fn store_app_state(&self, state: &AppState) {
//self.migrate_local_store().await; //self.migrate_local_store().await;
let state = match to_value(state) { let state = match to_value(state) {
@ -131,10 +149,6 @@ impl LocalStore {
}) })
.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");
} }
pub async fn fetch_app_state(&self) -> Option<AppState> { pub async fn fetch_app_state(&self) -> Option<AppState> {
@ -163,30 +177,13 @@ 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
// })
//})
} }
/// 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_value(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)
.expect("Failed to get object store"); .expect("Failed to get object store");
@ -202,16 +199,11 @@ impl LocalStore {
}) })
.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()
} }
// 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_value(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
@ -230,13 +222,6 @@ impl LocalStore {
}) })
.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 {
@ -251,10 +236,6 @@ impl LocalStore {
}) })
.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");
} }
} }
@ -342,7 +323,6 @@ impl LocalStore {
/// 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_value(&recipe_key).expect("Failed to serialize key");
self.store self.store
@ -358,6 +338,7 @@ impl LocalStore {
}) })
.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 //self.store
// .delete(&recipe_key) // .delete(&recipe_key)
// .expect(&format!("Failed to get recipe {}", recipe_key)); // .expect(&format!("Failed to get recipe {}", recipe_key));

View File

@ -11,13 +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;
use std::collections::HashSet;
pub fn get_storage() -> web_sys::Storage { pub fn get_storage() -> web_sys::Storage {
get_window() get_window()
@ -40,62 +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),
}
} }
} }
impl<'name> DBFactory<'name> { async fn version1_setup<'db>(
pub async fn get_indexed_db(&self) -> Result<Database<std::io::Error>> { stores: &HashSet<String>,
let factory = Factory::<std::io::Error>::get().context("opening IndexedDB")?; db: &'db Database<std::io::Error>,
let db = factory.open(self.name, self.version.unwrap_or(0), |evt| async move { ) -> Result<(), indexed_db::Error<std::io::Error>> {
// 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.
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 {
self.version1_setup(&stores, db).await?;
}
Ok(())
}).await.context(format!("Opening or creating the database {}", self.name))?;
Ok(db)
}
async fn version1_setup<'db>(&self, stores: &HashSet<String>, db: &'db Databse<std::io::Error>) -> std::result::Result<(), std::io::Error> {
// We use out of line keys for this object store // We use out of line keys for this object store
if !stores.contains(STATE_STORE_NAME) { if !stores.contains(STATE_STORE_NAME) {
db.build_object_store(STATE_STORE_NAME).create()?; db.build_object_store(STATE_STORE_NAME).create()?;
} }
if !stores.contains(RECIPE_STORE_NAME) { if !stores.contains(RECIPE_STORE_NAME) {
let recipe_store = db.build_object_store(RECIPE_STORE_NAME).create()?; let recipe_store = db.build_object_store(RECIPE_STORE_NAME).create()?;
recipe_store.build_index(CATEGORY_IDX, "category") recipe_store
.build_index(CATEGORY_IDX, "category")
.create()?; .create()?;
recipe_store.build_index(SERVING_COUNT_IDX, "serving_count") recipe_store
.build_index(SERVING_COUNT_IDX, "serving_count")
.create()?; .create()?;
} }
Ok(()) Ok(())
} }
pub async fn rw_transaction<Fun, RetFut, Ret>(&self, stores: &[&str], transaction: Fun) -> indexed_db::Result<Ret, std::io::Error> impl<'name> DBFactory<'name> {
where pub async fn get_indexed_db(&self) -> Result<Database<std::io::Error>> {
Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut, let factory = Factory::<std::io::Error>::get().context("opening IndexedDB")?;
RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>, let db = factory
Ret: 'static, .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
self.get_indexed_db().await.expect("Failed to open database") // databases with an older version than the one we requested to build.
.transaction(stores).rw() let db = evt.database();
.run(transaction).await 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 ro_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()
.await
.expect("Failed to open database")
.transaction(stores) .transaction(stores)
.run(transaction).await .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
} }
} }

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 {