feat: improve mobile native impl (#14481)

fix #13529 

#### PR Dependency Tree


* **PR #14481** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Mobile blob caching with file-backed storage for faster loads and
reduced network usage
* Blob decoding with lazy refresh on token-read failures for improved
reliability
  * Full-text search/indexing exposed to mobile apps
* Document sync APIs and peer clock management for robust cross-device
sync

* **Tests**
* Added unit tests covering payload decoding, cache safety, and
concurrency

* **Dependencies**
* Added an LRU cache dependency and a new mobile-shared package for
shared mobile logic
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-21 04:13:24 +08:00
committed by GitHub
parent d8cc0acdd0
commit c9bffc13b5
37 changed files with 2325 additions and 866 deletions
@@ -8,6 +8,8 @@ pub enum Error {
SqlxError(#[from] sqlx::Error),
#[error("Migrate Error: {0}")]
MigrateError(#[from] sqlx::migrate::MigrateError),
#[error("Connection in progress")]
ConnectionInProgress,
#[error("Invalid operation")]
InvalidOperation,
#[error("Serialization Error: {0}")]
+8 -1
View File
@@ -106,7 +106,7 @@ impl DocStoragePool {
})
}
async fn get(&self, universal_id: String) -> Result<Ref<'_, SqliteDocStorage>> {
async fn get(&self, universal_id: String) -> Result<Ref<SqliteDocStorage>> {
Ok(self.pool.get(universal_id).await?)
}
@@ -504,4 +504,11 @@ mod tests {
assert_eq!(err.status, napi::Status::GenericFailure);
assert!(err.reason.contains("Invalid operation"));
}
#[test]
fn napi_error_mapping_connection_in_progress() {
let err: napi::Error = error::Error::ConnectionInProgress.into();
assert_eq!(err.status, napi::Status::GenericFailure);
assert!(err.reason.contains("Connection in progress"));
}
}
+96 -59
View File
@@ -1,92 +1,129 @@
use core::ops::{Deref, DerefMut};
use std::collections::hash_map::{Entry, HashMap};
use core::ops::Deref;
use std::{
collections::hash_map::{Entry, HashMap},
sync::Arc,
};
use tokio::sync::{RwLock, RwLockMappedWriteGuard, RwLockReadGuard, RwLockWriteGuard};
use tokio::sync::RwLock;
use super::{
error::{Error, Result},
storage::SqliteDocStorage,
};
pub struct Ref<'a, V> {
_guard: RwLockReadGuard<'a, V>,
pub struct Ref<V> {
inner: Arc<V>,
}
impl<V> Deref for Ref<'_, V> {
impl<V> Deref for Ref<V> {
type Target = V;
fn deref(&self) -> &Self::Target {
self._guard.deref()
}
}
pub struct RefMut<'a, V> {
_guard: RwLockMappedWriteGuard<'a, V>,
}
impl<V> Deref for RefMut<'_, V> {
type Target = V;
fn deref(&self) -> &Self::Target {
&self._guard
}
}
impl<V> DerefMut for RefMut<'_, V> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self._guard
self.inner.deref()
}
}
#[derive(Default)]
pub struct SqliteDocStoragePool {
inner: RwLock<HashMap<String, SqliteDocStorage>>,
inner: RwLock<HashMap<String, StorageState>>,
}
enum StorageState {
Connecting(Arc<SqliteDocStorage>),
Connected(Arc<SqliteDocStorage>),
}
impl SqliteDocStoragePool {
async fn get_or_create_storage<'a>(&'a self, universal_id: String, path: &str) -> RefMut<'a, SqliteDocStorage> {
let lock = RwLockWriteGuard::map(self.inner.write().await, |lock| match lock.entry(universal_id) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let storage = SqliteDocStorage::new(path.to_string());
entry.insert(storage)
}
});
RefMut { _guard: lock }
}
pub async fn get(&self, universal_id: String) -> Result<Ref<'_, SqliteDocStorage>> {
let lock = RwLockReadGuard::try_map(self.inner.read().await, |lock| {
if let Some(storage) = lock.get(&universal_id) {
Some(storage)
} else {
None
}
});
match lock {
Ok(guard) => Ok(Ref { _guard: guard }),
Err(_) => Err(Error::InvalidOperation),
}
pub async fn get(&self, universal_id: String) -> Result<Ref<SqliteDocStorage>> {
let lock = self.inner.read().await;
let Some(state) = lock.get(&universal_id) else {
return Err(Error::InvalidOperation);
};
let StorageState::Connected(storage) = state else {
return Err(Error::InvalidOperation);
};
Ok(Ref {
inner: Arc::clone(storage),
})
}
/// Initialize the database and run migrations.
pub async fn connect(&self, universal_id: String, path: String) -> Result<()> {
let storage = self.get_or_create_storage(universal_id.to_owned(), &path).await;
let storage = {
let mut lock = self.inner.write().await;
match lock.entry(universal_id.clone()) {
Entry::Occupied(entry) => match entry.get() {
StorageState::Connected(_) => return Ok(()),
StorageState::Connecting(_) => return Err(Error::ConnectionInProgress),
},
Entry::Vacant(entry) => {
let storage = Arc::new(SqliteDocStorage::new(path));
entry.insert(StorageState::Connecting(Arc::clone(&storage)));
storage
}
}
};
storage.connect().await?;
Ok(())
}
if let Err(err) = storage.connect().await {
let mut lock = self.inner.write().await;
if matches!(
lock.get(&universal_id),
Some(StorageState::Connecting(existing)) if Arc::ptr_eq(existing, &storage)
) {
lock.remove(&universal_id);
}
return Err(err);
}
pub async fn disconnect(&self, universal_id: String) -> Result<()> {
let mut lock = self.inner.write().await;
let mut transitioned = false;
{
let mut lock = self.inner.write().await;
if matches!(
lock.get(&universal_id),
Some(StorageState::Connecting(existing)) if Arc::ptr_eq(existing, &storage)
) {
lock.insert(universal_id.clone(), StorageState::Connected(Arc::clone(&storage)));
transitioned = true;
}
}
if let Entry::Occupied(entry) = lock.entry(universal_id) {
let storage = entry.remove();
if !transitioned {
let mut lock = self.inner.write().await;
if matches!(
lock.get(&universal_id),
Some(StorageState::Connecting(existing)) if Arc::ptr_eq(existing, &storage)
) {
lock.remove(&universal_id);
}
drop(lock);
storage.close().await;
return Err(Error::InvalidOperation);
}
Ok(())
}
pub async fn disconnect(&self, universal_id: String) -> Result<()> {
let storage = {
let mut lock = self.inner.write().await;
match lock.get(&universal_id) {
None => return Ok(()),
Some(StorageState::Connecting(_)) => return Err(Error::ConnectionInProgress),
Some(StorageState::Connected(storage)) => {
// Prevent shutting down the shared storage while requests still hold refs.
if Arc::strong_count(storage) > 1 {
return Err(Error::InvalidOperation);
}
}
}
let Some(StorageState::Connected(storage)) = lock.remove(&universal_id) else {
return Err(Error::InvalidOperation);
};
storage
};
storage.close().await;
Ok(())
}
}