diff --git a/web/src/api.rs b/web/src/api.rs index 9a01279..5422125 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -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:: >(&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:: >(&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 { @@ -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 { 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) { - // 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)); diff --git a/web/src/js_lib.rs b/web/src/js_lib.rs index a212c31..ab84e8d 100644 --- a/web/src/js_lib.rs +++ b/web/src/js_lib.rs @@ -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, + db: &'db Database, +) -> Result<(), indexed_db::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> { let factory = Factory::::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::>(); - // 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::>(); + // 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, db: &'db Databse) -> 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( + &self, + stores: &[&str], + transaction: Fun, + ) -> indexed_db::Result + where + Fun: 'static + FnOnce(Transaction) -> RetFut, + RetFut: 'static + Future>, + Ret: 'static, + { + self.get_indexed_db() + .await + .expect("Failed to open database") + .transaction(stores) + .rw() + .run(transaction) + .await } - pub async fn rw_transaction(&self, stores: &[&str], transaction: Fun) -> indexed_db::Result -where - Fun: 'static + FnOnce(Transaction) -> RetFut, - RetFut: 'static + Future>, - Ret: 'static, + pub async fn ro_transaction( + &self, + stores: &[&str], + transaction: Fun, + ) -> indexed_db::Result + where + Fun: 'static + FnOnce(Transaction) -> RetFut, + RetFut: 'static + Future>, + Ret: 'static, { - self.get_indexed_db().await.expect("Failed to open database") - .transaction(stores).rw() - .run(transaction).await - } - - pub async fn ro_transaction(&self, stores: &[&str], transaction: Fun) -> indexed_db::Result -where - Fun: 'static + FnOnce(Transaction) -> RetFut, - RetFut: 'static + Future>, - 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 } } diff --git a/web/src/web.rs b/web/src/web.rs index 507eb97..b4abe08 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -27,6 +27,8 @@ pub fn UI(cx: Scope) -> View { 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 {