Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions src-tauri/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -251,9 +249,8 @@ fn password_for_endpoint(blob: Option<&str>, endpoint: &str) -> Result<Option<St

async fn keychain_password(profile_id: &str, endpoint: &str) -> Result<Option<String>, 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)
}

Expand Down
99 changes: 50 additions & 49 deletions src-tauri/src/excel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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> {
Expand All @@ -72,69 +69,73 @@ fn check_size(path: &str) -> Result<(), ExcelError> {
}

async fn timed_read<T>(
work: impl std::future::Future<Output = Result<Result<T, ExcelError>, String>>,
work: impl std::future::Future<Output = Result<Result<T, ExcelError>, tauri::Error>>,
) -> Result<T, ExcelError> {
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
// crafted/corrupt workbook aborts the app instead of returning Err. Accepted;
// check_size + the dialog-only pick keep realistic inputs benign.
#[tauri::command]
pub async fn excel_list_sheets(path: String) -> Result<Vec<String>, String> {
timed_read(blocking(move || -> Result<Vec<String>, 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<Vec<String>, 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<Rows, String> {
timed_read(blocking(move || -> Result<Rows, ExcelError> {
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<String> = 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<Rows, ExcelError> {
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<String> = 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<String>> = 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<String>> = 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())
}
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, T>(f: F) -> Result<T, String>
where
F: FnOnce() -> T + Send + 'static,
Expand Down