mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-01 20:19:39 +00:00
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>
This commit is contained in:
1 parent
6a0d434371
commit
8fe7d8d64d
484 files changed
+53949
-41152
No files matched your search
+274
@@ -0,0 +1,274 @@
|
||||
//! The storage of the engine in the browser: IndexedDB.
|
||||
//!
|
||||
//! Values are stored as `Uint8Array`s under string keys (the hyphenated UUIDs), in one object
|
||||
//! store per kind of data. A batch is a single transaction, so that the stored files always are
|
||||
//! ones that the engine had. The head is a number in the meta store, which the transaction of a
|
||||
//! batch checks before it writes anything (so that a tab that is behind cannot write over what
|
||||
//! another tab saved).
|
||||
|
||||
use gpx_engine::{
|
||||
Batch, ChunkKey, ChunkKind, CommitError, FileId, Revision, Storage, StorageError, StoredData,
|
||||
};
|
||||
use idb::TransactionMode;
|
||||
use idb::{Database, DatabaseEvent, Factory, ObjectStore, ObjectStoreParams, Query, Transaction};
|
||||
use js_sys::Uint8Array;
|
||||
use uuid::Uuid;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
const FILES: &str = "files";
|
||||
const TRACKPOINT_CHUNKS: &str = "trackpoint_chunks";
|
||||
const WAYPOINT_CHUNKS: &str = "waypoint_chunks";
|
||||
/// The order of the files and the head, under their own names.
|
||||
const META: &str = "meta";
|
||||
const SETTINGS: &str = "settings";
|
||||
const STORES: [&str; 5] = [FILES, TRACKPOINT_CHUNKS, WAYPOINT_CHUNKS, META, SETTINGS];
|
||||
/// The stores of the files, which a commit writes to together with the head.
|
||||
const COMMIT_STORES: [&str; 4] = [FILES, TRACKPOINT_CHUNKS, WAYPOINT_CHUNKS, META];
|
||||
|
||||
const ORDER_KEY: &str = "order";
|
||||
const HEAD_KEY: &str = "head";
|
||||
/// Version of the database.
|
||||
const VERSION: u32 = 1;
|
||||
|
||||
pub struct IdbStorage {
|
||||
database: Database,
|
||||
}
|
||||
|
||||
fn error(error: impl std::fmt::Display) -> StorageError {
|
||||
StorageError::Failed(error.to_string())
|
||||
}
|
||||
|
||||
/// The object store of a kind of chunks.
|
||||
fn chunk_store(kind: ChunkKind) -> &'static str {
|
||||
match kind {
|
||||
ChunkKind::Trackpoints => TRACKPOINT_CHUNKS,
|
||||
ChunkKind::Waypoints => WAYPOINT_CHUNKS,
|
||||
}
|
||||
}
|
||||
|
||||
fn key(id: Uuid) -> JsValue {
|
||||
JsValue::from_str(&id.to_string())
|
||||
}
|
||||
|
||||
fn bytes(value: &[u8]) -> JsValue {
|
||||
Uint8Array::from(value).into()
|
||||
}
|
||||
|
||||
fn to_vec(value: &JsValue) -> Vec<u8> {
|
||||
Uint8Array::new(value).to_vec()
|
||||
}
|
||||
|
||||
fn to_uuid(key: &JsValue) -> Result<Uuid, StorageError> {
|
||||
key.as_string()
|
||||
.and_then(|key| Uuid::parse_str(&key).ok())
|
||||
.ok_or_else(|| error("a key is not an id"))
|
||||
}
|
||||
|
||||
impl IdbStorage {
|
||||
/// Opens the database, which is created if it does not exist.
|
||||
pub async fn open(name: &str) -> Result<Self, StorageError> {
|
||||
let factory = Factory::new().map_err(error)?;
|
||||
let mut request = factory.open(name, Some(VERSION)).map_err(error)?;
|
||||
request.on_upgrade_needed(|event| {
|
||||
if let Ok(database) = event.database() {
|
||||
for store in STORES {
|
||||
let _ = database.create_object_store(store, ObjectStoreParams::new());
|
||||
}
|
||||
}
|
||||
});
|
||||
let database = request.await.map_err(error)?;
|
||||
Ok(Self { database })
|
||||
}
|
||||
|
||||
/// The head, 0 for a database that was never written to.
|
||||
async fn read_head(meta: &ObjectStore) -> Result<Revision, StorageError> {
|
||||
let head = meta
|
||||
.get(JsValue::from_str(HEAD_KEY))
|
||||
.map_err(error)?
|
||||
.await
|
||||
.map_err(error)?;
|
||||
Ok(Revision(
|
||||
head.and_then(|head| head.as_f64()).unwrap_or_default() as u64,
|
||||
))
|
||||
}
|
||||
|
||||
fn transaction(
|
||||
&self,
|
||||
stores: &[&str],
|
||||
mode: TransactionMode,
|
||||
) -> Result<Transaction, StorageError> {
|
||||
self.database.transaction(stores, mode).map_err(error)
|
||||
}
|
||||
|
||||
/// Everything stored in the object store, as `(key, bytes)`.
|
||||
async fn read_all(store: &ObjectStore) -> Result<Vec<(JsValue, Vec<u8>)>, StorageError> {
|
||||
let keys = store.get_all_keys(None, None).map_err(error)?;
|
||||
let values = store.get_all(None, None).map_err(error)?;
|
||||
let (keys, values) = (keys.await.map_err(error)?, values.await.map_err(error)?);
|
||||
Ok(keys.into_iter().zip(values.iter().map(to_vec)).collect())
|
||||
}
|
||||
|
||||
/// Writes a batch in a transaction, if the head is the one it is based on. Gives the new head.
|
||||
async fn write(
|
||||
&self,
|
||||
transaction: &Transaction,
|
||||
batch: &Batch,
|
||||
) -> Result<Revision, CommitError> {
|
||||
let meta = transaction.object_store(META).map_err(error)?;
|
||||
let head = Self::read_head(&meta).await?;
|
||||
if head != batch.expect_head {
|
||||
return Err(CommitError::Stale { head });
|
||||
}
|
||||
let new_head = head.next();
|
||||
let files = transaction.object_store(FILES).map_err(error)?;
|
||||
|
||||
// the requests are all made before waiting for them
|
||||
let mut puts = vec![];
|
||||
let mut deletes = vec![];
|
||||
for (ChunkKey { kind, id }, data) in &batch.put_chunks {
|
||||
let store = transaction
|
||||
.object_store(chunk_store(*kind))
|
||||
.map_err(error)?;
|
||||
puts.push(store.put(&bytes(data), Some(&key(*id))).map_err(error)?);
|
||||
}
|
||||
for (FileId(id), data) in &batch.put_files {
|
||||
puts.push(files.put(&bytes(data), Some(&key(*id))).map_err(error)?);
|
||||
}
|
||||
if let Some(order) = &batch.order {
|
||||
let value = bytes(order);
|
||||
puts.push(
|
||||
meta.put(&value, Some(&JsValue::from_str(ORDER_KEY)))
|
||||
.map_err(error)?,
|
||||
);
|
||||
}
|
||||
puts.push(
|
||||
meta.put(
|
||||
&JsValue::from_f64(new_head.0 as f64),
|
||||
Some(&JsValue::from_str(HEAD_KEY)),
|
||||
)
|
||||
.map_err(error)?,
|
||||
);
|
||||
for FileId(id) in &batch.delete_files {
|
||||
deletes.push(files.delete(Query::Key(key(*id))).map_err(error)?);
|
||||
}
|
||||
for ChunkKey { kind, id } in &batch.delete_chunks {
|
||||
let store = transaction
|
||||
.object_store(chunk_store(*kind))
|
||||
.map_err(error)?;
|
||||
deletes.push(store.delete(Query::Key(key(*id))).map_err(error)?);
|
||||
}
|
||||
for put in puts {
|
||||
put.await.map_err(error)?;
|
||||
}
|
||||
for delete in deletes {
|
||||
delete.await.map_err(error)?;
|
||||
}
|
||||
Ok(new_head)
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for IdbStorage {
|
||||
async fn load(&self) -> Result<StoredData, StorageError> {
|
||||
let transaction = self.transaction(
|
||||
&[FILES, TRACKPOINT_CHUNKS, WAYPOINT_CHUNKS, META],
|
||||
TransactionMode::ReadOnly,
|
||||
)?;
|
||||
let store = |name| transaction.object_store(name).map_err(error);
|
||||
let files = Self::read_all(&store(FILES)?).await?;
|
||||
let trackpoint_chunks = Self::read_all(&store(TRACKPOINT_CHUNKS)?).await?;
|
||||
let waypoint_chunks = Self::read_all(&store(WAYPOINT_CHUNKS)?).await?;
|
||||
let meta = store(META)?;
|
||||
let order = meta
|
||||
.get(JsValue::from_str(ORDER_KEY))
|
||||
.map_err(error)?
|
||||
.await
|
||||
.map_err(error)?;
|
||||
let head = Self::read_head(&meta).await?;
|
||||
transaction.await.map_err(error)?;
|
||||
|
||||
let ids = |records: Vec<(JsValue, Vec<u8>)>| -> Result<Vec<(Uuid, Vec<u8>)>, StorageError> {
|
||||
records
|
||||
.into_iter()
|
||||
.map(|(k, v)| Ok((to_uuid(&k)?, v)))
|
||||
.collect()
|
||||
};
|
||||
Ok(StoredData {
|
||||
files: ids(files)?
|
||||
.into_iter()
|
||||
.map(|(id, data)| (FileId(id), data))
|
||||
.collect(),
|
||||
chunks: ids(trackpoint_chunks)?
|
||||
.into_iter()
|
||||
.map(|(id, data)| (ChunkKey::trackpoints(id), data))
|
||||
.chain(
|
||||
ids(waypoint_chunks)?
|
||||
.into_iter()
|
||||
.map(|(id, data)| (ChunkKey::waypoints(id), data)),
|
||||
)
|
||||
.collect(),
|
||||
order: order.as_ref().map(to_vec),
|
||||
head,
|
||||
})
|
||||
}
|
||||
|
||||
async fn head(&self) -> Result<Revision, StorageError> {
|
||||
let transaction = self.transaction(&[META], TransactionMode::ReadOnly)?;
|
||||
let meta = transaction.object_store(META).map_err(error)?;
|
||||
let head = Self::read_head(&meta).await?;
|
||||
transaction.await.map_err(error)?;
|
||||
Ok(head)
|
||||
}
|
||||
|
||||
async fn commit(&self, batch: &Batch) -> Result<Revision, CommitError> {
|
||||
let transaction = self.transaction(&COMMIT_STORES, TransactionMode::ReadWrite)?;
|
||||
match self.write(&transaction, batch).await {
|
||||
Ok(head) => {
|
||||
transaction.commit().map_err(error)?.await.map_err(error)?;
|
||||
Ok(head)
|
||||
}
|
||||
Err(err) => {
|
||||
// nothing of the batch is kept
|
||||
let _ = transaction.abort();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_settings(&self) -> Result<Vec<(String, String)>, StorageError> {
|
||||
let transaction = self.transaction(&[SETTINGS], TransactionMode::ReadOnly)?;
|
||||
let store = transaction.object_store(SETTINGS).map_err(error)?;
|
||||
let keys = store.get_all_keys(None, None).map_err(error)?;
|
||||
let values = store.get_all(None, None).map_err(error)?;
|
||||
let (keys, values) = (keys.await.map_err(error)?, values.await.map_err(error)?);
|
||||
transaction.await.map_err(error)?;
|
||||
Ok(keys
|
||||
.into_iter()
|
||||
.zip(values)
|
||||
.filter_map(|(key, value)| Some((key.as_string()?, value.as_string()?)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn put_setting(&self, key: &str, value: &str) -> Result<(), StorageError> {
|
||||
let transaction = self.transaction(&[SETTINGS], TransactionMode::ReadWrite)?;
|
||||
let store = transaction.object_store(SETTINGS).map_err(error)?;
|
||||
store
|
||||
.put(&JsValue::from_str(value), Some(&JsValue::from_str(key)))
|
||||
.map_err(error)?
|
||||
.await
|
||||
.map_err(error)?;
|
||||
transaction.commit().map_err(error)?.await.map_err(error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_setting(&self, key: &str) -> Result<(), StorageError> {
|
||||
let transaction = self.transaction(&[SETTINGS], TransactionMode::ReadWrite)?;
|
||||
let store = transaction.object_store(SETTINGS).map_err(error)?;
|
||||
store
|
||||
.delete(Query::Key(JsValue::from_str(key)))
|
||||
.map_err(error)?
|
||||
.await
|
||||
.map_err(error)?;
|
||||
transaction.commit().map_err(error)?.await.map_err(error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user