Files
gpx.studio/gpx-rs/src/storage/memory.rs
T
vcoppeandvcoppe 8fe7d8d64d Migration to new Rust-based engine (#401)
* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
2026-10-31 18:08:22 +01:00

269 lines
8.9 KiB
Rust

use std::cell::RefCell;
use std::collections::HashMap;
use crate::{
Batch, ChunkKey, ChunkKind, CommitError, FileId, Revision, Storage, StorageError, StoredData,
};
/// A storage that lives as long as the session. It is what an engine without a host storage uses,
/// and what the tests use.
#[derive(Debug, Default)]
pub struct MemoryStorage {
state: RefCell<State>,
}
#[derive(Debug, Default)]
struct State {
head: Revision,
files: HashMap<FileId, Vec<u8>>,
chunks: HashMap<ChunkKey, Vec<u8>>,
order: Option<Vec<u8>>,
settings: HashMap<String, String>,
}
/// How much there is in a storage.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StorageCounts {
pub files: usize,
pub trackpoint_chunks: usize,
pub waypoint_chunks: usize,
}
impl MemoryStorage {
/// How many files and chunks are stored.
pub fn counts(&self) -> StorageCounts {
let state = self.state.borrow();
let chunks = |kind| state.chunks.keys().filter(|key| key.kind == kind).count();
StorageCounts {
files: state.files.len(),
trackpoint_chunks: chunks(ChunkKind::Trackpoints),
waypoint_chunks: chunks(ChunkKind::Waypoints),
}
}
}
impl Storage for MemoryStorage {
async fn load(&self) -> Result<StoredData, StorageError> {
let state = self.state.borrow();
Ok(StoredData {
files: state.files.iter().map(|(k, v)| (*k, v.clone())).collect(),
chunks: state.chunks.iter().map(|(k, v)| (*k, v.clone())).collect(),
order: state.order.clone(),
head: state.head,
})
}
async fn head(&self) -> Result<Revision, StorageError> {
Ok(self.state.borrow().head)
}
async fn commit(&self, batch: &Batch) -> Result<Revision, CommitError> {
let mut state = self.state.borrow_mut();
if state.head != batch.expect_head {
return Err(CommitError::Stale { head: state.head });
}
state.files.extend(batch.put_files.iter().cloned());
for id in &batch.delete_files {
state.files.remove(id);
}
state.chunks.extend(batch.put_chunks.iter().cloned());
for key in &batch.delete_chunks {
state.chunks.remove(key);
}
if let Some(order) = &batch.order {
state.order = Some(order.clone());
}
state.head = state.head.next();
Ok(state.head)
}
async fn load_settings(&self) -> Result<Vec<(String, String)>, StorageError> {
let state = self.state.borrow();
Ok(state
.settings
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect())
}
async fn put_setting(&self, key: &str, value: &str) -> Result<(), StorageError> {
self.state
.borrow_mut()
.settings
.insert(key.to_owned(), value.to_owned());
Ok(())
}
async fn delete_setting(&self, key: &str) -> Result<(), StorageError> {
self.state.borrow_mut().settings.remove(key);
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::task::{Context, Poll, Waker};
use uuid::Uuid;
use super::*;
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = std::pin::pin!(future);
let mut context = Context::from_waker(Waker::noop());
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => value,
Poll::Pending => panic!("the memory storage does not wait"),
}
}
fn counts(files: usize, trackpoint_chunks: usize, waypoint_chunks: usize) -> StorageCounts {
StorageCounts {
files,
trackpoint_chunks,
waypoint_chunks,
}
}
#[test]
fn test_empty_storage_has_nothing() {
let storage = MemoryStorage::default();
let data = block_on(storage.load()).unwrap();
assert_eq!(data, StoredData::default());
assert_eq!(storage.counts(), counts(0, 0, 0));
assert!(block_on(storage.load_settings()).unwrap().is_empty());
}
#[test]
fn test_commit_puts_and_deletes() {
let storage = MemoryStorage::default();
let (a, b) = (FileId::default(), FileId::default());
let (t, w) = (Uuid::new_v4(), Uuid::new_v4());
block_on(storage.commit(&Batch {
put_files: vec![(a, vec![1]), (b, vec![2])],
put_chunks: vec![
(ChunkKey::trackpoints(t), vec![3]),
(ChunkKey::waypoints(w), vec![4]),
],
order: Some(vec![5]),
expect_head: Revision(0),
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), counts(2, 1, 1));
let mut data = block_on(storage.load()).unwrap();
data.files.sort_by_key(|(_, bytes)| bytes.clone());
assert_eq!(data.files, vec![(a, vec![1]), (b, vec![2])]);
data.chunks.sort_by_key(|(_, bytes)| bytes.clone());
assert_eq!(
data.chunks,
vec![
(ChunkKey::trackpoints(t), vec![3]),
(ChunkKey::waypoints(w), vec![4])
]
);
assert_eq!(data.order, Some(vec![5]));
// a put replaces, a delete removes, what is not mentioned stays
block_on(storage.commit(&Batch {
put_files: vec![(a, vec![10])],
delete_files: vec![b],
delete_chunks: vec![ChunkKey::trackpoints(t)],
expect_head: Revision(1),
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), counts(1, 0, 1));
let data = block_on(storage.load()).unwrap();
assert_eq!(data.files, vec![(a, vec![10])]);
assert_eq!(data.order, Some(vec![5]));
block_on(storage.commit(&Batch {
delete_chunks: vec![ChunkKey::waypoints(w)],
order: Some(vec![]),
expect_head: Revision(2),
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), counts(1, 0, 0));
assert_eq!(block_on(storage.load()).unwrap().order, Some(vec![]));
}
#[test]
fn test_settings() {
let storage = MemoryStorage::default();
block_on(storage.put_setting("a", "1")).unwrap();
block_on(storage.put_setting("b", "2")).unwrap();
block_on(storage.put_setting("a", "3")).unwrap();
let mut settings = block_on(storage.load_settings()).unwrap();
settings.sort();
assert_eq!(
settings,
vec![("a".into(), "3".into()), ("b".into(), "2".into())]
);
block_on(storage.delete_setting("a")).unwrap();
// deleting what is not there is fine
block_on(storage.delete_setting("missing")).unwrap();
assert_eq!(
block_on(storage.load_settings()).unwrap(),
vec![("b".into(), "2".into())]
);
}
#[test]
fn test_settings_are_not_files() {
let storage = MemoryStorage::default();
block_on(storage.put_setting("a", "1")).unwrap();
assert_eq!(block_on(storage.load()).unwrap(), StoredData::default());
}
fn batch(expect_head: u64, entry: u8) -> Batch {
Batch {
put_files: vec![(FileId::default(), vec![entry])],
expect_head: Revision(expect_head),
..Default::default()
}
}
#[test]
fn test_every_commit_advances_the_head() {
let storage = MemoryStorage::default();
assert_eq!(block_on(storage.head()).unwrap(), Revision(0));
assert_eq!(block_on(storage.load()).unwrap().head, Revision(0));
assert_eq!(block_on(storage.commit(&batch(0, 1))), Ok(Revision(1)));
assert_eq!(block_on(storage.commit(&batch(1, 2))), Ok(Revision(2)));
assert_eq!(block_on(storage.head()).unwrap(), Revision(2));
assert_eq!(block_on(storage.load()).unwrap().head, Revision(2));
}
#[test]
fn test_a_commit_based_on_another_head_changes_nothing() {
let storage = MemoryStorage::default();
block_on(storage.commit(&batch(0, 1))).unwrap();
block_on(storage.commit(&batch(1, 2))).unwrap();
let before = block_on(storage.load()).unwrap();
// behind, or ahead: the head is not the one it is based on
for expect in [0, 1, 3] {
let stale = Batch {
delete_files: vec![before.files[0].0],
order: Some(vec![9]),
..batch(expect, 3)
};
assert_eq!(
block_on(storage.commit(&stale)),
Err(CommitError::Stale { head: Revision(2) })
);
}
let after = block_on(storage.load()).unwrap();
assert_eq!(after.head, Revision(2));
assert_eq!(after.files.len(), before.files.len());
assert_eq!(after.order, before.order);
// and it can be done again from the right head
assert_eq!(block_on(storage.commit(&batch(2, 3))), Ok(Revision(3)));
}
}