mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Give localstorage a full implemenation
This commit is contained in:
parent
fe3f2e896b
commit
c424432def
256
web/src/api.rs
256
web/src/api.rs
@ -21,7 +21,8 @@ use tracing::{debug, error, instrument, warn};
|
|||||||
|
|
||||||
use client_api::*;
|
use client_api::*;
|
||||||
use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
|
use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::{JsValue, UnwrapThrowExt};
|
||||||
|
use web_sys::Storage;
|
||||||
|
|
||||||
use crate::{app_state::AppState, js_lib};
|
use crate::{app_state::AppState, js_lib};
|
||||||
|
|
||||||
@ -107,14 +108,170 @@ fn token68(user: String, pass: String) -> String {
|
|||||||
base64::encode(format!("{}:{}", user, pass))
|
base64::encode(format!("{}:{}", user, pass))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LocalStore {
|
||||||
|
store: Storage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
store: js_lib::get_storage(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets user data from local storage.
|
||||||
|
pub fn get_user_data(&self) -> Option<UserData> {
|
||||||
|
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 fn set_user_data(&self, data: Option<&UserData>) {
|
||||||
|
if let Some(data) = data {
|
||||||
|
self.store
|
||||||
|
.set("user_data", &to_string(data).unwrap_throw())
|
||||||
|
.unwrap_throw();
|
||||||
|
} else {
|
||||||
|
self.store.delete("user_data").unwrap_throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets categories from local storage.
|
||||||
|
pub fn get_categories(&self) -> Option<String> {
|
||||||
|
self.store.get("categories").unwrap_throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the categories to the given string.
|
||||||
|
pub fn set_categories(&self, categories: Option<&String>) {
|
||||||
|
if let Some(c) = categories {
|
||||||
|
self.store.set("categories", c).unwrap_throw();
|
||||||
|
} else {
|
||||||
|
self.store.delete("categories").unwrap_throw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_storage_keys(&self) -> Vec<String> {
|
||||||
|
let mut keys = Vec::new();
|
||||||
|
for idx in 0..self.store.length().unwrap() {
|
||||||
|
keys.push(self.store.key(idx).unwrap_throw().unwrap_throw())
|
||||||
|
}
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
||||||
|
self.get_storage_keys()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|k| k.starts_with("recipe:"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all the recipes from local storage.
|
||||||
|
pub fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
|
||||||
|
let mut recipe_list = Vec::new();
|
||||||
|
for recipe_key in self.get_recipe_keys() {
|
||||||
|
if let Some(entry) = self.store.get(&recipe_key).unwrap_throw() {
|
||||||
|
match from_str(&entry) {
|
||||||
|
Ok(entry) => {
|
||||||
|
recipe_list.push(entry);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(recipe_key, err = ?e, "Failed to parse recipe entry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if recipe_list.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(recipe_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the set of recipes to the entries passed in. Deletes any recipes not
|
||||||
|
/// in the list.
|
||||||
|
pub fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
|
||||||
|
for recipe_key in self.get_recipe_keys() {
|
||||||
|
self.store.delete(&recipe_key).unwrap_throw();
|
||||||
|
}
|
||||||
|
for entry in entries {
|
||||||
|
self.set_recipe_entry(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set recipe entry in local storage.
|
||||||
|
pub fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
||||||
|
self.store
|
||||||
|
.set(
|
||||||
|
&recipe_key(entry.recipe_id()),
|
||||||
|
&to_string(&entry).unwrap_throw(),
|
||||||
|
)
|
||||||
|
.unwrap_throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete recipe entry from local storage.
|
||||||
|
pub fn delete_recipe_entry(&self, recipe_key: &str) {
|
||||||
|
self.store.delete(recipe_key).unwrap_throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save working plan to local storage.
|
||||||
|
pub fn save_plan(&self, plan: &Vec<(String, i32)>) {
|
||||||
|
self.store
|
||||||
|
.set("plan", &to_string(&plan).unwrap_throw())
|
||||||
|
.unwrap_throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_plan(&self) -> Option<Vec<(String, i32)>> {
|
||||||
|
if let Some(plan) = self.store.get("plan").unwrap_throw() {
|
||||||
|
Some(from_str(&plan).unwrap_throw())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_inventory_data(
|
||||||
|
&self,
|
||||||
|
) -> Option<(
|
||||||
|
BTreeSet<IngredientKey>,
|
||||||
|
BTreeMap<IngredientKey, String>,
|
||||||
|
Vec<(String, String)>,
|
||||||
|
)> {
|
||||||
|
if let Some(inventory) = self.store.get("inventory").unwrap_throw() {
|
||||||
|
return Some(from_str(&inventory).unwrap_throw());
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_inventory_data(&self) {
|
||||||
|
self.store.delete("inventory").unwrap_throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_inventory_data(
|
||||||
|
&self,
|
||||||
|
inventory: (
|
||||||
|
&BTreeSet<IngredientKey>,
|
||||||
|
&BTreeMap<IngredientKey, String>,
|
||||||
|
&Vec<(String, String)>,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
self.store
|
||||||
|
.set("inventory", &to_string(&inventory).unwrap_throw())
|
||||||
|
.unwrap_throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct HttpStore {
|
pub struct HttpStore {
|
||||||
root: String,
|
root: String,
|
||||||
|
local_store: LocalStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpStore {
|
impl HttpStore {
|
||||||
pub fn new(root: String) -> Self {
|
pub fn new(root: String) -> Self {
|
||||||
Self { root }
|
Self {
|
||||||
|
root,
|
||||||
|
local_store: LocalStore::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn v1_path(&self) -> String {
|
pub fn v1_path(&self) -> String {
|
||||||
@ -143,7 +300,6 @@ impl HttpStore {
|
|||||||
debug!("attempting login request against api.");
|
debug!("attempting login request against api.");
|
||||||
let mut path = self.v1_path();
|
let mut path = self.v1_path();
|
||||||
path.push_str("/auth");
|
path.push_str("/auth");
|
||||||
let storage = js_lib::get_storage();
|
|
||||||
let result = reqwasm::http::Request::get(&path)
|
let result = reqwasm::http::Request::get(&path)
|
||||||
.header(
|
.header(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
@ -158,12 +314,7 @@ impl HttpStore {
|
|||||||
.await
|
.await
|
||||||
.expect("Unparseable authentication response")
|
.expect("Unparseable authentication response")
|
||||||
.as_success();
|
.as_success();
|
||||||
storage
|
self.local_store.set_user_data(user_data.as_ref());
|
||||||
.set(
|
|
||||||
"user_data",
|
|
||||||
&to_string(&user_data).expect("Unable to serialize user_data"),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
return user_data;
|
return user_data;
|
||||||
}
|
}
|
||||||
error!(status = resp.status(), "Login was unsuccessful")
|
error!(status = resp.status(), "Login was unsuccessful")
|
||||||
@ -177,12 +328,11 @@ impl HttpStore {
|
|||||||
pub async fn get_categories(&self) -> Result<Option<String>, Error> {
|
pub async fn get_categories(&self) -> Result<Option<String>, Error> {
|
||||||
let mut path = self.v1_path();
|
let mut path = self.v1_path();
|
||||||
path.push_str("/categories");
|
path.push_str("/categories");
|
||||||
let storage = js_lib::get_storage();
|
|
||||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(reqwasm::Error::JsError(err)) => {
|
Err(reqwasm::Error::JsError(err)) => {
|
||||||
error!(path, ?err, "Error hitting api");
|
error!(path, ?err, "Error hitting api");
|
||||||
return Ok(storage.get("categories")?);
|
return Ok(self.local_store.get_categories());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(err)?;
|
return Err(err)?;
|
||||||
@ -190,14 +340,14 @@ impl HttpStore {
|
|||||||
};
|
};
|
||||||
if resp.status() == 404 {
|
if resp.status() == 404 {
|
||||||
debug!("Categories returned 404");
|
debug!("Categories returned 404");
|
||||||
storage.remove_item("categories")?;
|
self.local_store.set_categories(None);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else if resp.status() != 200 {
|
} else if resp.status() != 200 {
|
||||||
Err(format!("Status: {}", resp.status()).into())
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
} else {
|
} else {
|
||||||
debug!("We got a valid response back!");
|
debug!("We got a valid response back!");
|
||||||
let resp = resp.json::<CategoryResponse>().await?.as_success().unwrap();
|
let resp = resp.json::<CategoryResponse>().await?.as_success().unwrap();
|
||||||
storage.set("categories", &resp)?;
|
self.local_store.set_categories(Some(&resp));
|
||||||
Ok(Some(resp))
|
Ok(Some(resp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,26 +356,16 @@ impl HttpStore {
|
|||||||
pub async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> {
|
pub async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> {
|
||||||
let mut path = self.v1_path();
|
let mut path = self.v1_path();
|
||||||
path.push_str("/recipes");
|
path.push_str("/recipes");
|
||||||
let storage = js_lib::get_storage();
|
|
||||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(reqwasm::Error::JsError(err)) => {
|
Err(reqwasm::Error::JsError(err)) => {
|
||||||
error!(path, ?err, "Error hitting api");
|
error!(path, ?err, "Error hitting api");
|
||||||
let mut entries = Vec::new();
|
return Ok(self.local_store.get_recipes());
|
||||||
for key in js_lib::get_storage_keys() {
|
|
||||||
if key.starts_with("recipe:") {
|
|
||||||
let entry = from_str(&storage.get_item(&key)?.unwrap())
|
|
||||||
.map_err(|e| format!("{}", e))?;
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(Some(entries));
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(err)?;
|
return Err(err)?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let storage = js_lib::get_storage();
|
|
||||||
if resp.status() != 200 {
|
if resp.status() != 200 {
|
||||||
Err(format!("Status: {}", resp.status()).into())
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
} else {
|
} else {
|
||||||
@ -236,12 +376,7 @@ impl HttpStore {
|
|||||||
.map_err(|e| format!("{}", e))?
|
.map_err(|e| format!("{}", e))?
|
||||||
.as_success();
|
.as_success();
|
||||||
if let Some(ref entries) = entries {
|
if let Some(ref entries) = entries {
|
||||||
for r in entries.iter() {
|
self.local_store.set_all_recipes(&entries);
|
||||||
storage.set(
|
|
||||||
&recipe_key(r.recipe_id()),
|
|
||||||
&to_string(&r).expect("Unable to serialize recipe entries"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
@ -293,15 +428,11 @@ impl HttpStore {
|
|||||||
pub async fn save_recipes(&self, recipes: Vec<RecipeEntry>) -> Result<(), Error> {
|
pub async fn save_recipes(&self, recipes: Vec<RecipeEntry>) -> Result<(), Error> {
|
||||||
let mut path = self.v1_path();
|
let mut path = self.v1_path();
|
||||||
path.push_str("/recipes");
|
path.push_str("/recipes");
|
||||||
let storage = js_lib::get_storage();
|
|
||||||
for r in recipes.iter() {
|
for r in recipes.iter() {
|
||||||
if r.recipe_id().is_empty() {
|
if r.recipe_id().is_empty() {
|
||||||
return Err("Recipe Ids can not be empty".into());
|
return Err("Recipe Ids can not be empty".into());
|
||||||
}
|
}
|
||||||
storage.set(
|
self.local_store.set_recipe_entry(&r);
|
||||||
&recipe_key(r.recipe_id()),
|
|
||||||
&to_string(&r).expect("Unable to serialize recipe entries"),
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
let serialized = to_string(&recipes).expect("Unable to serialize recipe entries");
|
let serialized = to_string(&recipes).expect("Unable to serialize recipe entries");
|
||||||
let resp = reqwasm::http::Request::post(&path)
|
let resp = reqwasm::http::Request::post(&path)
|
||||||
@ -321,8 +452,7 @@ impl HttpStore {
|
|||||||
pub async fn save_categories(&self, categories: String) -> Result<(), Error> {
|
pub async fn save_categories(&self, categories: String) -> Result<(), Error> {
|
||||||
let mut path = self.v1_path();
|
let mut path = self.v1_path();
|
||||||
path.push_str("/categories");
|
path.push_str("/categories");
|
||||||
let storage = js_lib::get_storage();
|
self.local_store.set_categories(Some(&categories));
|
||||||
storage.set("categories", &categories)?;
|
|
||||||
let resp = reqwasm::http::Request::post(&path)
|
let resp = reqwasm::http::Request::post(&path)
|
||||||
.body(to_string(&categories).expect("Unable to encode categories as json"))
|
.body(to_string(&categories).expect("Unable to encode categories as json"))
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
@ -360,9 +490,7 @@ impl HttpStore {
|
|||||||
pub async fn save_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> {
|
pub async fn save_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> {
|
||||||
let mut path = self.v1_path();
|
let mut path = self.v1_path();
|
||||||
path.push_str("/plan");
|
path.push_str("/plan");
|
||||||
let storage = js_lib::get_storage();
|
self.local_store.save_plan(&plan);
|
||||||
let serialized_plan = to_string(&plan).expect("Unable to encode plan as json");
|
|
||||||
storage.set("plan", &serialized_plan)?;
|
|
||||||
let resp = reqwasm::http::Request::post(&path)
|
let resp = reqwasm::http::Request::post(&path)
|
||||||
.body(to_string(&plan).expect("Unable to encode plan as json"))
|
.body(to_string(&plan).expect("Unable to encode plan as json"))
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
@ -380,7 +508,6 @@ impl HttpStore {
|
|||||||
let mut path = self.v1_path();
|
let mut path = self.v1_path();
|
||||||
path.push_str("/plan");
|
path.push_str("/plan");
|
||||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||||
let storage = js_lib::get_storage();
|
|
||||||
if resp.status() != 200 {
|
if resp.status() != 200 {
|
||||||
Err(format!("Status: {}", resp.status()).into())
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
} else {
|
} else {
|
||||||
@ -391,8 +518,7 @@ impl HttpStore {
|
|||||||
.map_err(|e| format!("{}", e))?
|
.map_err(|e| format!("{}", e))?
|
||||||
.as_success();
|
.as_success();
|
||||||
if let Some(ref entry) = plan {
|
if let Some(ref entry) = plan {
|
||||||
let serialized: String = to_string(entry).map_err(|e| format!("{}", e))?;
|
self.local_store.save_plan(&entry);
|
||||||
storage.set("plan", &serialized)?
|
|
||||||
}
|
}
|
||||||
Ok(plan)
|
Ok(plan)
|
||||||
}
|
}
|
||||||
@ -414,26 +540,9 @@ impl HttpStore {
|
|||||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||||
if resp.status() != 200 {
|
if resp.status() != 200 {
|
||||||
let err = Err(format!("Status: {}", resp.status()).into());
|
let err = Err(format!("Status: {}", resp.status()).into());
|
||||||
Ok(match storage.get("inventory") {
|
Ok(match self.local_store.get_inventory_data() {
|
||||||
Ok(Some(val)) => match from_str(&val) {
|
Some(val) => val,
|
||||||
// TODO(jwall): Once we remove the v1 endpoint this is no longer needed.
|
None => return err,
|
||||||
Ok((filtered_ingredients, modified_amts)) => {
|
|
||||||
(filtered_ingredients, modified_amts, Vec::new())
|
|
||||||
}
|
|
||||||
Err(_) => match from_str(&val) {
|
|
||||||
Ok((filtered_ingredients, modified_amts, extra_items)) => {
|
|
||||||
(filtered_ingredients, modified_amts, extra_items)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Whatever is in storage is corrupted or invalid so we should delete it.
|
|
||||||
storage
|
|
||||||
.delete("inventory")
|
|
||||||
.expect("Unable to delete corrupt data in inventory cache");
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Ok(None) | Err(_) => return err,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
debug!("We got a valid response back");
|
debug!("We got a valid response back");
|
||||||
@ -447,11 +556,11 @@ impl HttpStore {
|
|||||||
.map_err(|e| format!("{}", e))?
|
.map_err(|e| format!("{}", e))?
|
||||||
.as_success()
|
.as_success()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = storage.set(
|
self.local_store.set_inventory_data((
|
||||||
"inventory",
|
&(filtered_ingredients.iter().cloned().collect()),
|
||||||
&to_string(&(&filtered_ingredients, &modified_amts))
|
&(modified_amts.iter().cloned().collect()),
|
||||||
.expect("Failed to serialize inventory data"),
|
&extra_items,
|
||||||
);
|
));
|
||||||
Ok((
|
Ok((
|
||||||
filtered_ingredients.into_iter().collect(),
|
filtered_ingredients.into_iter().collect(),
|
||||||
modified_amts.into_iter().collect(),
|
modified_amts.into_iter().collect(),
|
||||||
@ -471,13 +580,14 @@ impl HttpStore {
|
|||||||
path.push_str("/inventory");
|
path.push_str("/inventory");
|
||||||
let filtered_ingredients: Vec<IngredientKey> = filtered_ingredients.into_iter().collect();
|
let filtered_ingredients: Vec<IngredientKey> = filtered_ingredients.into_iter().collect();
|
||||||
let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect();
|
let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect();
|
||||||
|
debug!("Storing inventory data in cache");
|
||||||
|
self.local_store.set_inventory_data((
|
||||||
|
&(filtered_ingredients.iter().cloned().collect()),
|
||||||
|
&(modified_amts.iter().cloned().collect()),
|
||||||
|
&extra_items,
|
||||||
|
));
|
||||||
let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items))
|
let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items))
|
||||||
.expect("Unable to encode plan as json");
|
.expect("Unable to encode plan as json");
|
||||||
let storage = js_lib::get_storage();
|
|
||||||
debug!("Storing inventory data in cache");
|
|
||||||
storage
|
|
||||||
.set("inventory", &serialized_inventory)
|
|
||||||
.expect("Failed to cache inventory data");
|
|
||||||
debug!("Storing inventory data via API");
|
debug!("Storing inventory data via API");
|
||||||
let resp = reqwasm::http::Request::post(&path)
|
let resp = reqwasm::http::Request::post(&path)
|
||||||
.body(&serialized_inventory)
|
.body(&serialized_inventory)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user