|
| 1 | +// --------------------------------------------------------------------------------------------- |
| 2 | +// Mountain Sky UI Response Handlers (handlers/sky_ui_responses.rs) |
| 3 | +// -------------------------------------------------------------------------------------------- |
| 4 | +// Contains Tauri command handlers that the Sky frontend invokes to send back |
| 5 | +// results from UI interactions (dialogs, quick picks, input boxes) initiated by |
| 6 | +// Mountain's UiProvider effects. This module acts as the bridge for |
| 7 | +// asynchronous UI operations where Mountain requests a UI action and Sky |
| 8 | +// provides the outcome. |
| 9 | +// |
| 10 | +// Responsibilities: |
| 11 | +// - Implementing the `sky_resolves_ui_request` Tauri command. |
| 12 | +// - Receiving the `request_id`, `data_val` (on success/cancellation), and |
| 13 | +// `error_details_val` (on UI-side error) from Sky. |
| 14 | +// - Retrieving the corresponding `oneshot::Sender` from |
| 15 | +// `AppState.pending_ui_requests` using the `request_id`. |
| 16 | +// - Constructing a `Result<Value, CommonError>` based on the data received from |
| 17 | +// Sky. |
| 18 | +// - Sending this result back to the waiting task in `environment.rs` (which |
| 19 | +// initiated the UI request) via the `oneshot::Sender`. |
| 20 | +// - Handling cases where the request might have already timed out on Mountain's |
| 21 | +// side. |
| 22 | +// |
| 23 | +// Key Interactions: |
| 24 | +// - Invoked by the Sky frontend via Tauri's `invoke` system. |
| 25 | +// - Accesses `AppState.pending_ui_requests` (thread-safely) to find and consume |
| 26 | +// the `oneshot::Sender`. |
| 27 | +// - Uses `tokio::sync::oneshot::Sender` to communicate results back to async |
| 28 | +// tasks in `environment.rs`. |
| 29 | +// - Uses `Land_Common::errors::CommonError` for error propagation. |
| 30 | +// - Uses `handlers::error_utils` for formatting error strings returned by the |
| 31 | +// Tauri command itself (if this handler encounters an internal issue). |
| 32 | +// -------------------------------------------------------------------------------------------- |
| 33 | + |
| 34 | +use Land_Common::errors::CommonError; |
| 35 | +use log::{debug, error, info, trace, warn}; |
| 36 | +use serde_json::{Value, json}; |
| 37 | +use tauri::{AppHandle, Manager, Runtime}; |
| 38 | + |
| 39 | +// For formatting errors returned by this command itself |
| 40 | +use crate::{app_state::AppState, handlers::error_utils}; |
| 41 | + |
| 42 | +/// Formats a lock error specifically for the context of this Tauri command |
| 43 | +/// failing. This error is for the `Result<(), String>` of the Tauri command |
| 44 | +/// itself, not for the `oneshot::Sender`. |
| 45 | +fn format_lock_error_for_command<T>( |
| 46 | + e:std::sync::PoisonError<std::sync::MutexGuard<'_, T>>, |
| 47 | + |
| 48 | + // e.g., "pending UI requests" |
| 49 | + context:&str, |
| 50 | +) -> String { |
| 51 | + let user_facing_message = format!( |
| 52 | + "Internal server error while processing UI response: Could not access shared '{}' state. Please try the UI \ |
| 53 | + action again or restart the application if the issue persists.", |
| 54 | + context |
| 55 | + ); |
| 56 | + |
| 57 | + let internal_log_message = format!( |
| 58 | + "[Sky UI Resp LockErr] CRITICAL: Failed to acquire lock on {} state: {}", |
| 59 | + context, e |
| 60 | + ); |
| 61 | + |
| 62 | + error!("{}", internal_log_message); |
| 63 | + |
| 64 | + error_utils::rpc_error_string(user_facing_message, Some("ELOCKED_UI_RESPONSE_HANDLER")) |
| 65 | +} |
| 66 | + |
| 67 | +/// Helper to send the result (or error) via the oneshot sender and log. |
| 68 | +/// This communicates the outcome of the UI operation back to the waiting task |
| 69 | +/// in `environment.rs`. |
| 70 | +fn send_ui_result_to_environment( |
| 71 | + request_id:&str, |
| 72 | + |
| 73 | + sender:tokio::sync::oneshot::Sender<Result<Value, CommonError>>, |
| 74 | + |
| 75 | + result_to_send:Result<Value, CommonError>, |
| 76 | +) { |
| 77 | + let outcome_log = match &result_to_send { |
| 78 | + Ok(v) if v.is_null() => "cancellation/no-data", |
| 79 | + |
| 80 | + Ok(_) => "success data", |
| 81 | + |
| 82 | + Err(e) => { |
| 83 | + // Log the CommonError that will be sent back |
| 84 | + error!( |
| 85 | + "[Sky UI Resp] Preparing to send CommonError for ReqID '{}' to environment: {:?}", |
| 86 | + request_id, e |
| 87 | + ); |
| 88 | + |
| 89 | + "error" |
| 90 | + }, |
| 91 | + }; |
| 92 | + |
| 93 | + if sender.send(result_to_send).is_err() { |
| 94 | + // This means the receiving end of the oneshot channel (in environment.rs) was |
| 95 | + // dropped. This usually happens if the UiProvider method in environment.rs |
| 96 | + // timed out before Sky could call back with this response. |
| 97 | + warn!( |
| 98 | + "[Sky UI Resp] For ReqID '{}': Failed to send {} result back to Mountain's waiting task. Receiver dropped \ |
| 99 | + (likely UiProvider method timed out or task cancelled by Mountain). Sky's response was effectively too \ |
| 100 | + late or the original request context is gone.", |
| 101 | + request_id, outcome_log |
| 102 | + ); |
| 103 | + } else { |
| 104 | + info!( |
| 105 | + "[Sky UI Resp] For ReqID '{}': Successfully relayed Sky's {} response to Mountain's waiting task in \ |
| 106 | + environment.rs.", |
| 107 | + request_id, outcome_log |
| 108 | + ); |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +/// Generic Tauri command handler for Sky to send back results of any UI |
| 113 | +/// interaction initiated by Mountain's UiProvider effects. |
| 114 | +/// |
| 115 | +/// Sky should invoke this command with: |
| 116 | +/// - `request_id`: The ID originally sent by Mountain with the UI request |
| 117 | +/// event. |
| 118 | +/// - `data_val`: `Option<Value>`. |
| 119 | +/// - For successful interactions returning data (e.g., selected file paths, |
| 120 | +/// |
| 121 | +/// input string), this should be the data serialized as a |
| 122 | +/// `serde_json::Value`. |
| 123 | +/// - For cancellations or interactions that don't return data but succeeded |
| 124 | +/// (e.g., a simple message box closed), Sky should send `None` or |
| 125 | +/// `Value::Null` for this field. `None` is preferred for clarity. |
| 126 | +/// - `error_details_val`: `Option<Value>`. |
| 127 | +/// - If an error occurred within Sky while processing/displaying the UI, or |
| 128 | +/// if the user's action in Sky resulted in an error state defined by Sky, |
| 129 | +/// |
| 130 | +/// Sky should send error details here (e.g., `json!({"message": "UI |
| 131 | +/// component failed", "code": "ESKY_ERROR"})`). |
| 132 | +/// - If the UI interaction was successful or normally cancelled by the user |
| 133 | +/// without error, this should be `None`. |
| 134 | +#[tauri::command] |
| 135 | +pub async fn sky_resolves_ui_request( |
| 136 | + // Automatically injected by Tauri |
| 137 | + app_handle:AppHandle, |
| 138 | + |
| 139 | + request_id:String, |
| 140 | + |
| 141 | + data_val:Option<Value>, |
| 142 | + |
| 143 | + error_details_val:Option<Value>, |
| 144 | +) -> Result<(), String> { |
| 145 | + // This Result<(), String> is for the Tauri command's own execution status |
| 146 | + info!( |
| 147 | + "[Sky UI Resp] Received UI response for ReqID='{}': DataIsSome={}, ErrorIsSome={}", |
| 148 | + request_id, |
| 149 | + data_val.is_some(), |
| 150 | + error_details_val.is_some() |
| 151 | + ); |
| 152 | + |
| 153 | + trace!( |
| 154 | + "[Sky UI Resp] ReqID='{}': Data='{:?}', Error='{:?}'", |
| 155 | + request_id, data_val, error_details_val |
| 156 | + ); |
| 157 | + |
| 158 | + let app_state = app_handle.state::<AppState>(); |
| 159 | + |
| 160 | + let maybe_sender = { |
| 161 | + // Scope the lock |
| 162 | + let mut pending_guard = app_state |
| 163 | + .pending_ui_requests |
| 164 | + .lock() |
| 165 | + .map_err(|e| format_lock_error_for_command(e, "pending UI requests map"))?; |
| 166 | + |
| 167 | + // Check if the request is still pending before removing. |
| 168 | + // This helps avoid warnings if Sky calls back after Mountain has already timed |
| 169 | + // out and cleaned up. |
| 170 | + if !pending_guard.contains_key(&request_id) { |
| 171 | + warn!( |
| 172 | + "[Sky UI Resp] No pending UI request found for ReqID '{}' upon checking map (already handled, timed \ |
| 173 | + out, or invalid ID). Sky's response will be ignored.", |
| 174 | + request_id |
| 175 | + ); |
| 176 | + |
| 177 | + // Command itself processed fine, but no further action needed. |
| 178 | + return Ok(()); |
| 179 | + } |
| 180 | + // Remove and get the sender |
| 181 | + pending_guard.remove(&request_id) |
| 182 | + }; |
| 183 | + |
| 184 | + if let Some(sender) = maybe_sender { |
| 185 | + let result_to_send:Result<Value, CommonError> = match (data_val, error_details_val) { |
| 186 | + // Case 1: Error reported from Sky. This takes precedence over any data. |
| 187 | + (_, Some(err_val)) => { |
| 188 | + let err_msg_str = err_val |
| 189 | + .get("message") |
| 190 | + .and_then(Value::as_str) |
| 191 | + .unwrap_or_else(|| err_val.as_str().unwrap_or("Unknown error structure reported by Sky UI")) |
| 192 | + .to_string(); |
| 193 | + |
| 194 | + // Sky might send a code |
| 195 | + let err_code_str = err_val.get("code").and_then(Value::as_str); |
| 196 | + |
| 197 | + warn!( |
| 198 | + "[Sky UI Resp] ReqID '{}' resolved with an error from Sky: msg='{}', code='{:?}'", |
| 199 | + request_id, |
| 200 | + err_msg_str, |
| 201 | + err_code_str.unwrap_or("N/A") |
| 202 | + ); |
| 203 | + |
| 204 | + // Package this as a CommonError::UiInteraction to send back to the waiting |
| 205 | + // effect |
| 206 | + Err(CommonError::UiInteraction(format!( |
| 207 | + "Error from Sky UI (ReqID: {}): {} (Code: {})", |
| 208 | + request_id, |
| 209 | + err_msg_str, |
| 210 | + // Provide a default code if Sky doesn't |
| 211 | + err_code_str.unwrap_or("SKY_UI_ERROR") |
| 212 | + ))) |
| 213 | + }, |
| 214 | + |
| 215 | + // Case 2: Success with data from Sky |
| 216 | + (Some(data), None) => { |
| 217 | + debug!("[Sky UI Resp] ReqID '{}' resolved successfully with data from Sky.", request_id); |
| 218 | + |
| 219 | + trace!("[Sky UI Resp] ReqID '{}' data: {:?}", request_id, data); |
| 220 | + |
| 221 | + Ok(data) |
| 222 | + }, |
| 223 | + |
| 224 | + // Case 3: Success with no specific data (e.g., user cancellation, simple ack), or data was explicitly null |
| 225 | + (None, None) => { |
| 226 | + debug!( |
| 227 | + "[Sky UI Resp] ReqID '{}' resolved by Sky with no data and no error (e.g., user cancellation or \ |
| 228 | + void success).", |
| 229 | + request_id |
| 230 | + ); |
| 231 | + |
| 232 | + // Represent cancellation or no data with Value::Null |
| 233 | + Ok(Value::Null) |
| 234 | + }, |
| 235 | + }; |
| 236 | + |
| 237 | + send_ui_result_to_environment(&request_id, sender, result_to_send); |
| 238 | + } else { |
| 239 | + // This case means the sender was not found after the lock was released, |
| 240 | + |
| 241 | + // which should be rare if the contains_key check passed, but good for |
| 242 | + // robustness. |
| 243 | + warn!( |
| 244 | + "[Sky UI Resp] No pending UI request sender found for ReqID '{}' after lock (unexpected state or race). \ |
| 245 | + Sky's response ignored.", |
| 246 | + request_id |
| 247 | + ); |
| 248 | + } |
| 249 | + // The Tauri command itself (sky_resolves_ui_request) completed its processing |
| 250 | + // successfully. |
| 251 | + Ok(()) |
| 252 | +} |
| 253 | + |
| 254 | +// Specific handlers like sky_resolves_open_dialog are no longer strictly |
| 255 | +// necessary if sky_resolves_ui_request is used generically and Sky sends |
| 256 | +// appropriate `data_val` structures that the corresponding UiProvider methods |
| 257 | +// in environment.rs can parse from Value. |
0 commit comments