Skip to content

Commit a6f2c37

Browse files
refactor(handlers/storage): improve error handling and storage persistence logic
- Centralized error handling using shared `error_utils` for consistent RPC error responses - Implemented proper Mutex lock error mapping with enriched logging for storage state access - Modified workspace storage path handling to use locked access through AppState's Mutex - Added detailed tracing logs for storage operations and improved async persistence flow - Made StorageScope and StorageMap types public for integration with environment effect system - Simplified parameter validation using new `rpc_param_error_string` helper function - Added value truncation logging for debug traces in setValue operations - Fixed workspace storage persistence path resolution through proper Mutex access These changes strengthen the reliability of extension storage operations critical for VS Code extension compatibility in Cocoon. The improved error handling and locking mechanisms prevent state corruption scenarios, while enhanced logging aids in debugging storage-related issues during extension execution.
1 parent 2b3246b commit a6f2c37

2 files changed

Lines changed: 121 additions & 121 deletions

File tree

Source/Library.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ pub mod handlers {
3636
pub mod registry;
3737

3838
pub mod secrets;
39+
40+
pub mod storage;
3941
}

Source/handlers/storage.rs

Lines changed: 119 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,83 @@
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
3723
use 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};
4530
use 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};
4833
use tokio::{fs, io::AsyncWriteExt};
4934

50-
// Import AppState
5135
use 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.
11190
pub 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 }`
171179
pub 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 }`
194199
pub 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

Comments
 (0)