diff --git a/src-tauri/src/events/frontend/instances.rs b/src-tauri/src/events/frontend/instances.rs index c068f20..d6a8468 100644 --- a/src-tauri/src/events/frontend/instances.rs +++ b/src-tauri/src/events/frontend/instances.rs @@ -1,7 +1,7 @@ use super::Error; use crate::shared::{Action, ActionContext, ActionInstance, ActionState, Context, config_dir}; -use crate::store::profiles::{LocksMut, acquire_locks, acquire_locks_mut, get_instance_mut, get_slot, get_slot_mut, save_profile}; +use crate::store::profiles::{LocksMut, acquire_locks, acquire_locks_mut, get_instance_mut, get_slot, get_slot_mut, save_profile_now}; use tauri::{AppHandle, Emitter, Manager, command}; use tokio::fs::remove_dir_all; @@ -44,7 +44,7 @@ pub async fn create_instance(app: AppHandle, mut action: Action, context: Contex let _ = update_state(&app, parent.context.clone(), &mut locks).await; } - save_profile(&context.device, &mut locks).await?; + save_profile_now(&context.device, &mut locks).await?; drop(locks); let _ = crate::events::outbound::will_appear::will_appear(&instance).await; @@ -68,7 +68,7 @@ pub async fn create_instance(app: AppHandle, mut action: Action, context: Contex *slot = Some(instance.clone()); let slot = slot.clone(); - save_profile(&context.device, &mut locks).await?; + save_profile_now(&context.device, &mut locks).await?; let _ = crate::events::outbound::will_appear::will_appear(&instance).await; Ok(slot) @@ -146,7 +146,7 @@ pub async fn move_instance(source: Context, destination: Context, retain: bool) let _ = crate::events::outbound::will_appear::will_appear(&new).await; - save_profile(&destination.device, &mut locks).await?; + save_profile_now(&destination.device, &mut locks).await?; Ok(Some(new)) } @@ -190,7 +190,7 @@ pub async fn remove_instance(context: ActionContext) -> Result<(), Error> { } } - save_profile(&context.device, &mut locks).await?; + save_profile_now(&context.device, &mut locks).await?; Ok(()) } @@ -219,7 +219,7 @@ pub async fn set_state(context: ActionContext, index: u16, state: ActionState) - let reference = get_instance_mut(&context, &mut locks).await?.unwrap(); reference.states[index as usize] = state; let clone = reference.clone(); - save_profile(&context.device, &mut locks).await?; + save_profile_now(&context.device, &mut locks).await?; crate::events::outbound::states::title_parameters_did_change(&clone, index).await?; Ok(()) } diff --git a/src-tauri/src/events/frontend/profiles.rs b/src-tauri/src/events/frontend/profiles.rs index 5fc677d..7290cf0 100644 --- a/src-tauri/src/events/frontend/profiles.rs +++ b/src-tauri/src/events/frontend/profiles.rs @@ -1,7 +1,7 @@ use super::Error; use crate::shared::DEVICES; -use crate::store::profiles::{PROFILE_SAVE_DEBOUNCE, PROFILE_STORES, acquire_locks_mut, get_device_profiles, save_profile}; +use crate::store::profiles::{PROFILE_STORES, acquire_locks_mut, get_device_profiles, save_profile_now}; use tauri::{AppHandle, Emitter, Manager, command}; @@ -32,20 +32,8 @@ pub async fn set_selected_profile(device: String, id: String) -> Result<(), Erro } // If a profile save is pending for this device, save it immediately to prevent losing profile data - let entries = PROFILE_SAVE_DEBOUNCE - .iter() - .filter(|entry| entry.key().device == device) - .map(|entry| entry.key().clone()) - .collect::>(); - if !entries.is_empty() { - for context in &entries { - if let Some((_, handle)) = PROFILE_SAVE_DEBOUNCE.remove(context) { - handle.abort(); - } - } - if let Err(error) = save_profile(&device, &mut locks).await { - log::error!("Failed to save profile for device {device}: {error}"); - } + if let Err(error) = save_profile_now(&device, &mut locks).await { + log::error!("Failed to save profile for device {device}: {error}"); } let selected_profile = locks.device_stores.get_selected_profile(&device)?; diff --git a/src-tauri/src/events/inbound/devices.rs b/src-tauri/src/events/inbound/devices.rs index 1de3410..87242d3 100644 --- a/src-tauri/src/events/inbound/devices.rs +++ b/src-tauri/src/events/inbound/devices.rs @@ -71,6 +71,11 @@ pub async fn deregister_device(uuid: &str, event: PayloadEvent) -> Resul let _ = crate::events::outbound::will_appear::will_disappear(instance, false).await; } + // Flush any pending profile writes before removing the device. + if let Err(error) = crate::store::profiles::save_profile_now(&event.payload, &mut locks).await { + log::error!("Failed to flush profile for device {}: {error}", event.payload); + } + if let Ok(profiles) = get_device_profiles(&event.payload) { for profile in profiles { locks.profile_stores.remove_profile(&event.payload, &profile); diff --git a/src-tauri/src/events/inbound/settings.rs b/src-tauri/src/events/inbound/settings.rs index 58e0e4a..712743c 100644 --- a/src-tauri/src/events/inbound/settings.rs +++ b/src-tauri/src/events/inbound/settings.rs @@ -1,6 +1,6 @@ use crate::events::outbound::settings as outbound; use crate::shared::ActionContext; -use crate::store::profiles::{acquire_locks, acquire_locks_mut, get_instance, get_instance_mut, save_profile}; +use crate::store::profiles::{acquire_locks, acquire_locks_mut, get_instance, get_instance_mut, mark_profile_stale}; use std::io::Write; use std::str::FromStr; @@ -11,7 +11,7 @@ pub async fn set_settings(event: super::ContextAndPayloadEvent) -> Result } update_state(crate::APP_HANDLE.get().unwrap(), instance.context.clone(), &mut locks).await?; } - save_profile(&event.context.device, &mut locks).await?; + mark_profile_stale(&event.context.device, &mut locks).await?; Ok(()) } @@ -97,14 +97,7 @@ pub async fn set_image(mut event: ContextAndPayloadEvent) -> Re update_state(crate::APP_HANDLE.get().unwrap(), instance.context.clone(), &mut locks).await?; } - if let Some(image) = &event.payload.image - && image.trim().starts_with("data:") - { - debounce_profile_save(event.context); - } else { - save_profile(&event.context.device, &mut locks).await?; - } - + mark_profile_stale(&event.context.device, &mut locks).await?; Ok(()) } @@ -148,7 +141,7 @@ pub async fn set_state(event: ContextAndPayloadEvent) -> Result instance.current_state = event.payload.state; update_state(crate::APP_HANDLE.get().unwrap(), instance.context.clone(), &mut locks).await?; } - save_profile(&event.context.device, &mut locks).await?; + mark_profile_stale(&event.context.device, &mut locks).await?; Ok(()) } diff --git a/src-tauri/src/events/outbound/keypad.rs b/src-tauri/src/events/outbound/keypad.rs index 15dba2d..f9c8680 100644 --- a/src-tauri/src/events/outbound/keypad.rs +++ b/src-tauri/src/events/outbound/keypad.rs @@ -2,7 +2,7 @@ use super::{GenericInstancePayload, send_to_plugin}; use crate::events::frontend::instances::{key_moved, update_state}; use crate::shared::{ActionContext, Context}; -use crate::store::profiles::{acquire_locks_mut, get_slot_mut, save_profile}; +use crate::store::profiles::{acquire_locks_mut, get_slot_mut, mark_profile_stale}; use std::sync::LazyLock; use std::time::Duration; @@ -75,7 +75,7 @@ pub async fn key_down(device: &str, key: u8) -> Result<(), anyhow::Error> { let _ = update_state(crate::APP_HANDLE.get().unwrap(), child, &mut locks).await; } - save_profile(device, &mut locks).await?; + mark_profile_stale(device, &mut locks).await?; } else if instance.action.uuid == "opendeck.toggleaction" { let children = instance.children.as_ref().unwrap(); if children.is_empty() { @@ -168,7 +168,7 @@ pub async fn key_up(device: &str, key: u8) -> Result<(), anyhow::Error> { }; let _ = update_state(crate::APP_HANDLE.get().unwrap(), instance.context.clone(), &mut locks).await; - save_profile(device, &mut locks).await?; + mark_profile_stale(device, &mut locks).await?; Ok(()) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e1bd314..f3ad189 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -20,6 +20,7 @@ use events::frontend; use shared::PRODUCT_NAME; use std::sync::OnceLock; +use std::time::Duration; use tauri::{ AppHandle, Builder, Manager, WindowEvent, @@ -29,6 +30,7 @@ use tauri::{ use tauri_plugin_log::{Target, TargetKind}; static APP_HANDLE: OnceLock = OnceLock::new(); +const SAVE_PROBE: Duration = Duration::from_secs(30); fn show_window(app: &AppHandle) -> Result<(), tauri::Error> { #[cfg(target_os = "macos")] @@ -196,6 +198,16 @@ If you have already donated, thank you so much for your support!"#, tokio::time::sleep(std::time::Duration::from_secs(10)).await; } }); + + tokio::spawn(async { + loop { + tokio::time::sleep(SAVE_PROBE).await; + if let Err(error) = store::profiles::flush_stale_profiles().await { + log::error!("Failed to flush stale profiles: {error}"); + } + } + }); + plugins::initialise_plugins(); application_watcher::init_application_watcher(); device_sleep::init_device_sleep(); @@ -380,6 +392,13 @@ If you have already donated, thank you so much for your support!"#, if let tauri::RunEvent::Exit = event { #[cfg(windows)] futures::executor::block_on(plugins::deactivate_plugins()); + + let flush_future = store::profiles::flush_stale_profiles(); + match tauri::async_runtime::block_on(flush_future) { + Ok(_) => log::info!("Successfully flushed all stale profiles on exit"), + Err(error) => log::error!("Failed to flush stale profiles on exit: {error}"), + } + tokio::spawn(elgato::reset_devices()); use tauri_plugin_aptabase::EventTracker; app.flush_events_blocking(); diff --git a/src-tauri/src/shared.rs b/src-tauri/src/shared.rs index f802d8a..0f79b9c 100644 --- a/src-tauri/src/shared.rs +++ b/src-tauri/src/shared.rs @@ -439,6 +439,9 @@ pub struct Profile { pub sliders: Vec>, #[serde(default)] pub infobars: Vec>, + + #[serde(skip)] + pub stale: bool, } /// A map of category names to a list of actions in that category. diff --git a/src-tauri/src/store/profiles.rs b/src-tauri/src/store/profiles.rs index 9845f0b..dc3c404 100644 --- a/src-tauri/src/store/profiles.rs +++ b/src-tauri/src/store/profiles.rs @@ -8,10 +8,8 @@ use std::path::PathBuf; use std::sync::LazyLock; use anyhow::{Context, anyhow}; -use dashmap::DashMap; use serde::{Deserialize, Serialize}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use tokio::task::JoinHandle; pub struct ProfileStores { stores: HashMap>, @@ -40,6 +38,8 @@ impl ProfileStores { keys: Vec::new(), sliders: Vec::new(), infobars: Vec::new(), + + stale: false, }; let mut store = Store::new(&canonical_id, &config_dir().join("profiles"), default).context(format!("Failed to create store for profile {}", canonical_id))?; @@ -369,27 +369,34 @@ pub async fn get_instance_mut<'a>(context: &crate::shared::ActionContext, locks: Ok(None) } -pub async fn save_profile(device: &str, locks: &mut LocksMut<'_>) -> Result<(), anyhow::Error> { - let selected_profile = locks.device_stores.get_selected_profile(device)?; - let device = DEVICES.get(device).ok_or_else(|| anyhow!("device not found"))?; - let store = locks.profile_stores.get_profile_store(&device, &selected_profile)?; - store.save() +pub async fn mark_profile_stale(device_id: &str, locks: &mut LocksMut<'_>) -> Result<(), anyhow::Error> { + let selected_profile = locks.device_stores.get_selected_profile(device_id)?; + let device = DEVICES.get(device_id).ok_or_else(|| anyhow!("device not found"))?; + let store = locks.profile_stores.get_profile_store_mut(&device, &selected_profile).await?; + store.value.stale = true; + Ok(()) } -pub static PROFILE_SAVE_DEBOUNCE: LazyLock>> = LazyLock::new(DashMap::new); -pub fn debounce_profile_save(context: crate::shared::ActionContext) { - if let Some((_, handle)) = PROFILE_SAVE_DEBOUNCE.remove(&context) { - handle.abort(); +pub async fn save_profile_now(device_id: &str, locks: &mut LocksMut<'_>) -> Result<(), anyhow::Error> { + let selected_profile = locks.device_stores.get_selected_profile(device_id)?; + let device = DEVICES.get(device_id).ok_or_else(|| anyhow!("device not found"))?; + let store = locks.profile_stores.get_profile_store_mut(&device, &selected_profile).await?; + + if store.value.stale { + store.save()?; + store.value.stale = false; } - PROFILE_SAVE_DEBOUNCE.insert( - context.clone(), - tokio::spawn(async move { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - let mut locks = acquire_locks_mut().await; - if let Err(error) = save_profile(&context.device, &mut locks).await { - log::error!("Failed to save profile for device {}: {error}", context.device); - } - PROFILE_SAVE_DEBOUNCE.remove(&context); - }), - ); + + Ok(()) +} + +pub async fn flush_stale_profiles() -> Result<(), anyhow::Error> { + let mut locks = acquire_locks_mut().await; + for store in locks.profile_stores.stores.values_mut() { + if store.value.stale { + store.save()?; + store.value.stale = false; + } + } + Ok(()) } diff --git a/src-tauri/src/store/simplified_profile.rs b/src-tauri/src/store/simplified_profile.rs index 5421b31..d1926b7 100644 --- a/src-tauri/src/store/simplified_profile.rs +++ b/src-tauri/src/store/simplified_profile.rs @@ -216,6 +216,8 @@ impl DiskProfile { keys: self.keys.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(), sliders: self.sliders.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(), infobars: self.infobars.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(), + + stale: false, } } }