Skip to content

[RFC]: impl cache storage #501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ serde_json = "1.0"
thiserror = "1.0"
js-sys = "0.3"
gloo-utils = { version = "0.2", path = "../utils" }
wasm-bindgen-futures = "0.4.50"

[dependencies.web-sys]
version = "0.3"
features = ["Storage", "Window"]
features = ["Storage", "Window", "CacheStorage", "Cache", "Request", "Response"]

[dev-dependencies]
wasm-bindgen-test = "0.3"
Expand Down
145 changes: 145 additions & 0 deletions crates/storage/src/cache_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use crate::AsyncStorage;
use js_sys::Array;
use serde::de::Error;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::future::Future;
use wasm_bindgen::JsCast;
use wasm_bindgen::UnwrapThrowExt;
use wasm_bindgen_futures::JsFuture;
use web_sys::window;
use web_sys::{Cache, CacheStorage as WebCacheStorage, Request, Response};

use crate::errors::StorageError;
use crate::Result;

/// Provides API to deal with `CacheStorage`
#[derive(Debug)]
pub struct CacheStorage;

impl CacheStorage {
fn raw() -> WebCacheStorage {
window().expect_throw("no window").caches().unwrap_throw()
}

fn make_request(url: &str) -> Result<Request> {
Request::new_with_str(&url)
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))
}

async fn open_cache() -> Result<Cache> {
let promise = Self::raw().open("gloo-cache");
let cache = JsFuture::from(promise)
.await
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?;
Ok(Cache::from(cache))
}

async fn all_keys() -> Result<Vec<String>> {
let promise = Self::raw().keys();
let js_value = JsFuture::from(promise)
.await
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?;
let array: Array = js_value.dyn_into().unwrap_throw();
Ok(array.iter().filter_map(|v| v.as_string()).collect())
}
}

impl AsyncStorage for CacheStorage {
fn get<T>(key: &str) -> impl Future<Output = Result<T>>
where
T: for<'de> Deserialize<'de> + 'static,
{
let key = key.to_string();
async move {
let cache = Self::open_cache().await?;
let req = Self::make_request(&key)?;

let match_promise = cache.match_with_request(&req);
let res_val = JsFuture::from(match_promise)
.await
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?;

if res_val.is_undefined() {
return Err(StorageError::KeyNotFound(key));
}

let response: Response = res_val.dyn_into().unwrap_throw();
let text_promise = response
.text()
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?;
let text = JsFuture::from(text_promise)
.await
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?
.as_string()
.ok_or_else(|| {
StorageError::SerdeError(serde_json::Error::custom("Expected response text"))
})?;

Ok(serde_json::from_str(&text)?)
}
}

fn get_all<T>() -> impl Future<Output = Result<T>>
where
T: for<'de> Deserialize<'de> + 'static,
{
async move {
let keys = Self::all_keys().await?;
let mut map = Map::with_capacity(keys.len());
for key in keys {
let val: Value = Self::get(&key).await?;
map.insert(key, val);
}
Ok(serde_json::from_value(Value::Object(map))?)
}
}

fn set<T>(key: &str, value: T) -> impl Future<Output = Result<()>>
where
T: Serialize + 'static,
{
let key = key.to_string();
async move {
let cache = Self::open_cache().await?;
let req = Self::make_request(&key)?;
let json = serde_json::to_string(&value)?;
let res = Response::new_with_opt_str(Some(&json))
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?;

let put_promise = cache.put_with_request(&req, &res);
JsFuture::from(put_promise)
.await
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?;

Ok(())
}
}

fn delete(key: &str) -> impl Future<Output = Result<()>> {
let key = key.to_string();
async move {
let cache = Self::open_cache().await?;
let req = Self::make_request(&key)?;
let delete_promise = cache.delete_with_request(&req);
JsFuture::from(delete_promise)
.await
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?;
Ok(())
}
}

fn clear() -> impl Future<Output = Result<()>> {
async move {
let delete_promise = Self::raw().delete("gloo-cache");
JsFuture::from(delete_promise)
.await
.map_err(|e| StorageError::JsError(js_sys::Error::from(e).into()))?;
Ok(())
}
}

fn length() -> impl Future<Output = Result<u32>> {
async move { Ok(Self::all_keys().await?.len() as u32) }
}
}
30 changes: 30 additions & 0 deletions crates/storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
#![deny(missing_docs, missing_debug_implementations)]

