//! Keeping the files and the settings between sessions. //! //! The engine does not know where the data goes: the host (the browser, a desktop app...) //! implements [`Storage`], and a [`Persister`] works out what to write after each change. //! //! The files are not stored as a whole. Their trackpoints and waypoints are stored in chunks (see //! [`crate::Chunked`]) that are written once, under their identity, and a file is a small record //! that lists the chunks it is made of. An edit only writes the new chunks and the records of the //! files that changed; the chunks that nothing refers to anymore are deleted at the same time. mod codec; mod memory; mod persister; pub use memory::*; pub use persister::*; use uuid::Uuid; use crate::FileId; #[derive(Debug, Clone, PartialEq, Eq)] pub struct StorageError(pub String); impl std::fmt::Display for StorageError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "storage error: {}", self.0) } } impl std::error::Error for StorageError {} /// What a chunk holds. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ChunkKind { Trackpoints, Waypoints, } /// Where a chunk is stored: chunks are told apart by their identity within their kind. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ChunkKey { pub kind: ChunkKind, pub id: Uuid, } impl ChunkKey { pub fn trackpoints(id: Uuid) -> Self { Self { kind: ChunkKind::Trackpoints, id, } } pub fn waypoints(id: Uuid) -> Self { Self { kind: ChunkKind::Waypoints, id, } } } /// Everything that is stored about the files, as opaque bytes (see the module documentation). #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct StoredData { /// The record of each file. pub files: Vec<(FileId, Vec)>, pub chunks: Vec<(ChunkKey, Vec)>, /// The order of the files. pub order: Option>, /// The categories of the trackpoints, which their chunks refer to. pub categories: Option>, } /// A change of the stored data, to be applied all at once: a stored state is always one that the /// engine had. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Batch { pub put_files: Vec<(FileId, Vec)>, pub delete_files: Vec, pub put_chunks: Vec<(ChunkKey, Vec)>, pub delete_chunks: Vec, pub order: Option>, pub categories: Option>, } impl Batch { pub fn is_empty(&self) -> bool { self == &Self::default() } /// How many chunks of the kind the batch writes. pub fn puts(&self, kind: ChunkKind) -> usize { self.put_chunks .iter() .filter(|(key, _)| key.kind == kind) .count() } /// How many chunks of the kind the batch deletes. pub fn deletes(&self, kind: ChunkKind) -> usize { self.delete_chunks .iter() .filter(|key| key.kind == kind) .count() } } /// Where the data is kept. The implementation belongs to the host; [`MemoryStorage`] is one that /// keeps nothing after the session. /// /// The methods are async because storage often is (IndexedDB), the engine itself is not: it /// changes its files at once and the changes are written afterwards, see [`Persister`]. #[allow(async_fn_in_trait)] pub trait Storage { /// Reads the files. async fn load(&self) -> Result; /// Applies a batch: either all of it is stored, or none. async fn commit(&self, batch: &Batch) -> Result<(), StorageError>; /// Reads the settings, as `(key, value)`. The engine does not look into them. async fn load_settings(&self) -> Result, StorageError>; async fn put_setting(&self, key: &str, value: &str) -> Result<(), StorageError>; async fn delete_setting(&self, key: &str) -> Result<(), StorageError>; } #[cfg(test)] mod tests { use super::*; #[test] fn test_batch_is_empty_until_it_holds_something() { assert!(Batch::default().is_empty()); let holds = [ Batch { put_files: vec![(FileId::default(), vec![])], ..Default::default() }, Batch { delete_files: vec![FileId::default()], ..Default::default() }, Batch { put_chunks: vec![(ChunkKey::waypoints(Uuid::new_v4()), vec![])], ..Default::default() }, Batch { delete_chunks: vec![ChunkKey::trackpoints(Uuid::new_v4())], ..Default::default() }, Batch { order: Some(vec![]), ..Default::default() }, Batch { categories: Some(vec![]), ..Default::default() }, ]; assert!(holds.iter().all(|batch| !batch.is_empty())); } #[test] fn test_storage_error_is_readable() { let error = StorageError("disk full".into()); assert_eq!(error.to_string(), "storage error: disk full"); let _: &dyn std::error::Error = &error; } }