mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-27 02:27:18 +00:00
progress
This commit is contained in:
1 parent
41b9116c6a
commit
97d597e139
181 files changed
+1970
-2643
No files matched your search
@@ -0,0 +1,231 @@
|
||||
//! 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<StorageError> 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<u8>)>,
|
||||
pub chunks: Vec<(ChunkKey, Vec<u8>)>,
|
||||
/// The order of the files.
|
||||
pub order: Option<Vec<u8>>,
|
||||
/// 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<u8>)>,
|
||||
pub delete_files: Vec<FileId>,
|
||||
pub put_chunks: Vec<(ChunkKey, Vec<u8>)>,
|
||||
pub delete_chunks: Vec<ChunkKey>,
|
||||
pub order: Option<Vec<u8>>,
|
||||
/// 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<StoredData, StorageError>;
|
||||
|
||||
/// The head of the storage.
|
||||
async fn head(&self) -> Result<Revision, StorageError>;
|
||||
|
||||
/// 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<Revision, CommitError>;
|
||||
|
||||
/// Reads the settings, as `(key, value)`. The engine does not look into them.
|
||||
async fn load_settings(&self) -> Result<Vec<(String, String)>, 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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user