Merge branch 'main' into codex/close-large-upload-responses

This commit is contained in:
Gregory Schier
2026-07-05 10:44:10 -07:00
committed by GitHub
48 changed files with 1527 additions and 319 deletions
+77 -14
View File
@@ -191,12 +191,16 @@ fn build_url(r: &HttpRequest) -> String {
fn append_graphql_query_params(url: &str, body: &BTreeMap<String, serde_json::Value>) -> String {
let query = get_str_map(body, "query").to_string();
let variables = strip_json_comments(&get_str_map(body, "variables"));
let operation_name = get_str_map(body, "operationName").to_string();
let mut params = vec![("query".to_string(), query)];
if !variables.trim().is_empty() {
params.push(("variables".to_string(), variables));
}
if !operation_name.trim().is_empty() {
params.push(("operationName".to_string(), operation_name));
}
// Strip existing query/variables params to avoid duplicates
let url = strip_query_params(url, &["query", "variables"]);
let url = strip_query_params(url, &["query", "variables", "operationName"]);
append_query_params(&url, params)
}
@@ -329,23 +333,30 @@ fn build_graphql_body(
) -> Option<SendableBodyWithMeta> {
let query = get_str_map(body, "query");
let variables = strip_json_comments(&get_str_map(body, "variables"));
let operation_name = get_str_map(body, "operationName");
if method.to_lowercase() == "get" {
// GraphQL GET requests use query parameters, not a body
return None;
}
let body = if variables.trim().is_empty() {
format!(r#"{{"query":{}}}"#, serde_json::to_string(&query).unwrap_or_default())
} else {
format!(
r#"{{"query":{},"variables":{}}}"#,
serde_json::to_string(&query).unwrap_or_default(),
variables
)
};
let mut body = serde_json::Map::new();
body.insert("query".to_string(), serde_json::Value::String(query.to_string()));
if !variables.trim().is_empty() {
body.insert(
"variables".to_string(),
serde_json::from_str(&variables)
.unwrap_or_else(|_| serde_json::Value::String(variables)),
);
}
if !operation_name.trim().is_empty() {
body.insert(
"operationName".to_string(),
serde_json::Value::String(operation_name.to_string()),
);
}
Some(SendableBodyWithMeta::Bytes(Bytes::from(body)))
Some(SendableBodyWithMeta::Bytes(Bytes::from(serde_json::to_string(&body).unwrap_or_default())))
}
async fn build_multipart_body(
@@ -522,6 +533,33 @@ mod tests {
assert_eq!(result, "https://example.com/api?foo=bar&baz=qux");
}
#[test]
fn test_build_url_replaces_graphql_operation_name_from_body() {
let mut body = BTreeMap::new();
body.insert("query".to_string(), json!("query Foo { foo } query Bar { bar }"));
body.insert("operationName".to_string(), json!("Bar"));
let r = HttpRequest {
method: "GET".to_string(),
body_type: Some("graphql".to_string()),
body,
url: "https://example.com/graphql".to_string(),
url_parameters: vec![HttpUrlParameter {
enabled: true,
name: "operationName".to_string(),
value: "Foo".to_string(),
id: None,
}],
..Default::default()
};
let result = build_url(&r);
assert_eq!(
result,
"https://example.com/graphql?query=query%20Foo%20%7B%20foo%20%7D%20query%20Bar%20%7B%20bar%20%7D&operationName=Bar",
);
}
#[test]
fn test_build_url_with_disabled_params() {
let r = HttpRequest {
@@ -880,9 +918,34 @@ mod tests {
let result = build_graphql_body("POST", &body);
match result {
Some(SendableBodyWithMeta::Bytes(bytes)) => {
let expected =
r#"{"query":"{ user(id: $id) { name } }","variables":{"id": "123"}}"#;
assert_eq!(bytes, Bytes::from(expected));
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap(),
json!({
"query": "{ user(id: $id) { name } }",
"variables": { "id": "123" },
}),
);
}
_ => panic!("Expected Some(SendableBody::Bytes)"),
}
}
#[tokio::test]
async fn test_graphql_body_with_operation_name() {
let mut body = BTreeMap::new();
body.insert("query".to_string(), json!("query Search { viewer { id } }"));
body.insert("operationName".to_string(), json!("Search"));
let result = build_graphql_body("POST", &body);
match result {
Some(SendableBodyWithMeta::Bytes(bytes)) => {
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap(),
json!({
"query": "query Search { viewer { id } }",
"operationName": "Search",
}),
);
}
_ => panic!("Expected Some(SendableBody::Bytes)"),
}
+1
View File
@@ -402,6 +402,7 @@ export type Settings = {
themeLight: string;
updateChannel: string;
hideLicenseBadge: boolean;
promptFeedback: boolean;
autoupdate: boolean;
autoDownloadUpdates: boolean;
checkNotifications: boolean;
@@ -0,0 +1,3 @@
-- Add a setting to enable in-app feature feedback prompts
ALTER TABLE settings
ADD COLUMN prompt_feedback BOOLEAN DEFAULT TRUE NOT NULL;
+6 -9
View File
@@ -5,7 +5,6 @@ use log::{debug, info};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, params};
use std::sync::{Arc, Mutex};
static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations");
@@ -25,23 +24,21 @@ impl BodyChunk {
}
/// Manages the blob database connection pool.
// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the
// blocking `get()` serializes every blob access behind the slowest waiter, freezing the
// whole app whenever the pool is exhausted.
#[derive(Debug, Clone)]
pub struct BlobManager {
pool: Arc<Mutex<Pool<SqliteConnectionManager>>>,
pool: Pool<SqliteConnectionManager>,
}
impl BlobManager {
pub fn new(pool: Pool<SqliteConnectionManager>) -> Self {
Self { pool: Arc::new(Mutex::new(pool)) }
Self { pool }
}
pub fn connect(&self) -> BlobContext {
let conn = self
.pool
.lock()
.expect("Failed to gain lock on blob DB")
.get()
.expect("Failed to get blob DB connection from pool");
let conn = self.pool.get().expect("Failed to get blob DB connection from pool");
BlobContext { conn }
}
}
+8 -3
View File
@@ -54,11 +54,15 @@ pub fn init_standalone(
create_dir_all(parent)?;
}
// Main database pool
// Main database pool. Sized for concurrent in-flight queries, not concurrent app
// features — connections are held per-statement, so even heavy fan-out (e.g. many
// gRPC streams) only needs a handful at once. Keep max_size modest: WAL connections
// hold ~3 file descriptors each, and macOS GUI apps get a 256 fd soft limit.
info!("Initializing app database {db_path:?}");
let manager = sqlite_file_manager(db_path);
let pool = Pool::builder()
.max_size(100)
.max_size(20)
.min_idle(Some(2))
.connection_timeout(Duration::from_secs(10))
.build(manager)
.map_err(|e| Error::Database(e.to_string()))?;
@@ -70,7 +74,8 @@ pub fn init_standalone(
// Blob database pool
let blob_manager = sqlite_file_manager(blob_path);
let blob_pool = Pool::builder()
.max_size(50)
.max_size(10)
.min_idle(Some(1))
.connection_timeout(Duration::from_secs(10))
.build(blob_manager)
.map_err(|e| Error::Database(e.to_string()))?;
+4
View File
@@ -246,6 +246,7 @@ pub struct Settings {
pub theme_light: String,
pub update_channel: String,
pub hide_license_badge: bool,
pub prompt_feedback: bool,
pub autoupdate: bool,
pub auto_download_updates: bool,
pub check_notifications: bool,
@@ -303,6 +304,7 @@ impl UpsertModelInfo for Settings {
(ThemeLight, self.theme_light.as_str().into()),
(UpdateChannel, self.update_channel.into()),
(HideLicenseBadge, self.hide_license_badge.into()),
(PromptFeedback, self.prompt_feedback.into()),
(Autoupdate, self.autoupdate.into()),
(AutoDownloadUpdates, self.auto_download_updates.into()),
(ColoredMethods, self.colored_methods.into()),
@@ -332,6 +334,7 @@ impl UpsertModelInfo for Settings {
SettingsIden::ThemeLight,
SettingsIden::UpdateChannel,
SettingsIden::HideLicenseBadge,
SettingsIden::PromptFeedback,
SettingsIden::Autoupdate,
SettingsIden::AutoDownloadUpdates,
SettingsIden::ColoredMethods,
@@ -372,6 +375,7 @@ impl UpsertModelInfo for Settings {
autoupdate: row.get("autoupdate")?,
auto_download_updates: row.get("auto_download_updates")?,
hide_license_badge: row.get("hide_license_badge")?,
prompt_feedback: row.get("prompt_feedback")?,
colored_methods: row.get("colored_methods")?,
check_notifications: row.get("check_notifications")?,
hotkeys: serde_json::from_str(&hotkeys).unwrap_or_default(),
@@ -38,6 +38,7 @@ impl<'a> ClientDb<'a> {
autoupdate: true,
colored_methods: false,
hide_license_badge: false,
prompt_feedback: true,
auto_download_updates: true,
check_notifications: true,
hotkeys: HashMap::new(),
+9 -21
View File
@@ -4,27 +4,25 @@ use crate::util::ModelPayload;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::TransactionBehavior;
use std::sync::{Arc, Mutex, mpsc};
use std::sync::mpsc;
use yaak_database::{ConnectionOrTx, DbContext};
// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the
// blocking `get()` serializes every DB access behind the slowest waiter, freezing the
// whole app whenever the pool is exhausted.
#[derive(Debug, Clone)]
pub struct QueryManager {
pool: Arc<Mutex<Pool<SqliteConnectionManager>>>,
pool: Pool<SqliteConnectionManager>,
events_tx: mpsc::Sender<ModelPayload>,
}
impl QueryManager {
pub fn new(pool: Pool<SqliteConnectionManager>, events_tx: mpsc::Sender<ModelPayload>) -> Self {
QueryManager { pool: Arc::new(Mutex::new(pool)), events_tx }
QueryManager { pool, events_tx }
}
pub fn connect(&self) -> ClientDb<'_> {
let conn = self
.pool
.lock()
.expect("Failed to gain lock on DB")
.get()
.expect("Failed to get a new DB connection from the pool");
let conn = self.pool.get().expect("Failed to get a new DB connection from the pool");
let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
ClientDb::new(ctx, self.events_tx.clone())
}
@@ -33,12 +31,7 @@ impl QueryManager {
where
F: FnOnce(&ClientDb) -> T,
{
let conn = self
.pool
.lock()
.expect("Failed to gain lock on DB for transaction")
.get()
.expect("Failed to get new DB connection from the pool");
let conn = self.pool.get().expect("Failed to get new DB connection from the pool");
let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
let db = ClientDb::new(ctx, self.events_tx.clone());
@@ -53,12 +46,7 @@ impl QueryManager {
where
E: From<crate::error::Error>,
{
let mut conn = self
.pool
.lock()
.expect("Failed to gain lock on DB for transaction")
.get()
.expect("Failed to get new DB connection from the pool");
let mut conn = self.pool.get().expect("Failed to get new DB connection from the pool");
let tx = conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.expect("Failed to start DB transaction");