//! 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. //! //! Several engines can write to a storage (tabs, a server). It has a head, a revision that each //! commit advances, and a commit only applies if it is based on the current head: an engine that //! is behind is refused, and has to read the storage again, which replaces what it holds, before //! it writes. So what an engine knows of the storage is exact whenever it writes, which is what //! lets it write only what changed and delete what it does not use anymore. //! //! The surface, highway and scales of the trackpoints are stored as names, in the chunk that has //! them: the codes that the engine gives them (see [`crate::TrackpointCategories`]) depend on the //! order in which it learned the names, so they mean nothing outside of it. A storage that two //! engines (tabs, a server) write to therefore has nothing to agree on but the chunks. mod codec; mod memory; mod persister; pub use memory::*; pub use persister::*; use uuid::Uuid; use crate::FileId; #[derive(Debug, Clone, PartialEq, Eq)] pub enum StorageError { /// The storage could not be read or written. Failed(String), /// Some of what is stored was written by a newer version of the app, which this one cannot /// read. Nothing is written then, so that the newer version finds its data again. Newer, } impl std::fmt::Display for StorageError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Failed(message) => write!(f, "storage error: {message}"), Self::Newer => write!(f, "stored by a newer version"), } } } impl std::error::Error for StorageError {} /// A state of the storage: the head is the revision of the last commit, 0 for a storage that was /// never written to. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Revision(pub u64); impl Revision { pub fn next(self) -> Self { Self(self.0 + 1) } } /// Why a commit did not happen. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CommitError { /// The head is not the one the commit is based on: another engine wrote since. Stale { head: Revision, }, Failed(StorageError), } impl std::fmt::Display for CommitError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Stale { head } => write!(f, "the storage changed (now at revision {})", head.0), Self::Failed(error) => error.fmt(f), } } } impl std::error::Error for CommitError {} impl From for CommitError { fn from(error: StorageError) -> Self { Self::Failed(error) } } /// 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 revision of the storage that this is the state of. pub head: Revision, } /// 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>, /// The head that the batch is based on: it is only applied if the head still is that one. pub expect_head: Revision, } 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; /// The head of the storage. async fn head(&self) -> Result; /// Applies a batch if the head is the one it is based on (`Stale` otherwise): either all of it /// is stored, or none. The head advances. Gives the new head. async fn commit(&self, batch: &Batch) -> Result; /// 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() }, ]; assert!(holds.iter().all(|batch| !batch.is_empty())); } #[test] fn test_storage_error_is_readable() { let error = StorageError::Failed("disk full".into()); assert_eq!(error.to_string(), "storage error: disk full"); let _: &dyn std::error::Error = &error; } }