mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-28 20:29:53 -04:00
Compare commits
5 Commits
eccbb7581b
...
84cc2a2713
Author | SHA1 | Date | |
---|---|---|---|
84cc2a2713 | |||
f75652befa | |||
b93edd2701 | |||
4767115da6 | |||
1c55a315b0 |
1674
Cargo.lock
generated
1674
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
1
Makefile
1
Makefile
@ -41,6 +41,7 @@ wasm: wasm-dist static-prep
|
||||
|
||||
wasm-dist: web/src/*.rs web/src/components/*.rs
|
||||
cd web; sh ../scripts/wasm-build.sh debug
|
||||
cd web; sh ../scripts/wasm-sourcemap.sh
|
||||
|
||||
clean:
|
||||
rm -rf web/dist/*
|
||||
|
@ -6,10 +6,12 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = "<=1.0.171"
|
||||
recipes = { path = "../recipes" }
|
||||
chrono = "0.4.22"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.axum]
|
||||
version = "0.5.16"
|
||||
|
50
flake.lock
generated
50
flake.lock
generated
@ -1,5 +1,21 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cargo-wasm2map-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1693927731,
|
||||
"narHash": "sha256-oqJ9ZZLvUK57A9Kf6L4pPrW6nHqb+18+JGKj9HfIaaM=",
|
||||
"owner": "mtolmacs",
|
||||
"repo": "wasm2map",
|
||||
"rev": "c7d80748b7f3af37df24770b9330b17aa9599e3e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mtolmacs",
|
||||
"repo": "wasm2map",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@ -31,24 +47,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@ -114,6 +112,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"cargo-wasm2map-src": "cargo-wasm2map-src",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"gitignore": "gitignore",
|
||||
@ -142,21 +141,6 @@
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
14
flake.nix
14
flake.nix
@ -11,8 +11,9 @@
|
||||
};
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
|
||||
cargo-wasm2map-src = { url = "github:mtolmacs/wasm2map"; flake = false; };
|
||||
};
|
||||
outputs = {nixpkgs, flake-utils, rust-overlay, naersk, ...}:
|
||||
outputs = {nixpkgs, flake-utils, rust-overlay, naersk, cargo-wasm2map-src, ...}:
|
||||
let
|
||||
kitchenGen = (import ./nix/kitchen/default.nix);
|
||||
kitchenWasmGen = (import ./nix/kitchenWasm/default.nix);
|
||||
@ -42,9 +43,16 @@
|
||||
wasm-pack = wasm-packGen {
|
||||
inherit rust-wasm naersk-lib pkgs;
|
||||
};
|
||||
cargo-wasm2map = naersk-lib.buildPackage {
|
||||
pname = "cargo-wasm2map";
|
||||
version = "v0.1.0";
|
||||
build-inputs = [ rust-wasm ];
|
||||
src = cargo-wasm2map-src;
|
||||
cargoBuildOptions = opts: opts ++ ["-p" "cargo-wasm2map" ];
|
||||
};
|
||||
wasm-bindgen = pkgs.callPackage wasm-bindgenGen { inherit pkgs; };
|
||||
kitchenWasm = kitchenWasmGen {
|
||||
inherit pkgs rust-wasm wasm-bindgen version;
|
||||
inherit pkgs rust-wasm wasm-bindgen version cargo-wasm2map;
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
# I'm maintaining some patches for these so the lockfile hashes are a little
|
||||
@ -89,7 +97,7 @@
|
||||
program = "${kitchen}/bin/kitchen";
|
||||
};
|
||||
devShell = pkgs.callPackage ./nix/devShell/default.nix {
|
||||
inherit rust-wasm wasm-bindgen;
|
||||
inherit rust-wasm wasm-bindgen cargo-wasm2map;
|
||||
wasm-pack-hermetic = wasm-pack;
|
||||
};
|
||||
}
|
||||
|
@ -18,14 +18,19 @@ async-trait = "0.1.57"
|
||||
async-session = "3.0.0"
|
||||
ciborium = "0.2.0"
|
||||
tower = "0.4.13"
|
||||
serde = "<=1.0.171"
|
||||
cookie = "0.17.0"
|
||||
chrono = "0.4.22"
|
||||
metrics = "0.20.1"
|
||||
metrics-exporter-prometheus = "0.11.0"
|
||||
futures = "0.3"
|
||||
metrics-process = "1.0.8"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.22"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
|
||||
[dependencies.argon2]
|
||||
version = "0.5.0"
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
let
|
||||
lib = import ../lib/lib.nix;
|
||||
in
|
||||
{ pkgs, rust-wasm, wasm-pack-hermetic, wasm-bindgen }:
|
||||
{ pkgs, rust-wasm, wasm-pack-hermetic, wasm-bindgen, cargo-wasm2map }:
|
||||
with pkgs;
|
||||
mkShell {
|
||||
buildInputs = (lib.darwin-sdk pkgs) ++ (with pkgs; [wasm-bindgen wasm-pack-hermetic llvm clang rust-wasm binaryen]);
|
||||
buildInputs = (lib.darwin-sdk pkgs) ++ (with pkgs; [wasm-bindgen wasm-pack-hermetic llvm clang rust-wasm binaryen cargo-wasm2map]);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
wasm-bindgen,
|
||||
lockFile,
|
||||
outputHashes,
|
||||
cargo-wasm2map,
|
||||
}:
|
||||
with pkgs;
|
||||
let
|
||||
@ -18,7 +19,7 @@ stdenv.mkDerivation {
|
||||
inherit src pname;
|
||||
version = version;
|
||||
# we need wasmb-bindgen v0.2.83 exactly
|
||||
buildInputs = [ rust-wasm wasm-bindgen wasm-pack binaryen];
|
||||
buildInputs = [ rust-wasm wasm-bindgen wasm-pack binaryen cargo-wasm2map];
|
||||
propagatedBuildInputs = [ rust-wasm wasm-bindgen wasm-pack binaryen];
|
||||
phases = [ "postUnpackPhase" "buildPhase"];
|
||||
postUnpackPhase = ''
|
||||
@ -34,6 +35,7 @@ stdenv.mkDerivation {
|
||||
export project=kitchen
|
||||
sh ../scripts/wasm-build.sh release
|
||||
sh ../scripts/wasm-opt.sh release
|
||||
sh ../scripts/wasm-sourcemap.sh
|
||||
rm -f $out/kitchen_wasm_bg.wasm
|
||||
cp -r index.html $out
|
||||
cp -r favicon.ico $out
|
||||
|
@ -8,8 +8,14 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
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]
|
||||
version = "~0.4.0"
|
||||
|
3
scripts/wasm-sourcemap.sh
Normal file
3
scripts/wasm-sourcemap.sh
Normal file
@ -0,0 +1,3 @@
|
||||
set -x
|
||||
|
||||
cargo-wasm2map wasm2map --patch $out/${project}_wasm_bg.wasm --base-url=http://localhost:3030
|
@ -27,9 +27,12 @@ sycamore-router = "0.8"
|
||||
js-sys = "0.3.60"
|
||||
wasm-web-component = { git = "https://github.com/zaphar/wasm-web-components.git", rev = "v0.3.0" }
|
||||
maud = "*"
|
||||
indexed-db = "0.4.1"
|
||||
anyhow = "1.0.86"
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "<=1.0.171"
|
||||
version = "1.0.204"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
|
366
web/src/api.rs
366
web/src/api.rs
@ -16,18 +16,22 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use base64::{self, Engine};
|
||||
use chrono::NaiveDate;
|
||||
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 tracing::{debug, error, instrument};
|
||||
|
||||
use anyhow::Result;
|
||||
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;
|
||||
|
||||
use crate::{
|
||||
app_state::{parse_recipes, AppState},
|
||||
js_lib,
|
||||
js_lib::{self, DBFactory},
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -86,38 +90,68 @@ fn token68(user: String, pass: String) -> String {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
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 {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
store: js_lib::get_storage(),
|
||||
store: DBFactory::default(),
|
||||
//old_store: js_lib::get_storage(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_app_state(&self, state: &AppState) {
|
||||
self.migrate_local_store();
|
||||
let state = match to_string(state) {
|
||||
pub async fn store_app_state(&self, state: &AppState) {
|
||||
//self.migrate_local_store().await;
|
||||
let state = match to_value(state) {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
error!(?err, ?state, "Error deserializing app_state");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let key = to_value(APP_STATE_KEY).expect("Failed to serialize key");
|
||||
self.store
|
||||
.set("app_state", &state)
|
||||
.expect("Failed to set our app state");
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::STATE_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");
|
||||
self.store.get("app_state").map_or(None, |val| {
|
||||
val.map(|s| {
|
||||
debug!("Found an app_state object");
|
||||
let recipes = parse_recipes(&self.get_recipes().await).expect("Failed to parse recipes");
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let key = to_value(APP_STATE_KEY).expect("Failed to serialize key");
|
||||
let object_store = trx
|
||||
.object_store(js_lib::STATE_STORE_NAME)
|
||||
.expect("Failed to get object store");
|
||||
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");
|
||||
match object_store.get(&key).await.expect("Failed to read from") {
|
||||
Some(s) => from_value(s).expect("Failed to deserialize app state"),
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
if let Some(recipes) = recipes {
|
||||
debug!("Populating recipes");
|
||||
for (id, recipe) in recipes {
|
||||
@ -125,78 +159,152 @@ impl LocalStore {
|
||||
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.
|
||||
pub fn get_user_data(&self) -> Option<UserData> {
|
||||
pub async 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()
|
||||
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let key = to_value("user_data").expect("Failed to serialize key");
|
||||
let object_store = trx
|
||||
.object_store(js_lib::STATE_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.
|
||||
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 {
|
||||
let data = data.clone();
|
||||
self.store
|
||||
.set(
|
||||
"user_data",
|
||||
&to_string(data).expect("Failed to desrialize user_data"),
|
||||
)
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::STATE_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");
|
||||
// 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
|
||||
.delete("user_data")
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::STATE_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("user_data")
|
||||
// .expect("Failed to delete user_data");
|
||||
}
|
||||
}
|
||||
|
||||
fn get_storage_keys(&self) -> Vec<String> {
|
||||
let mut keys = Vec::new();
|
||||
for idx in 0..self.store.length().unwrap() {
|
||||
if let Some(k) = self.store.key(idx).expect("Failed to get storage key") {
|
||||
keys.push(k)
|
||||
}
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn migrate_local_store(&self) {
|
||||
for k in self.get_storage_keys().into_iter().filter(|k| {
|
||||
k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples"
|
||||
}) {
|
||||
// Deleting old local store key
|
||||
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> {
|
||||
self.get_storage_keys()
|
||||
.into_iter()
|
||||
.filter(|k| k.starts_with("recipe:"))
|
||||
async fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let mut keys = Vec::new();
|
||||
let object_store = trx
|
||||
.object_store(js_lib::RECIPE_STORE_NAME)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to get storage keys").into_iter()
|
||||
}
|
||||
|
||||
/// 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();
|
||||
for recipe_key in self.get_recipe_keys() {
|
||||
if let Some(entry) = self
|
||||
for recipe_key in self.get_recipe_keys().await {
|
||||
let key = to_value(&recipe_key).expect("Failed to serialize key");
|
||||
let entry = self
|
||||
.store
|
||||
.get(&recipe_key)
|
||||
.expect(&format!("Failed to get recipe: {}", recipe_key))
|
||||
{
|
||||
match from_str(&entry) {
|
||||
Ok(entry) => {
|
||||
recipe_list.push(entry);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(recipe_key, err = ?e, "Failed to parse recipe entry");
|
||||
}
|
||||
}
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::RECIPE_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")
|
||||
{
|
||||
Some(v) => from_value(v).expect("Failed to deserialize entry"),
|
||||
None => None,
|
||||
};
|
||||
Ok(entry)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to get recipes");
|
||||
if let Some(entry) = entry {
|
||||
recipe_list.push(entry);
|
||||
}
|
||||
}
|
||||
if recipe_list.is_empty() {
|
||||
@ -205,42 +313,122 @@ impl LocalStore {
|
||||
Some(recipe_list)
|
||||
}
|
||||
|
||||
pub fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
|
||||
let key = recipe_key(id);
|
||||
pub async fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
|
||||
let key = to_value(&recipe_key(id)).expect("Failed to serialize key");
|
||||
self.store
|
||||
.get(&key)
|
||||
.expect(&format!("Failed to get recipe {}", key))
|
||||
.map(|entry| from_str(&entry).expect(&format!("Failed to get recipe {}", key)))
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::RECIPE_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")
|
||||
{
|
||||
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
|
||||
/// in the list.
|
||||
pub fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
|
||||
for recipe_key in self.get_recipe_keys() {
|
||||
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
|
||||
.delete(&recipe_key)
|
||||
.expect(&format!("Failed to get recipe {}", recipe_key));
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::STATE_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 {
|
||||
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(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::RECIPE_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.
|
||||
pub fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
||||
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()))
|
||||
pub async fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
||||
let entry = entry.clone();
|
||||
let key = to_value(&recipe_key(entry.recipe_id())).expect("Failed to serialize recipe key");
|
||||
self.store.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::RECIPE_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");
|
||||
// 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.
|
||||
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
|
||||
.delete(&recipe_key(recipe_id))
|
||||
.expect(&format!("Failed to delete recipe {}", recipe_id))
|
||||
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx
|
||||
.object_store(js_lib::RECIPE_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 +553,7 @@ impl HttpStore {
|
||||
Ok(resp) => resp,
|
||||
Err(gloo_net::Error::JsError(err)) => {
|
||||
error!(path, ?err, "Error hitting api");
|
||||
return Ok(self.local_store.get_recipes());
|
||||
return Ok(self.local_store.get_recipes().await);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err)?;
|
||||
@ -395,7 +583,7 @@ impl HttpStore {
|
||||
Ok(resp) => resp,
|
||||
Err(gloo_net::Error::JsError(err)) => {
|
||||
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) => {
|
||||
return Err(err)?;
|
||||
@ -415,7 +603,7 @@ impl HttpStore {
|
||||
.as_success()
|
||||
.unwrap();
|
||||
if let Some(ref entry) = entry {
|
||||
self.local_store.set_recipe_entry(entry);
|
||||
self.local_store.set_recipe_entry(entry).await;
|
||||
}
|
||||
Ok(entry)
|
||||
}
|
||||
|
@ -40,9 +40,11 @@ pub struct AppState {
|
||||
pub recipe_counts: BTreeMap<String, usize>,
|
||||
pub recipe_categories: BTreeMap<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>>,
|
||||
#[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 category_map: BTreeMap<String, String>,
|
||||
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
@ -178,7 +180,7 @@ impl StateMachine {
|
||||
// call set on the signal once. When the LinearSignal get's dropped it
|
||||
// will call set on the contained Signal.
|
||||
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);
|
||||
}
|
||||
let mut state = original.get().as_ref().clone();
|
||||
@ -201,7 +203,7 @@ impl StateMachine {
|
||||
|
||||
info!("Synchronizing recipe");
|
||||
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
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
@ -255,11 +257,11 @@ impl StateMachine {
|
||||
info!("Checking for user account data");
|
||||
if let Some(user_data) = store.fetch_user_data().await {
|
||||
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);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
info!("Synchronizing categories");
|
||||
@ -293,7 +295,7 @@ impl StateMachine {
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
Ok(())
|
||||
}
|
||||
@ -349,8 +351,9 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
.or_insert(cat);
|
||||
}
|
||||
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 {
|
||||
local_store.set_recipe_entry(&entry).await;
|
||||
if let Err(e) = store.store_recipes(vec![entry]).await {
|
||||
// FIXME(jwall): We should have a global way to trigger error messages
|
||||
error!(err=?e, "Unable to save Recipe");
|
||||
@ -363,9 +366,10 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
Message::RemoveRecipe(recipe, callback) => {
|
||||
original_copy.recipe_counts.remove(&recipe);
|
||||
original_copy.recipes.remove(&recipe);
|
||||
self.local_store.delete_recipe_entry(&recipe);
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
local_store.delete_recipe_entry(&recipe).await;
|
||||
if let Err(err) = store.delete_recipe(&recipe).await {
|
||||
error!(?err, "Failed to delete recipe");
|
||||
}
|
||||
@ -396,8 +400,11 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
original_copy.modified_amts.insert(key, amt);
|
||||
}
|
||||
Message::SetUserData(user_data) => {
|
||||
self.local_store.set_user_data(Some(&user_data));
|
||||
original_copy.auth = Some(user_data);
|
||||
let local_store = self.local_store.clone();
|
||||
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) => {
|
||||
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 {
|
||||
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);
|
||||
f.map(|f| f());
|
||||
});
|
||||
@ -478,7 +485,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
.store_plan_for_date(vec![], &date)
|
||||
.await
|
||||
.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);
|
||||
|
||||
callback.map(|f| f());
|
||||
@ -501,7 +508,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
original_copy.filtered_ingredients = BTreeSet::new();
|
||||
original_copy.modified_amts = BTreeMap::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);
|
||||
|
||||
callback.map(|f| f());
|
||||
@ -513,8 +520,13 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.local_store.store_app_state(&original_copy);
|
||||
original.set(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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,15 +13,76 @@
|
||||
// limitations under the License.
|
||||
use js_sys::Date;
|
||||
use tracing::error;
|
||||
use web_sys::{window, Storage, Window};
|
||||
use web_sys::{window, Window};
|
||||
use indexed_db::{self, Factory, Database, Transaction};
|
||||
use anyhow::{Result, Context};
|
||||
use std::future::Future;
|
||||
|
||||
pub fn get_storage() -> Storage {
|
||||
pub fn get_storage() -> web_sys::Storage {
|
||||
get_window()
|
||||
.local_storage()
|
||||
.expect("Failed to get storage")
|
||||
.expect("No storage available")
|
||||
}
|
||||
|
||||
pub const STATE_STORE_NAME: &'static str = "state-store";
|
||||
pub const RECIPE_STORE_NAME: &'static str = "recipe-store";
|
||||
pub const DB_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DBFactory<'name> {
|
||||
name: &'name str,
|
||||
version: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for DBFactory<'static> {
|
||||
fn default() -> Self {
|
||||
DBFactory { name: STATE_STORE_NAME, version: Some(DB_VERSION) }
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// NOTE(jwall): This needs to be somewhat clever in handling version upgrades.
|
||||
if db.version() == 1 {
|
||||
// We use out of line keys for this object store
|
||||
db.build_object_store(STATE_STORE_NAME).create()?;
|
||||
db.build_object_store(RECIPE_STORE_NAME).create()?;
|
||||
// TODO(jwall): Do we need indexes?
|
||||
}
|
||||
Ok(())
|
||||
}).await.context(format!("Opening or creating the database {}", self.name))?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ms_timestamp() -> u32 {
|
||||
Date::new_0().get_milliseconds()
|
||||
}
|
||||
|
@ -20,20 +20,20 @@ use crate::{api, routing::Handler as RouteHandler};
|
||||
#[instrument]
|
||||
#[component]
|
||||
pub fn UI<G: Html>(cx: Scope) -> View<G> {
|
||||
let view = create_signal(cx, View::empty());
|
||||
api::HttpStore::provide_context(cx, "/api".to_owned());
|
||||
let store = api::HttpStore::get_from_context(cx).as_ref().clone();
|
||||
info!("Starting UI");
|
||||
let local_store = api::LocalStore::new();
|
||||
let app_state = if let Some(app_state) = local_store.fetch_app_state() {
|
||||
app_state
|
||||
} else {
|
||||
crate::app_state::AppState::new()
|
||||
};
|
||||
debug!(?app_state, "Loaded app state from local storage");
|
||||
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 {
|
||||
let local_store = api::LocalStore::new();
|
||||
let app_state = if let Some(app_state) = local_store.fetch_app_state().await {
|
||||
app_state
|
||||
} else {
|
||||
crate::app_state::AppState::new()
|
||||
};
|
||||
debug!(?app_state, "Loaded app state from local storage");
|
||||
let sh = crate::app_state::get_state_handler(cx, app_state, store);
|
||||
sh.dispatch(cx, Message::LoadState(None));
|
||||
view.set(view! { cx,
|
||||
RouteHandler(sh=sh)
|
||||
|
Loading…
x
Reference in New Issue
Block a user