feat(core): add blob cache wiring and avatar download orchestration
- Add chanora_cache dependency to Cargo.toml - Add blob_cache field to ChanoraSession (Arc<Mutex<Option<BlobCache>>>) - Add init_cache() method: creates BlobCache, runs eviction - Add get_avatar() method: cache-first, download on miss, store in cache - Add clear_cache() and cache_size() methods for cache management - Add CoreError::Cache variant for BlobCacheError conversion - Add avatar_cache integration test
This commit is contained in:
@@ -10,6 +10,7 @@ repository.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chanora_cache = { path = "../../crates/chanora_cache" }
|
||||
chanora_protocol = { path = "../../crates/chanora_protocol" }
|
||||
chanora_state = { path = "../../crates/chanora_state" }
|
||||
chanora_audio = { path = "../../crates/chanora_audio" }
|
||||
|
||||
@@ -92,6 +92,9 @@ pub enum CoreError {
|
||||
/// Storage error.
|
||||
#[error("storage: {0}")]
|
||||
Storage(#[from] chanora_storage::StorageError),
|
||||
/// Blob-cache failure.
|
||||
#[error("cache: {0}")]
|
||||
Cache(#[from] chanora_cache::BlobCacheError),
|
||||
/// Diagnostics error.
|
||||
#[error("diagnostics: {0}")]
|
||||
Diagnostics(#[from] chanora_diagnostics::DiagnosticsError),
|
||||
@@ -197,6 +200,7 @@ pub struct ChanoraSession {
|
||||
/// extension). Lives alongside the identity file. Wired by
|
||||
/// [`Self::init_storage`].
|
||||
bookmark_store: Arc<Mutex<Option<BookmarkRepository>>>,
|
||||
blob_cache: Arc<Mutex<Option<chanora_cache::BlobCache>>>,
|
||||
/// Invisible server-address prefetch cache. Warmed by Flutter typing
|
||||
/// but validated by Rust before Connect can reuse it.
|
||||
server_prefetch: ServerPrefetcher,
|
||||
@@ -252,6 +256,7 @@ impl ChanoraSession {
|
||||
network_tx,
|
||||
identity_store: Arc::new(Mutex::new(None)),
|
||||
bookmark_store: Arc::new(Mutex::new(None)),
|
||||
blob_cache: Arc::new(Mutex::new(None)),
|
||||
server_prefetch: ServerPrefetcher::new(),
|
||||
voice_selector: selector,
|
||||
release_tail,
|
||||
@@ -337,6 +342,65 @@ impl ChanoraSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configure the blob cache root.
|
||||
pub async fn init_cache(&self, dir: &str) -> Result<(), CoreError> {
|
||||
let cache = chanora_cache::BlobCache::new(dir, 100 * 1024 * 1024)?;
|
||||
cache.evict().await?;
|
||||
let mut guard = self.blob_cache.lock().await;
|
||||
*guard = Some(cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve avatar bytes.
|
||||
pub async fn get_avatar(
|
||||
&self,
|
||||
avatar_hash: &str,
|
||||
client_uid: &str,
|
||||
) -> Result<Option<Vec<u8>>, CoreError> {
|
||||
{
|
||||
let guard = self.blob_cache.lock().await;
|
||||
if let Some(ref cache) = *guard {
|
||||
if let Some(bytes) = cache.get(chanora_cache::PREFIX_AVATAR, avatar_hash).await? {
|
||||
return Ok(Some(bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let guard = self.inner.lock().await;
|
||||
let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
|
||||
let bytes = state.protocol.download_avatar(client_uid).await?;
|
||||
|
||||
{
|
||||
let cache_guard = self.blob_cache.lock().await;
|
||||
if let Some(ref cache) = *cache_guard {
|
||||
let _ = cache
|
||||
.put(chanora_cache::PREFIX_AVATAR, avatar_hash, &bytes)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(bytes))
|
||||
}
|
||||
|
||||
/// Purge cached protocol-owned assets.
|
||||
pub async fn clear_cache(&self) -> Result<(), CoreError> {
|
||||
let guard = self.blob_cache.lock().await;
|
||||
if let Some(ref cache) = *guard {
|
||||
cache.clear().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Report the configured blob-cache size.
|
||||
pub async fn cache_size(&self) -> Result<u64, CoreError> {
|
||||
let guard = self.blob_cache.lock().await;
|
||||
if let Some(ref cache) = *guard {
|
||||
Ok(cache.total_size().await?)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// List persisted bookmarks. Returns an empty list if the store
|
||||
/// has not been wired or has no entries.
|
||||
pub async fn list_bookmarks(&self) -> Result<Vec<Bookmark>, CoreError> {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_avatar_returns_cached_bytes_without_connection() {
|
||||
let tmp = mktemp("chanora_core_avatar_cache_test");
|
||||
let session = chanora_core::ChanoraSession::new();
|
||||
session.init_cache(tmp.to_str().unwrap()).await.unwrap();
|
||||
|
||||
let cache = chanora_cache::BlobCache::new(&tmp, 100 * 1024 * 1024).unwrap();
|
||||
let hash = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6";
|
||||
cache
|
||||
.put(chanora_cache::PREFIX_AVATAR, hash, b"avatar-bytes")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bytes = session
|
||||
.get_avatar(hash, "client-uid")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(bytes, b"avatar-bytes");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_avatar_without_cache_or_connection_returns_not_connected() {
|
||||
let session = chanora_core::ChanoraSession::new();
|
||||
let err = session
|
||||
.get_avatar("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "client-uid")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, chanora_core::CoreError::NotConnected));
|
||||
}
|
||||
|
||||
fn mktemp(label: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let p = env::temp_dir()
|
||||
.join(label)
|
||||
.join(format!("{}-{nanos}", process::id()));
|
||||
std::fs::create_dir_all(&p).unwrap();
|
||||
p
|
||||
}
|
||||
Reference in New Issue
Block a user