use super::{BoxFuture, Storage, StorageError, StorageResult};
use crate::canvas::CanvasDocument;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{IdbDatabase, IdbObjectStore, IdbRequest, IdbTransactionMode};
use std::cell::RefCell;
use std::rc::Rc;
const DB_NAME: &str = "drafftink";
const DB_VERSION: u32 = 1;
const STORE_NAME: &str = "documents";
pub struct IndexedDbStorage {
db: Rc<RefCell<Option<IdbDatabase>>>,
}
impl IndexedDbStorage {
pub fn new() -> Self {
Self {
db: Rc::new(RefCell::new(None)),
}
}
async fn get_db(&self) -> StorageResult<IdbDatabase> {
if let Some(db) = self.db.borrow().as_ref() {
return Ok(db.clone());
}
let window = web_sys::window()
.ok_or_else(|| StorageError::Other("No window object".to_string()))?;
let idb_factory = window
.indexed_db()
.map_err(|e| StorageError::Other(format!("IndexedDB error: {:?}", e)))?
.ok_or_else(|| StorageError::Other("IndexedDB not available".to_string()))?;
let open_request = idb_factory
.open_with_u32(DB_NAME, DB_VERSION)
.map_err(|e| StorageError::Other(format!("Failed to open DB: {:?}", e)))?;
let onupgrade = Closure::once(Box::new(move |event: web_sys::IdbVersionChangeEvent| {
let target = event.target().unwrap();
let request: IdbRequest = target.unchecked_into();
let db: IdbDatabase = request.result().unwrap().unchecked_into();
if !db.object_store_names().contains(STORE_NAME) {
db.create_object_store(STORE_NAME)
.expect("Failed to create object store");
}
}) as Box<dyn FnOnce(_)>);
open_request.set_onupgradeneeded(Some(onupgrade.as_ref().unchecked_ref()));
onupgrade.forget();
let db = await_idb_request::<IdbDatabase>(&open_request).await?;
*self.db.borrow_mut() = Some(db.clone());
Ok(db)
}
fn get_store(&self, db: &IdbDatabase, mode: IdbTransactionMode) -> StorageResult<IdbObjectStore> {
let transaction = db
.transaction_with_str_and_mode(STORE_NAME, mode)
.map_err(|e| StorageError::Other(format!("Transaction error: {:?}", e)))?;
transaction
.object_store(STORE_NAME)
.map_err(|e| StorageError::Other(format!("Store error: {:?}", e)))
}
}
impl Default for IndexedDbStorage {
fn default() -> Self {
Self::new()
}
}
impl Storage for IndexedDbStorage {
fn save(&self, id: &str, document: &CanvasDocument) -> BoxFuture<'_, StorageResult<()>> {
let id = id.to_string();
let json = match document.to_json() {
Ok(j) => j,
Err(e) => return Box::pin(async move {
Err(StorageError::Serialization(e.to_string()))
}),
};
Box::pin(async move {
let db = self.get_db().await?;
let store = self.get_store(&db, IdbTransactionMode::Readwrite)?;
let js_value = JsValue::from_str(&json);
let request = store
.put_with_key(&js_value, &JsValue::from_str(&id))
.map_err(|e| StorageError::Other(format!("Put error: {:?}", e)))?;
await_idb_request::<JsValue>(&request).await?;
Ok(())
})
}
fn load(&self, id: &str) -> BoxFuture<'_, StorageResult<CanvasDocument>> {
let id = id.to_string();
Box::pin(async move {
let db = self.get_db().await?;
let store = self.get_store(&db, IdbTransactionMode::Readonly)?;
let request = store
.get(&JsValue::from_str(&id))
.map_err(|e| StorageError::Other(format!("Get error: {:?}", e)))?;
let result = await_idb_request::<JsValue>(&request).await?;
if result.is_undefined() || result.is_null() {
return Err(StorageError::NotFound(id));
}
let json = result
.as_string()
.ok_or_else(|| StorageError::Serialization("Invalid stored data".to_string()))?;
CanvasDocument::from_json(&json)
.map_err(|e| StorageError::Serialization(e.to_string()))
})
}
fn delete(&self, id: &str) -> BoxFuture<'_, StorageResult<()>> {
let id = id.to_string();
Box::pin(async move {
let db = self.get_db().await?;
let store = self.get_store(&db, IdbTransactionMode::Readwrite)?;
let request = store
.delete(&JsValue::from_str(&id))
.map_err(|e| StorageError::Other(format!("Delete error: {:?}", e)))?;
await_idb_request::<JsValue>(&request).await?;
Ok(())
})
}
fn list(&self) -> BoxFuture<'_, StorageResult<Vec<String>>> {
Box::pin(async move {
let db = self.get_db().await?;
let store = self.get_store(&db, IdbTransactionMode::Readonly)?;
let request = store
.get_all_keys()
.map_err(|e| StorageError::Other(format!("GetAllKeys error: {:?}", e)))?;
let result = await_idb_request::<js_sys::Array>(&request).await?;
let mut ids = Vec::new();
for i in 0..result.length() {
if let Some(key) = result.get(i).as_string() {
ids.push(key);
}
}
Ok(ids)
})
}
fn exists(&self, id: &str) -> BoxFuture<'_, StorageResult<bool>> {
let id = id.to_string();
Box::pin(async move {
let db = self.get_db().await?;
let store = self.get_store(&db, IdbTransactionMode::Readonly)?;
let request = store
.count_with_key(&JsValue::from_str(&id))
.map_err(|e| StorageError::Other(format!("Count error: {:?}", e)))?;
let result = await_idb_request::<JsValue>(&request).await?;
let count = result.as_f64().unwrap_or(0.0) as u32;
Ok(count > 0)
})
}
}
async fn await_idb_request<T: JsCast>(request: &IdbRequest) -> StorageResult<T> {
use wasm_bindgen_futures::JsFuture;
let promise = js_sys::Promise::new(&mut |resolve, reject| {
let onsuccess = Closure::once(Box::new(move |event: web_sys::Event| {
let target = event.target().unwrap();
let request: IdbRequest = target.unchecked_into();
let result = request.result().unwrap();
resolve.call1(&JsValue::NULL, &result).unwrap();
}) as Box<dyn FnOnce(_)>);
let onerror = Closure::once(Box::new(move |event: web_sys::Event| {
let _target = event.target();
reject.call1(&JsValue::NULL, &JsValue::from_str("IndexedDB request failed")).unwrap();
}) as Box<dyn FnOnce(_)>);
request.set_onsuccess(Some(onsuccess.as_ref().unchecked_ref()));
request.set_onerror(Some(onerror.as_ref().unchecked_ref()));
onsuccess.forget();
onerror.forget();
});
JsFuture::from(promise)
.await
.map_err(|e| StorageError::Other(format!("IndexedDB request failed: {:?}", e)))?
.dyn_into::<T>()
.map_err(|_| StorageError::Other("Type conversion failed".to_string()))
}