From b543f06f2c1d60137e33032f21a5a09f47d9ce6d Mon Sep 17 00:00:00 2001
From: wiseaidev <oss@wiseai.dev>
Date: Thu, 10 Apr 2025 08:37:01 +0300
Subject: [PATCH] feat: impl cache storage

---
 crates/storage/Cargo.toml             |   4 +-
 crates/storage/src/cache_storage.rs   | 145 ++++++++++++++++++++++++++
 crates/storage/src/lib.rs             |  30 ++++++
 crates/storage/tests/cache_storage.rs | 103 ++++++++++++++++++
 4 files changed, 281 insertions(+), 1 deletion(-)
 create mode 100644 crates/storage/src/cache_storage.rs
 create mode 100644 crates/storage/tests/cache_storage.rs

diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml
index 56172162..d63594e2 100644
--- a/crates/storage/Cargo.toml
+++ b/crates/storage/Cargo.toml
@@ -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"
diff --git a/crates/storage/src/cache_storage.rs b/crates/storage/src/cache_storage.rs
new file mode 100644
index 00000000..bffd20e0
--- /dev/null
+++ b/crates/storage/src/cache_storage.rs
@@ -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) }
+    }
+}
diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs
index f5713f67..e2cfac3e 100644
--- a/crates/storage/src/lib.rs
+++ b/crates/storage/src/lib.rs
@@ -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;
 
@@ -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>>;
+}
diff --git a/crates/storage/tests/cache_storage.rs b/crates/storage/tests/cache_storage.rs
new file mode 100644
index 00000000..fa865161
--- /dev/null
+++ b/crates/storage/tests/cache_storage.rs
@@ -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);
+}