Skip to main content

brows3r_lib/commands/
settings_cmd.rs

1//! Tauri commands for reading and updating application settings.
2//!
3//! # Commands
4//!
5//! - `settings_get`    — return the current `Settings` snapshot.
6//! - `settings_update` — apply a JSON patch, validate, persist atomically.
7//!
8//! Both commands take `tauri::State<SettingsHandle>` so they share the same
9//! `Arc<Mutex<Settings>>` that was seeded at app start.
10//!
11//! ## Hot-reload
12//!
13//! `settings_update` also pushes runtime-affecting changes into the live
14//! services:
15//! - `proxy`               → `ClientPool::set_proxy` (rebuilds connectors).
16//! - `transfer_concurrency`→ `TransferQueue::rebuild_semaphore`.
17//!
18//! Cache TTL changes still require an app restart — the cache stores them as
19//! immutable config at open time.
20
21use serde_json::Value;
22use tauri::State;
23
24use crate::{
25    error::AppError,
26    s3::S3ClientPoolHandle,
27    settings::{validate_patch, Settings, SettingsHandle},
28    transfers::TransferQueueHandle,
29};
30
31/// Return the current settings as a serialised `Settings` value.
32///
33/// The frontend can call this on startup to hydrate its settings state.
34#[tauri::command]
35pub async fn settings_get(handle: State<'_, SettingsHandle>) -> Result<Settings, AppError> {
36    let settings = handle.inner.lock().await;
37    Ok(settings.clone())
38}
39
40/// Apply a JSON patch to the current settings, validate, and persist.
41///
42/// `patch` is a partial JSON object; only the keys present in `patch` are
43/// updated.  The merge strategy is a shallow JSON merge (RFC 7396 spirit):
44/// top-level keys in `patch` overwrite the corresponding fields in the stored
45/// settings.  Sub-structs are replaced wholesale when their key appears in
46/// `patch` (standard `serde_json::Value` merge semantics).
47///
48/// Returns the updated `Settings` on success, or `AppError::Validation` if
49/// the patch violates a constraint (e.g. `transferConcurrency = 0`).
50///
51/// Pass `force: true` to bypass shortcut-conflict checks (future use; ignored
52/// in v1 but accepted so callers compiled against this signature do not need
53/// updating).
54#[tauri::command]
55pub async fn settings_update(
56    handle: State<'_, SettingsHandle>,
57    pool: State<'_, S3ClientPoolHandle>,
58    queue: State<'_, TransferQueueHandle>,
59    patch: Value,
60    #[allow(unused_variables)] force: Option<bool>,
61) -> Result<Settings, AppError> {
62    // Validate before acquiring the lock so we fail fast without blocking.
63    validate_patch(&patch)?;
64
65    let mut settings = handle.inner.lock().await;
66
67    // Capture the pre-patch values so we only push side-effects when the
68    // relevant fields actually changed.
69    let prev_proxy = settings.proxy.clone();
70    let prev_concurrency = settings.transfer_concurrency;
71
72    // Merge the patch into the current settings via JSON round-trip.
73    // Strategy: serialize current → merge patch → deserialize.
74    let mut current_value =
75        serde_json::to_value(settings.clone()).map_err(|e| AppError::Internal {
76            trace_id: format!("settings_update_serialize: {e}"),
77        })?;
78
79    json_merge(&mut current_value, patch);
80
81    let updated: Settings =
82        serde_json::from_value(current_value).map_err(|e| AppError::Internal {
83            trace_id: format!("settings_update_deserialize: {e}"),
84        })?;
85
86    // Persist atomically before updating in-memory state so a failed write
87    // does not leave the in-memory state ahead of disk.
88    updated.save(&handle.path).await?;
89
90    *settings = updated.clone();
91    // Drop the lock before touching the live services to avoid holding it
92    // across their awaits.
93    drop(settings);
94
95    // ---------- hot-reload: proxy ----------
96    if updated.proxy != prev_proxy {
97        pool.inner.set_proxy(updated.proxy.clone().into()).await;
98    }
99
100    // ---------- hot-reload: transfer concurrency ----------
101    if updated.transfer_concurrency != prev_concurrency {
102        queue.0.rebuild_semaphore(updated.transfer_concurrency);
103    }
104
105    Ok(updated)
106}
107
108/// Recursive JSON merge (RFC 7396 spirit).
109///
110/// Keys from `patch` overwrite keys in `base`.  When both `base[key]` and
111/// `patch[key]` are objects, the merge recurses.  Otherwise `patch[key]`
112/// replaces `base[key]`.
113fn json_merge(base: &mut Value, patch: Value) {
114    match (base, patch) {
115        (Value::Object(base_map), Value::Object(patch_map)) => {
116            for (k, v) in patch_map {
117                let entry = base_map.entry(k).or_insert(Value::Null);
118                json_merge(entry, v);
119            }
120        }
121        (base, patch) => {
122            *base = patch;
123        }
124    }
125}