11// ---------------------------------------------------------------------------------------------
22// Mountain Storage Handlers (handlers/storage.rs)
3-
43// --------------------------------------------------------------------------------------------
54// Implements the backend logic for the Extension Storage API (Memento),
65
76// handling persistent key-value storage for extensions running in sidecars
87// (Cocoon).
98//
109// Responsibilities:
11- // - Handling `$getValue` and `$setValue` RPC calls proxied from Cocoon's
12- // `storage-shim.js`.
13- // - Differentiating between Global (1) and Workspace (0) storage scopes based
14- // on parameters.
15- // - Accessing the corresponding in-memory storage maps (`global_memento`,
16-
17- // `workspace_memento`) within the managed `AppState`.
18- // - Implementing basic persistence:
19- // - Loading initial state from disk (e.g., JSON files) into `AppState` on
20- // startup (handled in app_state.rs using `load_storage_from_disk`).
21- // - Triggering asynchronous saving of modified storage maps back to disk
22- // after `$setValue` using `save_storage_to_disk`.
23- // - Resolving appropriate file paths for persistence based on scope and
24- // workspace context.
25- // - Handling file I/O errors and lock contention gracefully.
10+ // - Handling `$getValue` and `$setValue` RPC calls (via effects in Track).
11+ // - Differentiating between Global and Workspace storage scopes.
12+ // - Accessing in-memory storage maps in `AppState`.
13+ // - Implementing persistence: loading (in app_state.rs) and saving to disk.
14+ // - Resolving file paths for persistence.
2615//
2716// Key Interactions:
28- // - Called by `track::dispatch_sidecar_request` for RPC methods.
29- // - Interacts with `AppState` via Mutex to access/modify Memento HashMaps and
30- // storage paths.
31- // - Uses `tokio::fs` and `tokio::io::AsyncWriteExt` for asynchronous file
32- // writing.
33- // - Uses `serde_json` for serializing/deserializing storage state.
17+ // - Called by effects created in `track.rs` or by `environment.rs`.
18+ // - Interacts with `AppState` for Memento HashMaps and storage paths.
19+ // - Uses `tokio::fs` for asynchronous file writing.
20+ // - Uses `serde_json` for serialization/deserialization.
3421// --------------------------------------------------------------------------------------------
3522
36- // Use StdMutex if used directly in AppState
3723use std:: {
3824 collections:: HashMap ,
3925 path:: { Path , PathBuf } ,
4026 sync:: { Arc , Mutex as StdMutex , MutexGuard } ,
4127} ;
4228
43- // Use the log crate for logging
44- use log;
29+ use log:: { debug, error, info, trace, warn} ;
4530use serde_json:: { Value , json} ;
46- use tauri :: { AppHandle , Manager , Runtime , State } ;
47- // Use tokio fs/io for async operations
31+ // Removed State as it's not directly used in RPC handlers
32+ use tauri :: { AppHandle , Manager , Runtime } ;
4833use tokio:: { fs, io:: AsyncWriteExt } ;
4934
50- // Import AppState
5135use crate :: app_state:: AppState ;
36+ // Use shared error utilities
37+ use crate :: handlers:: error_utils;
5238
53- // Import Vine if needed for future storage change events
54- // use crate::vine ;
39+ // Not directly returned by these RPC handlers
40+ // use Land_Common::errors::CommonError ;
5541
56- // Type aliases for clarity
42+ // Type aliases
5743// 0 = Workspace, 1 = Global
58- type StorageScope = u32 ;
44+ // Made pub for use in environment.rs
45+ pub type StorageScope = u32 ;
5946
60- // Key-value map for storage
61- type StorageMap = HashMap < String , Value > ;
47+ // Exporting for environment.rs
48+ pub type StorageMap = HashMap < String , Value > ;
6249
6350// --- Helper Functions ---
6451
65- /// Helper to create a structured error JSON string for RPC error responses.
66- fn create_error_string ( message : String , code : Option < & str > ) -> String {
67- json ! ( { "message" : message, "code" : code. unwrap_or( "EUNKNOWN" ) } ) . to_string ( )
68- }
52+ /// Helper to map Mutex lock poisoning errors for storage state.
53+ fn map_storage_lock_error_to_str < T > ( e : std:: sync:: PoisonError < MutexGuard < ' _ , T > > ) -> String {
54+ let msg = format ! ( "[Storage Handler LockErr] Failed to acquire lock on storage state: {}" , e) ;
55+
56+ // Log the specific error
57+ error ! ( "{}" , msg) ;
6958
70- /// Helper to map Mutex lock poisoning errors to a structured error string.
71- fn map_lock_error < T > ( e : std:: sync:: PoisonError < MutexGuard < ' _ , T > > ) -> String {
72- create_error_string ( format ! ( "Failed to acquire lock on storage state: {}" , e) , Some ( "ELOCKED" ) )
59+ error_utils:: rpc_error_string ( msg, Some ( "ELOCKED_STORAGE" ) )
7360}
7461
7562/// Parses the storage scope (0 for Workspace, 1 for Global) and the string key
7663/// from the JSON Value parameters received via RPC.
77- fn get_storage_scope_key ( params : & Value ) -> Result < ( StorageScope , String ) , String > {
64+ fn get_storage_scope_key ( params : & Value , operation_name : & str ) -> Result < ( StorageScope , String ) , String > {
7865 let scope = params. get ( "scope" ) . and_then ( Value :: as_u64) . ok_or_else ( || {
79- create_error_string (
80- "Missing or invalid 'scope' parameter (expected 0 or 1)" . to_string ( ) ,
81- Some ( "EBADARG" ) ,
82- )
66+ error_utils:: rpc_param_error_string ( operation_name, "scope" , "0 (Workspace) or 1 (Global)" , Some ( 0 ) )
8367 } ) ? as StorageScope ;
8468
8569 if scope != 0 && scope != 1 {
86- return Err ( create_error_string (
70+ return Err ( error_utils :: rpc_error_string (
8771 "Invalid 'scope' value (must be 0 for Workspace or 1 for Global)" . to_string ( ) ,
88- Some ( "EBADARG " ) ,
72+ Some ( "EBADARG_SCOPE " ) ,
8973 ) ) ;
9074 }
9175
9276 let key = params
9377 . get ( "key" )
9478 . and_then ( Value :: as_str)
9579 . filter ( |s| !s. is_empty ( ) )
96- . ok_or_else ( || {
97- create_error_string (
98- "Missing or invalid 'key' parameter (expected non-empty string)" . to_string ( ) ,
99- Some ( "EBADARG" ) ,
100- )
101- } ) ?
80+ . ok_or_else ( || error_utils:: rpc_param_error_string ( operation_name, "key" , "non-empty string" , Some ( 0 ) ) ) ?
10281 . to_string ( ) ;
10382
10483 Ok ( ( scope, key) )
@@ -109,81 +88,107 @@ fn get_storage_scope_key(params:&Value) -> Result<(StorageScope, String), String
10988/// Asynchronously saves the storage map (JSON) to the specified file path.
11089/// Creates the parent directory if it doesn't exist.
11190pub async fn save_storage_to_disk ( path : & Path , data : & StorageMap ) -> Result < ( ) , String > {
112- let json_string =
113- serde_json:: to_string_pretty ( data) . map_err ( |e| format ! ( "Failed to serialize storage data: {}" , e) ) ?;
91+ let json_string = serde_json:: to_string_pretty ( data) . map_err ( |e| {
92+ format ! (
93+ "[Storage Persistence] Failed to serialize storage data for {}: {}" ,
94+ path. display( ) ,
95+ e
96+ )
97+ } ) ?;
11498
11599 if let Some ( parent) = path. parent ( ) {
116- if !parent. exists ( ) {
117- log:: info!( "[Storage Persistence] Creating storage directory: {}" , parent. display( ) ) ;
118-
119- fs:: create_dir_all ( parent)
120- . await
121- . map_err ( |e| format ! ( "Failed to create storage directory {}: {}" , parent. display( ) , e) ) ?;
100+ // Use tokio::fs for async check
101+ if !tokio:: fs:: try_exists ( parent) . await . unwrap_or ( false ) {
102+ info ! ( "[Storage Persistence] Creating storage directory: {}" , parent. display( ) ) ;
103+
104+ // Use tokio::fs
105+ fs:: create_dir_all ( parent) . await . map_err ( |e| {
106+ format ! (
107+ "[Storage Persistence] Failed to create storage directory {}: {}" ,
108+ parent. display( ) ,
109+ e
110+ )
111+ } ) ?;
122112 }
123113 } else {
124- return Err ( format ! ( "Invalid storage path (has no parent): {}" , path. display( ) ) ) ;
114+ return Err ( format ! (
115+ "[Storage Persistence] Invalid storage path (has no parent): {}" ,
116+ path. display( )
117+ ) ) ;
125118 }
126119
127- log :: debug!( "[Storage Persistence] Writing storage state to {}" , path. display( ) ) ;
120+ debug ! ( "[Storage Persistence] Writing storage state to {}" , path. display( ) ) ;
128121
129- let mut file = fs:: File :: create ( path)
130- . await
131- . map_err ( |e| format ! ( "Failed to create/open storage file {}: {}" , path. display( ) , e) ) ?;
122+ // Use tokio::fs
123+ let mut file = fs:: File :: create ( path) . await . map_err ( |e| {
124+ format ! (
125+ "[Storage Persistence] Failed to create/open storage file {}: {}" ,
126+ path. display( ) ,
127+ e
128+ )
129+ } ) ?;
132130
133131 file. write_all ( json_string. as_bytes ( ) )
134132 . await
135- . map_err ( |e| format ! ( "Failed to write storage file {}: {}" , path. display( ) , e) ) ?;
133+ . map_err ( |e| format ! ( "[Storage Persistence] Failed to write storage file {}: {}" , path. display( ) , e) ) ?;
136134
137- log:: info!( "[Storage Persistence] Successfully saved state to {}" , path. display( ) ) ;
135+ // Keep: Confirmation log
136+ info ! ( "[Storage Persistence] Successfully saved state to {}" , path. display( ) ) ;
138137
139138 Ok ( ( ) )
140139}
141140
142141// --- State Access Helper ---
143142
144- /// Retrieves the appropriate storage map mutex and file path based on the
145- /// scope. Made public for potential use during AppState initialization or by
146- /// effects .
147- pub fn get_storage_map_mutex_and_path (
143+ /// Retrieves the appropriate storage map mutex and file path based on scope.
144+ /// Made public for use by storage effects in `environment.rs` or `app_state.rs`
145+ /// init .
146+ pub fn get_storage_map_and_path (
148147 app_state : & AppState ,
149148
150149 scope : StorageScope ,
151150) -> Result < ( Arc < StdMutex < StorageMap > > , Option < PathBuf > ) , String > {
151+ // Global
152152 let mutex = if scope == 1 {
153153 app_state. global_memento . clone ( )
154+ // Workspace (scope == 0)
154155 } else {
155156 app_state. workspace_memento . clone ( )
156157 } ;
157158
158159 let path_opt = if scope == 1 {
159160 Some ( app_state. global_memento_path . clone ( ) )
160161 } else {
161- app_state. workspace_memento_path . clone ( )
162+ // Workspace path itself is Arc<StdMutex<Option<PathBuf>>>
163+
164+ match app_state. workspace_memento_path . lock ( ) {
165+ // Clone the Option<PathBuf> from within the guard
166+ Ok ( guard) => guard. clone ( ) ,
167+
168+ Err ( e) => return Err ( map_storage_lock_error_to_str ( e) ) ,
169+ }
162170 } ;
163171
164172 Ok ( ( mutex, path_opt) )
165173}
166174
167- // --- RPC Request Handlers (Called by Track dispatcher) ---
175+ // --- RPC Request Handlers (Called by effects or direct RPC dispatcher) ---
168176
169177/// Handles the `storage_getValue` request from the Cocoon storage shim.
170178/// Args: `params: { scope: 0 | 1, key: string }`
171179pub async fn handle_get_value < R : Runtime > ( app : AppHandle < R > , params : Value ) -> Result < Value , String > {
172- let ( scope, key) = get_storage_scope_key ( params) ?;
180+ let ( scope, key) = get_storage_scope_key ( params, "storage_getValue" ) ?;
173181
174182 let scope_name = if scope == 1 { "Global" } else { "Workspace" } ;
175183
176184 // Reduce logging for get requests unless debugging
177- log :: trace!( "[Storage Handler] GetValue scope={}, key='{}'" , scope_name, key) ;
185+ trace ! ( "[Storage Handler] GetValue scope={}, key='{}'" , scope_name, key) ;
178186
179187 let app_state = app. state :: < AppState > ( ) ;
180188
181- let ( storage_mutex, _path_opt) = get_storage_map_mutex_and_path ( & app_state, scope) ?;
189+ let ( storage_mutex, _path_opt) = get_storage_map_and_path ( & app_state, scope) ?;
182190
183- // TODO: Implement optional load-on-demand if needed
184- let storage_guard = storage_mutex
185- . lock ( )
186- . map_err ( |e| create_handler_error_string ( format ! ( "Failed to lock storage state: {}" , e) , Some ( "ELOCKED" ) ) ) ?;
191+ let storage_guard = storage_mutex. lock ( ) . map_err ( map_storage_lock_error_to_str) ?;
187192
188193 Ok ( storage_guard. get ( & key) . cloned ( ) . unwrap_or ( Value :: Null ) )
189194}
@@ -192,78 +197,71 @@ pub async fn handle_get_value<R:Runtime>(app:AppHandle<R>, params:Value) -> Resu
192197/// Updates the in-memory map and triggers asynchronous persistence to disk.
193198/// Args: `params: { scope: 0 | 1, key: string, value: any }`
194199pub async fn handle_set_value < R : Runtime > ( app : AppHandle < R > , params : Value ) -> Result < Value , String > {
195- let ( scope, key) = get_storage_scope_key ( params) ?;
200+ let ( scope, key) = get_storage_scope_key ( params, "storage_setValue" ) ?;
196201
197- let value = params
198- . get ( "value" )
199- . cloned ( )
200- . ok_or_else ( || create_error_string ( "Missing 'value' parameter" . to_string ( ) , Some ( "EBADARG" ) ) ) ?;
202+ // value can be null to delete
203+ let value_to_set = params . get ( "value" ) . cloned ( ) . ok_or_else ( || {
204+ error_utils :: rpc_param_error_string ( "storage_setValue" , "value" , "any JSON value or null" , Some ( 0 ) )
205+ } ) ?;
201206
202207 let scope_name = if scope == 1 { "Global" } else { "Workspace" } ;
203208
204209 // Keep log for set operations
205- log:: info!( "[Storage Handler] SetValue scope={}, key='{}'" , scope_name, key) ;
206-
207- // Log truncated value: log::debug!("[Storage Handler] Value: '{}...'",
208-
209- // value.to_string().chars().take(100).collect::<String>());
210-
211- // Validate value is JSON compatible
212- if !value. is_null ( )
213- && !value. is_string ( )
214- && !value. is_number ( )
215- && !value. is_boolean ( )
216- && !value. is_array ( )
217- && !value. is_object ( )
218- {
219- return Err ( create_error_string (
220- format ! ( "Invalid non-JSON compatible value type received for key '{}'" , key) ,
221- Some ( "EBADARG" ) ,
222- ) ) ;
223- }
210+ info ! ( "[Storage Handler] SetValue scope={}, key='{}'" , scope_name, key) ;
211+
212+ trace ! (
213+ "[Storage Handler] Value for '{}': {}..." ,
214+ key,
215+ value_to_set. to_string( ) . chars( ) . take( 100 ) . collect:: <String >( )
216+ ) ;
224217
225218 let app_state = app. state :: < AppState > ( ) ;
226219
227- let ( storage_mutex, path_opt) = get_storage_map_mutex_and_path ( & app_state, scope) ?;
220+ let ( storage_mutex, path_opt) = get_storage_map_and_path ( & app_state, scope) ?;
228221
229222 // Clone data needed for saving *after* the lock is released
230223 let data_clone_for_save: Option < StorageMap > = {
231- let mut storage_guard = storage_mutex. lock ( ) . map_err ( |e| {
232- create_handler_error_string ( format ! ( "Failed to lock storage state: {}" , e) , Some ( "ELOCKED" ) )
233- } ) ?;
224+ let mut storage_guard = storage_mutex. lock ( ) . map_err ( map_storage_lock_error_to_str) ?;
234225
235- if value . is_null ( ) {
236- log :: debug!( "[Storage Handler] Deleting key '{}' in scope {}" , key, scope_name) ;
226+ if value_to_set . is_null ( ) {
227+ debug ! ( "[Storage Handler] Deleting key '{}' in scope {}" , key, scope_name) ;
237228
238229 storage_guard. remove ( & key) ;
239230 } else {
240- storage_guard. insert ( key. clone ( ) , value ) ;
231+ storage_guard. insert ( key. clone ( ) , value_to_set ) ;
241232 }
242233
243- // Clone HashMap for saving
234+ // Clone HashMap for saving only if a path is available for this scope
244235 path_opt. as_ref ( ) . map ( |_| storage_guard. clone ( ) )
245-
246- // Lock released here
247236 } ;
248237
249238 // Trigger async save task
250239 if let ( Some ( path) , Some ( data_clone) ) = ( path_opt, data_clone_for_save) {
240+ // Clone for the async task
251241 let path_owned = path. clone ( ) ;
252242
253243 tokio:: spawn ( async move {
254- log :: debug!(
244+ debug ! (
255245 "[Storage Handler] Persisting {} storage to {}" ,
256246 scope_name,
257247 path_owned. display( )
258248 ) ;
259249
260- if let Err ( e) = save_storage_to_disk ( & path_owned, & data_clone) . await {
261- log:: error!( "[Storage Handler] Error persisting storage: {}" , e) ;
250+ if let Err ( e_str) = save_storage_to_disk ( & path_owned, & data_clone) . await {
251+ error ! (
252+ "[Storage Handler] Error persisting {} storage to {}: {}" ,
253+ scope_name,
254+ path_owned. display( ) ,
255+ e_str
256+ ) ;
262257 }
263258 } ) ;
259+
260+ // If workspace scope but no path
264261 } else if scope != 1 && path_opt. is_none ( ) {
265- log:: warn!(
266- "[Storage Handler] Workspace storage path not set. Cannot persist value for key '{}'." ,
262+ warn ! (
263+ "[Storage Handler] Workspace storage path not set. Cannot persist value for key '{}'. Change will only be \
264+ in memory.",
267265 key
268266 ) ;
269267 }
0 commit comments