use js_sys::Object;
use luct_core::store::{OrderedStoreRead, SearchableStoreRead, StoreBase, StoreRead, StoreWrite};
use luct_store::{StringStoreKey, StringStoreValue};
use std::marker::PhantomData;
use web_sys::{Storage, window};
#[derive(Debug)]
pub struct BrowserStore<K, V> {
_kv: PhantomData<(K, V)>,
prefix: String,
storage: Storage,
}
impl<K, V> BrowserStore<K, V> {
pub fn new_local_store(prefix: String) -> Result<Self, String> {
let storage = browser_local_store()?;
Ok(Self {
_kv: PhantomData,
prefix,
storage,
})
}
}
impl<K: StringStoreKey, V> BrowserStore<K, V> {
fn get_key_string(&self, key: &K) -> String {
format!("{}/{}", self.prefix, key.serialize_key())
}
fn key_from_str(&self, key: &str) -> Option<K> {
if !key.starts_with(&self.prefix) || key.chars().nth(self.prefix.len()) != Some('/') {
return None;
}
K::deserialize_key(&key[self.prefix.len() + 1..])
}
fn count_key(&self) -> String {
format!("{}#count", self.prefix)
}
fn get_count(&self) -> usize {
self.storage
.get_item(&self.count_key())
.expect("Failed to retrieve count")
.unwrap_or("0".to_string())
.parse()
.expect("Count contains non integer value")
}
fn inc_count(&self) {
let count: usize = self.get_count();
self.storage
.set_item(&self.count_key(), &(count + 1).to_string())
.expect("Failed to set count");
}
fn dec_count(&self) {
let count: usize = self.get_count();
self.storage
.set_item(&self.count_key(), &(count - 1).to_string())
.expect("Failed to set count");
}
}
impl<K, V> StoreBase for BrowserStore<K, V> {
type Key = K;
type Value = V;
}
impl<K, V> StoreRead for BrowserStore<K, V>
where
K: StringStoreKey,
V: StringStoreValue,
{
fn get(&self, key: &K) -> Option<V> {
let key = self.get_key_string(key);
self.storage
.get_item(&key)
.expect("Failed to retreive value from local store")
.and_then(|val| V::deserialize_value(&val))
}
fn len(&self) -> usize {
self.get_count()
}
}
impl<K, V> StoreWrite for BrowserStore<K, V>
where
K: StringStoreKey,
V: StringStoreValue,
{
fn insert(&self, key: K, value: V) {
let key = self.get_key_string(&key);
let val = value.serialize_value();
if self
.storage
.get_item(&key)
.expect("Failed to retreive value into local store")
.is_none()
{
self.inc_count();
}
self.storage
.set_item(&key, &val)
.expect("Failed to insert value into local store");
}
fn delete(&self, key: &K) -> bool {
let key = self.get_key_string(key);
let had_item = self
.storage
.get_item(&key)
.expect("Failed to retreive value from local store")
.is_some();
if had_item {
self.dec_count();
}
self.storage
.remove_item(&key)
.expect("Failed to remove value from local store");
had_item
}
}
impl<K, V> OrderedStoreRead for BrowserStore<K, V>
where
K: StringStoreKey + Ord,
V: StringStoreValue,
{
fn last(&self) -> Option<(K, V)> {
let key = Object::keys(&self.storage)
.iter()
.filter_map(|key| key.as_string())
.filter_map(|key| self.key_from_str(&key))
.max()?;
let key_str = self.get_key_string(&key);
let val = self
.storage
.get_item(&key_str)
.expect("Failed to retreive last element of store")?;
let val = V::deserialize_value(&val)?;
Some((key, val))
}
}
impl<K, V> SearchableStoreRead for BrowserStore<K, V>
where
K: StringStoreKey + Ord,
V: StringStoreValue,
{
fn filter(&self, mut pred: impl FnMut(&K, &V) -> bool) -> Vec<(K, V)> {
Object::keys(&self.storage)
.iter()
.filter_map(|key| key.as_string())
.filter_map(|key| self.key_from_str(&key))
.filter_map(|key| {
self.storage
.get_item(&self.get_key_string(&key))
.expect("Failed to reteive element from store")
.map(|data| (key, data))
})
.filter_map(|(key, data)| V::deserialize_value(&data).map(|val| (key, val)))
.filter(|(key, val)| pred(key, val))
.collect()
}
}
pub fn browser_local_store() -> Result<Storage, String> {
let store = window()
.map(|window| window.local_storage())
.transpose()
.map_err(|err| err.as_string().unwrap())?
.flatten()
.ok_or("Failed to retreive local store")?;
Ok(store)
}
#[cfg(test)]
mod test {
use super::*;
use luct_test::{
store::{ordered_store_test, searchable_store_test, store_test},
utils::test_tracing,
};
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn browser_store() {
clear_storage();
test_tracing();
let store = BrowserStore::new_local_store("test".to_string()).unwrap();
store_test(store);
}
#[wasm_bindgen_test]
fn browser_ordered_store() {
clear_storage();
test_tracing();
let store = BrowserStore::new_local_store("test".to_string()).unwrap();
ordered_store_test(store);
}
#[wasm_bindgen_test]
fn browser_searchable_store() {
clear_storage();
test_tracing();
let store = BrowserStore::new_local_store("test".to_string()).unwrap();
searchable_store_test(store);
}
fn clear_storage() {
window()
.unwrap()
.local_storage()
.unwrap()
.unwrap()
.clear()
.unwrap();
}
}