diff --git a/src/application/dtos/search_dto.rs b/src/application/dtos/search_dto.rs index e599da4f..3c5a8f0f 100644 --- a/src/application/dtos/search_dto.rs +++ b/src/application/dtos/search_dto.rs @@ -156,7 +156,7 @@ pub struct SearchFolderResultDto { * both files and folders that match the search criteria, along with pagination * information and server-computed metadata. */ -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct SearchResultsDto { /// Files matching the search criteria (enriched with metadata) pub files: Vec, diff --git a/src/application/ports/inbound.rs b/src/application/ports/inbound.rs index 731b6d2f..6cdba0c3 100644 --- a/src/application/ports/inbound.rs +++ b/src/application/ports/inbound.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use std::sync::Arc; use crate::application::dtos::folder_dto::{ CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto, @@ -92,13 +93,15 @@ pub trait FolderUseCase: Send + Sync + 'static { pub trait SearchUseCase: Send + Sync + 'static { /// Performs a full search based on the specified criteria. /// + /// Returns `Arc` so the cache and the caller share + /// the same allocation — zero-copy on both insert and hit. /// `user_id` identifies the authenticated user so that SQL queries filter /// by owner and the result cache is isolated per tenant. async fn search( &self, criteria: SearchCriteriaDto, user_id: &str, - ) -> Result; + ) -> Result, DomainError>; /// Returns quick suggestions for autocomplete (lightweight, fast). async fn suggest( diff --git a/src/application/services/search_service.rs b/src/application/services/search_service.rs index f25ede7c..84675248 100644 --- a/src/application/services/search_service.rs +++ b/src/application/services/search_service.rs @@ -42,9 +42,9 @@ pub struct SearchService { folder_repository: Arc, /// Lock-free concurrent cache with automatic TTL and LRU eviction (moka). - /// Uses `future::Cache` so `.get()` / `.insert()` never block the Tokio - /// runtime (maintenance work is deferred to an internal async task). - search_cache: moka::future::Cache, + /// Values are `Arc` so cache insert/hit is a single + /// atomic ref-count increment (~1 ns) instead of cloning thousands of Strings. + search_cache: moka::future::Cache>, } // ─── Utility functions (pure, no self — computed on the server) ───────── @@ -134,12 +134,12 @@ impl SearchService { } /// Attempts to retrieve results from the cache. - async fn get_from_cache(&self, key: u64) -> Option { + async fn get_from_cache(&self, key: u64) -> Option> { self.search_cache.get(&key).await } /// Stores results in the cache. - async fn store_in_cache(&self, key: u64, results: SearchResultsDto) { + async fn store_in_cache(&self, key: u64, results: Arc) { self.search_cache.insert(key, results).await; } @@ -272,7 +272,7 @@ impl SearchUseCase for SearchService { * - Human-readable size formatting * - Pagination */ - async fn search(&self, criteria: SearchCriteriaDto, user_id: &str) -> Result { + async fn search(&self, criteria: SearchCriteriaDto, user_id: &str) -> Result> { let start = Instant::now(); // Try to get from cache @@ -359,7 +359,7 @@ impl SearchUseCase for SearchService { let elapsed_ms = start.elapsed().as_millis() as u64; - let search_results = SearchResultsDto::new( + let search_results = Arc::new(SearchResultsDto::new( paginated_files, paginated_folders, criteria.limit, @@ -367,9 +367,9 @@ impl SearchUseCase for SearchService { Some(total_count), elapsed_ms, criteria.sort_by.clone(), - ); + )); - self.store_in_cache(cache_key, search_results.clone()).await; + self.store_in_cache(cache_key, Arc::clone(&search_results)).await; return Ok(search_results); } @@ -443,7 +443,7 @@ impl SearchUseCase for SearchService { let elapsed_ms = start.elapsed().as_millis() as u64; - let search_results = SearchResultsDto::new( + let search_results = Arc::new(SearchResultsDto::new( paginated_files, paginated_folders, criteria.limit, @@ -451,10 +451,10 @@ impl SearchUseCase for SearchService { Some(total_count), elapsed_ms, criteria.sort_by.clone(), - ); + )); - // Store in cache - self.store_in_cache(cache_key, search_results.clone()).await; + // Store in cache — Arc::clone is ~1 ns (atomic increment) + self.store_in_cache(cache_key, Arc::clone(&search_results)).await; Ok(search_results) } @@ -490,8 +490,8 @@ impl SearchService { &self, _criteria: SearchCriteriaDto, _user_id: &str, - ) -> Result { - Ok(SearchResultsDto::empty()) + ) -> Result> { + Ok(Arc::new(SearchResultsDto::empty())) } async fn suggest( diff --git a/src/common/stubs.rs b/src/common/stubs.rs index 6eff7934..c00fe2b3 100644 --- a/src/common/stubs.rs +++ b/src/common/stubs.rs @@ -597,8 +597,8 @@ impl SearchUseCase for StubSearchUseCase { &self, _criteria: SearchCriteriaDto, _user_id: &str, - ) -> Result { - Ok(SearchResultsDto::empty()) + ) -> Result, DomainError> { + Ok(Arc::new(SearchResultsDto::empty())) } async fn suggest( diff --git a/src/interfaces/api/handlers/search_handler.rs b/src/interfaces/api/handlers/search_handler.rs index 7bfbe545..be17646a 100644 --- a/src/interfaces/api/handlers/search_handler.rs +++ b/src/interfaces/api/handlers/search_handler.rs @@ -67,7 +67,7 @@ impl SearchHandler { results.files.len(), results.folders.len() ); - (StatusCode::OK, Json(results)).into_response() + (StatusCode::OK, Json(&*results)).into_response() } Err(err) => { error!("Search error: {}", err); @@ -108,7 +108,7 @@ impl SearchHandler { results.files.len(), results.folders.len() ); - (StatusCode::OK, Json(results)).into_response() + (StatusCode::OK, Json(&*results)).into_response() } Err(err) => { error!("Search error: {}", err);