use std::cell::{Cell, RefCell};
use std::fmt;
use std::sync::Arc;
use crate::store::NodeStore;
use crate::store::cache::{CacheError, LruCache};
use crate::tree::{Hash, Node};
const DEFAULT_CACHE_CAPACITY: usize = 1_024;
const DB_VERSION: u32 = 1;
const NODE_STORE: &str = "nodes";
const COMPRESSION_LEVEL: i32 = 1;
#[cfg(not(all(feature = "wasm", target_arch = "wasm32", target_os = "unknown")))]
compile_error!("store::indexeddb requires the wasm feature on wasm32-unknown-unknown");
mod browser {
use super::{DB_VERSION, Hash, IndexedDbStoreError, NODE_STORE, Node};
use indexed_db_futures::database::Database as IdbDatabase;
use indexed_db_futures::prelude::{Build, BuildPrimitive, QuerySource};
use indexed_db_futures::transaction::TransactionMode;
use indexed_db_futures::typed_array::{Uint8Array, Uint8ArraySlice};
pub(super) async fn open_database(name: String) -> Result<IdbDatabase, IndexedDbStoreError> {
IdbDatabase::open(name)
.with_version(DB_VERSION)
.with_on_upgrade_needed(|_event, db| {
let has_nodes = db.object_store_names().any(|store| store == NODE_STORE);
if !has_nodes {
db.create_object_store(NODE_STORE).build()?;
}
Ok(())
})
.await
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))
}
pub(super) async fn read_node(
db: &IdbDatabase,
hash: &Hash,
) -> Result<Option<Node>, IndexedDbStoreError> {
let compressed = fetch_compressed(db, hash).await?;
compressed
.map(|bytes| super::decode_compressed_node(bytes.as_ref()))
.transpose()
}
pub(super) async fn has_node(
db: &IdbDatabase,
hash: &Hash,
) -> Result<bool, IndexedDbStoreError> {
Ok(fetch_compressed(db, hash).await?.is_some())
}
async fn fetch_compressed(
db: &IdbDatabase,
hash: &Hash,
) -> Result<Option<Uint8Array>, IndexedDbStoreError> {
let tx = db
.transaction(NODE_STORE)
.with_mode(TransactionMode::Readonly)
.build()
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?;
let store = tx
.object_store(NODE_STORE)
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?;
let compressed: Option<Uint8Array> = store
.get(hash_key(hash))
.primitive()
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?
.await
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?;
drop(store);
tx.commit()
.await
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?;
Ok(compressed)
}
pub(super) async fn write_node(
db: &IdbDatabase,
hash: &Hash,
compressed: &[u8],
) -> Result<(), IndexedDbStoreError> {
let tx = db
.transaction(NODE_STORE)
.with_mode(TransactionMode::Readwrite)
.build()
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?;
let store = tx
.object_store(NODE_STORE)
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?;
store
.put(Uint8ArraySlice::new(compressed))
.with_key(hash_key(hash))
.without_key_type()
.primitive()
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?
.await
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))?;
drop(store);
tx.commit()
.await
.map_err(|error| IndexedDbStoreError::IndexedDb(error.to_string()))
}
fn hash_key(hash: &Hash) -> String {
hash.to_string()
}
}
type BrowserDatabase = indexed_db_futures::database::Database;
#[derive(Debug)]
pub struct IndexedDbStore {
db: BrowserDatabase,
cache: RefCell<LruCache>,
transaction_count: Cell<usize>,
}
impl IndexedDbStore {
pub async fn open(name: impl Into<String>) -> Result<Self, IndexedDbStoreError> {
Self::open_with_cache_capacity(name, DEFAULT_CACHE_CAPACITY).await
}
pub async fn open_with_cache_capacity(
name: impl Into<String>,
cache_capacity: usize,
) -> Result<Self, IndexedDbStoreError> {
let cache = LruCache::new(cache_capacity).map_err(IndexedDbStoreError::from)?;
let db = browser::open_database(name.into()).await?;
Ok(Self {
db,
cache: RefCell::new(cache),
transaction_count: Cell::new(0),
})
}
pub fn cache_capacity(&self) -> usize {
self.cache.borrow().capacity()
}
pub const fn indexeddb_transaction_count(&self) -> usize {
self.transaction_count.get()
}
pub async fn get_async(&self, hash: &Hash) -> Result<Option<Arc<Node>>, IndexedDbStoreError> {
if let Some(node) = self.cache_get(hash) {
return Ok(Some(node));
}
self.record_transaction();
let node = browser::read_node(&self.db, hash).await?.map(Arc::new);
if let Some(node) = &node {
self.cache_put(*hash, Arc::clone(node));
}
Ok(node)
}
pub async fn put_async(&self, node: &Node) -> Result<Hash, IndexedDbStoreError> {
let hash = node.hash();
self.record_transaction();
if browser::has_node(&self.db, &hash).await? {
self.cache_put(hash, Arc::new(node.clone()));
return Ok(hash);
}
let compressed = encode_node(node)?;
self.record_transaction();
browser::write_node(&self.db, &hash, &compressed).await?;
self.cache_put(hash, Arc::new(node.clone()));
Ok(hash)
}
fn cache_get(&self, hash: &Hash) -> Option<Arc<Node>> {
self.cache.borrow_mut().get(hash)
}
fn cache_put(&self, hash: Hash, node: Arc<Node>) {
self.cache.borrow_mut().put(hash, node);
}
fn record_transaction(&self) {
self.transaction_count
.set(self.transaction_count.get().saturating_add(1));
}
}
impl NodeStore for IndexedDbStore {
type Error = IndexedDbStoreError;
fn get(&self, hash: &Hash) -> Result<Option<Arc<Node>>, Self::Error> {
if let Some(node) = self.cache_get(hash) {
return Ok(Some(node));
}
Err(IndexedDbStoreError::AsyncRequired { hash: *hash })
}
fn put(&mut self, node: &Node) -> Result<Hash, Self::Error> {
Err(IndexedDbStoreError::AsyncRequired { hash: node.hash() })
}
}
#[derive(Debug)]
pub enum IndexedDbStoreError {
InvalidCapacity,
IndexedDb(String),
Compression(String),
Deserialise(String),
AsyncRequired { hash: Hash },
}
impl fmt::Display for IndexedDbStoreError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidCapacity => write!(formatter, "cache capacity must be greater than zero"),
Self::IndexedDb(error) => write!(formatter, "IndexedDB error: {error}"),
Self::Compression(error) => write!(formatter, "zstd compression error: {error}"),
Self::Deserialise(error) => write!(formatter, "node deserialisation error: {error}"),
Self::AsyncRequired { hash } => write!(
formatter,
"IndexedDB NodeStore cache miss for {hash} requires async worker transaction"
),
}
}
}
impl std::error::Error for IndexedDbStoreError {}
impl From<CacheError> for IndexedDbStoreError {
fn from(error: CacheError) -> Self {
match error {
CacheError::InvalidCapacity => Self::InvalidCapacity,
}
}
}
fn encode_node(node: &Node) -> Result<Vec<u8>, IndexedDbStoreError> {
zrip::compress(node.serialise().as_slice(), COMPRESSION_LEVEL)
.map_err(|error| IndexedDbStoreError::Compression(error.to_string()))
}
fn decode_compressed_node(compressed: &[u8]) -> Result<Node, IndexedDbStoreError> {
let serialised = zrip::decompress(compressed)
.map_err(|error| IndexedDbStoreError::Compression(error.to_string()))?;
Node::deserialise(&serialised)
.map_err(|error| IndexedDbStoreError::Deserialise(error.to_string()))
}