use serde::{Deserialize, Serialize};
use std::future::Future;
use wasm_bindgen::prelude::*;

use crate::errors::js_to_error;
use errors::StorageError;
use serde_json::{Map, Value};

mod cache_storage;
pub mod errors;
mod local_storage;
mod session_storage;
pub use cache_storage::CacheStorage;
pub use local_storage::LocalStorage;
pub use session_storage::SessionStorage;

Expand Down Expand Up @@ -95,3 +98,30 @@ pub trait Storage {
.expect_throw("unreachable: length does not throw an exception")
}
}

/// Trait for async cache-like storage
pub trait AsyncStorage {
/// Get a value by key
fn get<T>(key: &str) -> impl Future<Output = Result<T>>
where
T: for<'de> Deserialize<'de> + 'static;

/// Get all keys/values as a deserialized map or struct
fn get_all<T>() -> impl Future<Output = Result<T>>
where
T: for<'de> Deserialize<'de> + 'static;

/// Set a value by key
fn set<T>(key: &str, value: T) -> impl Future<Output = Result<()>>
where
T: Serialize + 'static;

/// Delete a key
fn delete(key: &str) -> impl Future<Output = Result<()>>;

/// Clear all keys
fn clear() -> impl Future<Output = Result<()>>;

/// Get number of stored items
fn length() -> impl Future<Output = Result<u32>>;
}
103 changes: 103 additions & 0 deletions crates/storage/tests/cache_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use gloo_storage::AsyncStorage;
use gloo_storage::CacheStorage;
use serde::{Deserialize, Serialize};
use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
async fn get_and_set() {
let key = "https://rustacean.net/assets/cuddlyferris.png";
let value = "Ferris is cute 🦀";

CacheStorage::set(key, value).await.unwrap();
let obtained: String = CacheStorage::get(key).await.unwrap();

assert_eq!(obtained, value);
}

#[derive(Serialize, Deserialize)]
struct FerrisFacts {
cuteness: String,
power: String,
}

#[wasm_bindgen_test]
async fn get_all() {
CacheStorage::set(
"https://rustacean.net/assets/cuddlyferris.png",
"Ferris is cute 🦀",
)
.await
.unwrap();
CacheStorage::set("power", "Ferris is King 👑")
.await
.unwrap();

let facts: serde_json::Value = CacheStorage::get_all().await.unwrap();

assert_eq!(
facts["https://rustacean.net/assets/cuddlyferris.png"],
"Ferris is cute 🦀"
);
assert_eq!(facts["power"], "Ferris is King 👑");
}

#[wasm_bindgen_test]
async fn set_and_length() {
CacheStorage::clear().await.unwrap();

let len = CacheStorage::length().await.unwrap();
assert_eq!(len, 0);

CacheStorage::set(
"https://rustacean.net/assets/cuddlyferris.png",
"Trust the compiler, no cap 🧠",
)
.await
.unwrap();
let len = CacheStorage::length().await.unwrap();
assert_eq!(len, 1);

CacheStorage::clear().await.unwrap();
let len = CacheStorage::length().await.unwrap();
assert_eq!(len, 0);
}

#[wasm_bindgen_test]
async fn delete_key() {
let key = "https://rustacean.net/assets/cuddlyferris.png";
CacheStorage::set(key, "Goodbye, Ferris, see you tomorrow 😢")
.await
.unwrap();
assert!(CacheStorage::get::<String>(key).await.is_ok());

CacheStorage::delete(key).await.unwrap();
let result = CacheStorage::get::<String>(key).await;

assert!(result.is_err());
}

#[wasm_bindgen_test]
async fn clear_storage() {
CacheStorage::set(
"https://rustacean.net/assets/cuddlyferris.png",
"Ferris remembers everything 🧠",
)
.await
.unwrap();
CacheStorage::set(
"https://rustacean.net/assets/cuddlyferris.png",
"Except when cleared, no cap 😅",
)
.await
.unwrap();

let len_before = CacheStorage::length().await.unwrap();
assert_eq!(len_before, 2);

CacheStorage::clear().await.unwrap();

let len_after = CacheStorage::length().await.unwrap();
assert_eq!(len_after, 0);
}
Loading