diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 1e849c8b..ed666585 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -46,10 +46,8 @@ enum DbError { PasswordMalformed, #[error("sqlite has no password")] SqliteNoPassword, - /// The transport::blocking worker thread panicked (stringified JoinError). - /// The one explicit stringly bridge; converted only at that call site. - #[error("{0}")] - Join(String), + #[error(transparent)] + Join(#[from] tauri::Error), } /// Single source of the over-budget check, so the streaming query and its test @@ -251,9 +249,8 @@ fn password_for_endpoint(blob: Option<&str>, endpoint: &str) -> Result Result, DbError> { let name = password_cred(profile_id); - let blob = blocking(move || credentials::read_password(&name)) - .await - .map_err(DbError::Join)??; + let blob = + tauri::async_runtime::spawn_blocking(move || credentials::read_password(&name)).await??; password_for_endpoint(blob.as_deref(), endpoint) } diff --git a/src-tauri/src/excel.rs b/src-tauri/src/excel.rs index 7f38d091..8f1f389e 100644 --- a/src-tauri/src/excel.rs +++ b/src-tauri/src/excel.rs @@ -8,7 +8,6 @@ use std::time::Duration; use calamine::{open_workbook_auto, Data, Reader}; use crate::dataset::{Rows, ROW_CAP}; -use crate::transport::blocking; /// Same budget as the db queries. Cannot cancel the blocking parse (calamine /// materializes the whole sheet), but bounds what the user waits on. @@ -44,10 +43,8 @@ enum ExcelError { TooLargeUncompressed, #[error("empty sheet: {0}")] EmptySheet(String), - /// The transport::blocking worker thread panicked (stringified JoinError). - /// The one explicit stringly bridge; converted only in timed_read. - #[error("{0}")] - Join(String), + #[error(transparent)] + Join(#[from] tauri::Error), } fn check_size(path: &str) -> Result<(), ExcelError> { @@ -72,12 +69,12 @@ fn check_size(path: &str) -> Result<(), ExcelError> { } async fn timed_read( - work: impl std::future::Future, String>>, + work: impl std::future::Future, tauri::Error>>, ) -> Result { let parsed = tokio::time::timeout(READ_TIMEOUT, work) .await .map_err(|_| ExcelError::Timeout)?; - parsed.map_err(ExcelError::Join)? + parsed? } // calamine parses under the release panic="abort" profile, so a panic on a @@ -85,56 +82,60 @@ async fn timed_read( // check_size + the dialog-only pick keep realistic inputs benign. #[tauri::command] pub async fn excel_list_sheets(path: String) -> Result, String> { - timed_read(blocking(move || -> Result, ExcelError> { - check_size(&path)?; - let workbook = open_workbook_auto(&path)?; - Ok(workbook.sheet_names().to_vec()) - })) + timed_read(tauri::async_runtime::spawn_blocking( + move || -> Result, ExcelError> { + check_size(&path)?; + let workbook = open_workbook_auto(&path)?; + Ok(workbook.sheet_names().to_vec()) + }, + )) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn excel_fetch(path: String, sheet: String) -> Result { - timed_read(blocking(move || -> Result { - check_size(&path)?; - let mut workbook = open_workbook_auto(&path)?; - let range = workbook.worksheet_range(&sheet)?; - // The range starts at the first used cell; offset so a synthesized name - // matches the sheet's real column number. - let col_offset = range.start().map_or(0, |(_, c)| c as usize); - let mut rows_iter = range.rows(); - let Some(header_row) = rows_iter.next() else { - return Err(ExcelError::EmptySheet(sheet.clone())); - }; - let headers: Vec = header_row - .iter() - .enumerate() - // Blank header cells still need a stable, mappable name. - .map(|(i, c)| { - let name = cell_text(c); - if name.is_empty() { - format!("Column {}", col_offset + i + 1) - } else { - name + timed_read(tauri::async_runtime::spawn_blocking( + move || -> Result { + check_size(&path)?; + let mut workbook = open_workbook_auto(&path)?; + let range = workbook.worksheet_range(&sheet)?; + // The range starts at the first used cell; offset so a synthesized name + // matches the sheet's real column number. + let col_offset = range.start().map_or(0, |(_, c)| c as usize); + let mut rows_iter = range.rows(); + let Some(header_row) = rows_iter.next() else { + return Err(ExcelError::EmptySheet(sheet)); + }; + let headers: Vec = header_row + .iter() + .enumerate() + // Blank header cells still need a stable, mappable name. + .map(|(i, c)| { + let name = cell_text(c); + if name.is_empty() { + format!("Column {}", col_offset + i + 1) + } else { + name + } + }) + .collect(); + let mut truncated = false; + let mut rows: Vec> = Vec::new(); + for row in rows_iter { + if rows.len() >= ROW_CAP { + truncated = true; + break; } - }) - .collect(); - let mut truncated = false; - let mut rows: Vec> = Vec::new(); - for row in rows_iter { - if rows.len() >= ROW_CAP { - truncated = true; - break; + rows.push(row.iter().map(cell_text).collect()); } - rows.push(row.iter().map(cell_text).collect()); - } - Ok(Rows { - headers, - rows, - truncated, - }) - })) + Ok(Rows { + headers, + rows, + truncated, + }) + }, + )) .await .map_err(|e| e.to_string()) } diff --git a/src-tauri/src/transport.rs b/src-tauri/src/transport.rs index 1f74a865..b937dde6 100644 --- a/src-tauri/src/transport.rs +++ b/src-tauri/src/transport.rs @@ -34,8 +34,9 @@ pub(crate) fn check_payload(len: usize) -> Result<(), String> { Ok(()) } -/// Run a blocking closure on the runtime's blocking pool, flattening the -/// `JoinError` into a String so every blocking command drops that boilerplate. +/// Run a blocking closure on the pool, flattening the `JoinError` to a String +/// for the String-returning command edges. Typed callers (db/excel) use +/// `spawn_blocking` directly so `#[from] tauri::Error` folds the panic in. pub(crate) async fn blocking(f: F) -> Result where F: FnOnce() -> T + Send + 'static,