use crate::canvas::CanvasDocument;
use crate::storage::{Storage, StorageResult};
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::time::{Duration, Instant};
#[cfg(target_arch = "wasm32")]
use web_time::{Duration, Instant};
pub const DEFAULT_AUTOSAVE_INTERVAL_SECS: u64 = 30;
pub const LAST_DOCUMENT_KEY: &str = "__last_document__";
pub struct AutoSaveManager<S: Storage> {
storage: Arc<S>,
interval: Duration,
last_save: Option<Instant>,
dirty: bool,
current_doc_id: Option<String>,
}
impl<S: Storage> AutoSaveManager<S> {
pub fn new(storage: Arc<S>) -> Self {
Self {
storage,
interval: Duration::from_secs(DEFAULT_AUTOSAVE_INTERVAL_SECS),
last_save: None,
dirty: false,
current_doc_id: None,
}
}
pub fn set_interval(&mut self, interval: Duration) {
self.interval = interval;
}
pub fn interval(&self) -> Duration {
self.interval
}
pub fn mark_dirty(&mut self) {
self.dirty = true;
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn set_document_id(&mut self, id: Option<String>) {
self.current_doc_id = id;
}
pub fn document_id(&self) -> Option<&str> {
self.current_doc_id.as_deref()
}
pub fn should_save(&self) -> bool {
if !self.dirty {
return false;
}
match self.last_save {
Some(last) => last.elapsed() >= self.interval,
None => true, }
}
pub async fn maybe_save(&mut self, document: &CanvasDocument) -> StorageResult<bool> {
if !self.should_save() {
return Ok(false);
}
self.save(document).await?;
Ok(true)
}
pub async fn save(&mut self, document: &CanvasDocument) -> StorageResult<()> {
let doc_id = self.current_doc_id
.clone()
.unwrap_or_else(|| document.id.clone());
self.storage.save(&doc_id, document).await?;
self.storage.save(LAST_DOCUMENT_KEY, document).await?;
self.last_save = Some(Instant::now());
self.dirty = false;
Ok(())
}
pub async fn load(&mut self, id: &str) -> StorageResult<CanvasDocument> {
let doc = self.storage.load(id).await?;
self.current_doc_id = Some(id.to_string());
self.dirty = false;
self.last_save = Some(Instant::now());
Ok(doc)
}
pub async fn load_last(&mut self) -> Option<CanvasDocument> {
match self.storage.load(LAST_DOCUMENT_KEY).await {
Ok(doc) => {
self.current_doc_id = Some(doc.id.clone());
self.dirty = false;
self.last_save = Some(Instant::now());
Some(doc)
}
Err(_) => None,
}
}
pub async fn delete(&self, id: &str) -> StorageResult<()> {
self.storage.delete(id).await
}
pub async fn list_documents(&self) -> StorageResult<Vec<String>> {
let mut docs = self.storage.list().await?;
docs.retain(|id| id != LAST_DOCUMENT_KEY);
Ok(docs)
}
pub async fn exists(&self, id: &str) -> StorageResult<bool> {
self.storage.exists(id).await
}
pub fn storage(&self) -> &Arc<S> {
&self.storage
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn create_default_storage() -> StorageResult<Arc<crate::storage::FileStorage>> {
Ok(Arc::new(crate::storage::FileStorage::default_location()?))
}
#[cfg(target_arch = "wasm32")]
pub fn create_default_storage() -> StorageResult<Arc<crate::storage::IndexedDbStorage>> {
Ok(Arc::new(crate::storage::IndexedDbStorage::new()))
}
#[cfg(not(target_arch = "wasm32"))]
pub type PlatformStorage = crate::storage::FileStorage;
#[cfg(target_arch = "wasm32")]
pub type PlatformStorage = crate::storage::IndexedDbStorage;
pub type PlatformAutoSaveManager = AutoSaveManager<PlatformStorage>;
pub fn create_autosave_manager() -> StorageResult<PlatformAutoSaveManager> {
let storage = create_default_storage()?;
Ok(AutoSaveManager::new(storage))
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use crate::storage::MemoryStorage;
fn block_on<F: std::future::Future>(f: F) -> F::Output {
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
fn dummy_raw_waker() -> RawWaker {
fn no_op(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker { dummy_raw_waker() }
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, no_op, no_op, no_op);
RawWaker::new(std::ptr::null(), &VTABLE)
}
let waker = unsafe { Waker::from_raw(dummy_raw_waker()) };
let mut cx = Context::from_waker(&waker);
let mut f = std::pin::pin!(f);
loop {
match f.as_mut().poll(&mut cx) {
Poll::Ready(result) => return result,
Poll::Pending => {}
}
}
}
#[test]
fn test_autosave_manager_creation() {
let storage = Arc::new(MemoryStorage::new());
let manager = AutoSaveManager::new(storage);
assert!(!manager.is_dirty());
assert!(!manager.should_save());
}
#[test]
fn test_autosave_dirty_flag() {
let storage = Arc::new(MemoryStorage::new());
let mut manager = AutoSaveManager::new(storage);
assert!(!manager.is_dirty());
manager.mark_dirty();
assert!(manager.is_dirty());
assert!(manager.should_save());
}
#[test]
fn test_autosave_save_clears_dirty() {
let storage = Arc::new(MemoryStorage::new());
let mut manager = AutoSaveManager::new(storage);
manager.mark_dirty();
assert!(manager.is_dirty());
let doc = CanvasDocument::new();
block_on(manager.save(&doc)).unwrap();
assert!(!manager.is_dirty());
}
#[test]
fn test_autosave_load_last() {
let storage = Arc::new(MemoryStorage::new());
let mut manager = AutoSaveManager::new(storage);
let mut doc = CanvasDocument::new();
doc.name = "Test Document".to_string();
manager.mark_dirty();
block_on(manager.save(&doc)).unwrap();
let storage2 = manager.storage().clone();
let mut manager2 = AutoSaveManager::new(storage2);
let loaded = block_on(manager2.load_last()).expect("Should load last document");
assert_eq!(loaded.name, "Test Document");
}
#[test]
fn test_autosave_list_excludes_special_key() {
let storage = Arc::new(MemoryStorage::new());
let mut manager = AutoSaveManager::new(storage);
let doc = CanvasDocument::new();
manager.mark_dirty();
block_on(manager.save(&doc)).unwrap();
let list = block_on(manager.list_documents()).unwrap();
assert!(!list.contains(&LAST_DOCUMENT_KEY.to_string()));
}
}