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 gloo_net;
// 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 tracing::{debug, error, instrument};
@ -26,6 +26,7 @@ use client_api::*;
use recipes::{IngredientKey, RecipeEntry};
use serde_wasm_bindgen::{from_value, to_value};
use wasm_bindgen::JsValue;
use web_sys::Storage;
// TODO(jwall): Remove this when we have gone a few migrations past.
//use web_sys::Storage;
@ -90,21 +91,38 @@ fn token68(user: String, pass: String) -> String {
#[derive(Clone, Debug)]
pub struct LocalStore {
// FIXME(zaphar): Migration from local storage to indexed db
//old_store: Storage,
// TODO(zaphar): Remove this when it's safe to delete the migration
old_store: Storage,
store: DBFactory<'static>,
}
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(),
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) {
//self.migrate_local_store().await;
let state = match to_value(state) {
@ -131,10 +149,6 @@ impl LocalStore {
})
.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 async fn fetch_app_state(&self) -> Option<AppState> {
@ -163,30 +177,13 @@ impl LocalStore {
})
.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.
pub async fn get_user_data(&self) -> Option<UserData> {
self.store
.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
.object_store(js_lib::STATE_STORE_NAME)
.expect("Failed to get object store");
@ -202,16 +199,11 @@ impl LocalStore {
})
.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.
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 {
let data = data.clone();
self.store
@ -230,13 +222,6 @@ impl LocalStore {
})
.await
.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 {
self.store
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
@ -251,10 +236,6 @@ impl LocalStore {
})
.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");
}
}
@ -342,7 +323,6 @@ impl LocalStore {
/// 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>) {
// 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
@ -358,6 +338,7 @@ impl LocalStore {
})
.await
.expect("Failed to delete user_data");
// FIXME(zaphar): Migration from local storage to indexed db
//self.store
// .delete(&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.
// 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 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 {
get_window()
@ -40,62 +40,91 @@ pub struct DBFactory<'name> {
impl Default for DBFactory<'static> {
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> {
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
// 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))?;
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)
}
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
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(())
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 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,
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).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")
self.get_indexed_db()
.await
.expect("Failed to open database")
.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, {
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 {