diff --git a/src/filesystem.rs b/src/filesystem.rs index 82b9b5b..2b3f815 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -1,10 +1,11 @@ use std::{ - collections::HashMap, + any::Any, + collections::{HashMap, hash_map::Entry}, fs, ops::Deref, path::PathBuf, sync::{ - Arc, Mutex, + Arc, Mutex, RwLock, atomic::{AtomicU64, Ordering}, }, }; @@ -16,42 +17,56 @@ use serde::{Deserialize, Serialize}; #[derive(Clone)] pub struct Persistence { config_dir: Option, - versions: Arc>>>, + states: Arc>>>, } impl Persistence { pub fn new() -> Self { Self { config_dir: init_config_dir(), - versions: Arc::new(Mutex::new(HashMap::new())), + states: Arc::new(Mutex::new(HashMap::new())), } } pub fn config(&self, file: &str) -> Config { - let latest_version = self - .versions - .lock() - .unwrap() - .entry(file.to_string()) - .or_default() - .clone(); - let path = self - .config_dir - .as_ref() - .map(|p| p.join(file).with_extension("json")); - let value = if let Some(p) = path.as_ref() - && let Ok(bytes) = fs::read(p) - && let Ok(v) = serde_json::from_slice(&bytes) - { - v - } else { - T::default() + let state = { + let mut lock = self.states.lock().unwrap(); + match lock.entry(file.to_string()) { + Entry::Vacant(e) => { + let path = self + .config_dir + .as_ref() + .map(|p| p.join(file).with_extension("json")); + let value = if let Some(p) = path.as_ref() + && let Ok(bytes) = fs::read(p) + && let Ok(v) = serde_json::from_slice(&bytes) + { + v + } else { + T::default() + }; + + let state = Arc::new(ConfigState { + current: RwLock::new(value), + version: AtomicU64::new(0), + path, + }); + e.insert(state.clone()); + state + } + Entry::Occupied(e) => e + .get() + .clone() + .downcast() + .expect("same config file accessed with two types"), + } }; + let value = state.current.read().unwrap().clone(); + let version = state.version.load(Ordering::Relaxed); Config { value, - version: latest_version.load(Ordering::Relaxed), - latest_version, - path, + version, + state, } } @@ -90,16 +105,27 @@ fn project_dirs() -> Option { ProjectDirs::from("com", "virtual-boy", "Lemur") } -pub trait ConfigCompatible: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a> {} -impl Deserialize<'a>> ConfigCompatible for T {} +pub trait ConfigCompatible: + Send + Sync + 'static + Clone + Default + Eq + Serialize + for<'a> Deserialize<'a> +{ +} +impl Deserialize<'a>> + ConfigCompatible for T +{ +} -// A wrapper for a T (probably) stored on the filesystem. -// Cheap to dereference, easy to sync changes to all copies. +struct ConfigState { + current: RwLock, + version: AtomicU64, + path: Option, +} + +// A wrapper for a config struct, loaded from disk and referenced across the codebase. +// Cheap to read, and lets you sync changes to all copies. pub struct Config { value: T, version: u64, - latest_version: Arc, - path: Option, + state: Arc>, } impl Deref for Config { @@ -112,32 +138,30 @@ impl Deref for Config { impl Config { pub fn refresh(&mut self) { - let version = self.latest_version.load(Ordering::Relaxed); + let version = self.state.version.load(Ordering::Relaxed); if self.version == version { return; } - if let Some(path) = &self.path - && let Ok(bytes) = fs::read(path) - && let Ok(latest) = serde_json::from_slice(&bytes) - { - self.value = latest; - } + self.value = self.state.current.read().unwrap().clone(); self.version = version; } - pub fn update(&mut self, update: impl FnOnce(&mut T)) { + pub fn update(&mut self, mut update: impl FnMut(&mut T)) { let mut new_value = self.value.clone(); update(&mut new_value); if self.value == new_value { return; } - if let Some(path) = &self.path - && let Ok(bytes) = serde_json::to_vec(&new_value) + + let mut current = self.state.current.write().unwrap(); + update(&mut current); + if let Some(path) = &self.state.path + && let Ok(bytes) = serde_json::to_vec(&*current) { let _ = fs::write(path, bytes); } - let version = self.latest_version.fetch_add(1, Ordering::Relaxed) + 1; - self.value = new_value.clone(); + let version = self.state.version.fetch_add(1, Ordering::Relaxed) + 1; + self.value = current.clone(); self.version = version; } }