Files
gpx.studio/wasm/src/idb.rs
T

274 lines
10 KiB
Rust
Raw Normal View History

2026-10-31 18:08:22 +01:00
//! 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(())
}
}