Don't lean on filesystem so much

This commit is contained in:
2026-10-31 12:33:12 -04:00
parent 55d3cf737d
commit bdec3baa74
+56 -32
View File
@@ -1,10 +1,11 @@
use std::{ use std::{
collections::HashMap, any::Any,
collections::{HashMap, hash_map::Entry},
fs, fs,
ops::Deref, ops::Deref,
path::PathBuf, path::PathBuf,
sync::{ sync::{
Arc, Mutex, Arc, Mutex, RwLock,
atomic::{AtomicU64, Ordering}, atomic::{AtomicU64, Ordering},
}, },
}; };
@@ -16,25 +17,22 @@ use serde::{Deserialize, Serialize};
#[derive(Clone)] #[derive(Clone)]
pub struct Persistence { pub struct Persistence {
config_dir: Option<PathBuf>, config_dir: Option<PathBuf>,
versions: Arc<Mutex<HashMap<String, Arc<AtomicU64>>>>, states: Arc<Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>>,
} }
impl Persistence { impl Persistence {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
config_dir: init_config_dir(), config_dir: init_config_dir(),
versions: Arc::new(Mutex::new(HashMap::new())), states: Arc::new(Mutex::new(HashMap::new())),
} }
} }
pub fn config<T: ConfigCompatible>(&self, file: &str) -> Config<T> { pub fn config<T: ConfigCompatible>(&self, file: &str) -> Config<T> {
let latest_version = self let state = {
.versions let mut lock = self.states.lock().unwrap();
.lock() match lock.entry(file.to_string()) {
.unwrap() Entry::Vacant(e) => {
.entry(file.to_string())
.or_default()
.clone();
let path = self let path = self
.config_dir .config_dir
.as_ref() .as_ref()
@@ -47,11 +45,28 @@ impl Persistence {
} else { } else {
T::default() 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 { Config {
value, value,
version: latest_version.load(Ordering::Relaxed), version,
latest_version, state,
path,
} }
} }
@@ -90,16 +105,27 @@ fn project_dirs() -> Option<ProjectDirs> {
ProjectDirs::from("com", "virtual-boy", "Lemur") ProjectDirs::from("com", "virtual-boy", "Lemur")
} }
pub trait ConfigCompatible: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a> {} pub trait ConfigCompatible:
impl<T: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>> ConfigCompatible for T {} Send + Sync + 'static + Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>
{
}
impl<T: Send + Sync + 'static + Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>>
ConfigCompatible for T
{
}
// A wrapper for a T (probably) stored on the filesystem. struct ConfigState<T> {
// Cheap to dereference, easy to sync changes to all copies. current: RwLock<T>,
version: AtomicU64,
path: Option<PathBuf>,
}
// 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<T> { pub struct Config<T> {
value: T, value: T,
version: u64, version: u64,
latest_version: Arc<AtomicU64>, state: Arc<ConfigState<T>>,
path: Option<PathBuf>,
} }
impl<T> Deref for Config<T> { impl<T> Deref for Config<T> {
@@ -112,32 +138,30 @@ impl<T> Deref for Config<T> {
impl<T: ConfigCompatible> Config<T> { impl<T: ConfigCompatible> Config<T> {
pub fn refresh(&mut self) { 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 { if self.version == version {
return; return;
} }
if let Some(path) = &self.path self.value = self.state.current.read().unwrap().clone();
&& let Ok(bytes) = fs::read(path)
&& let Ok(latest) = serde_json::from_slice(&bytes)
{
self.value = latest;
}
self.version = version; 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(); let mut new_value = self.value.clone();
update(&mut new_value); update(&mut new_value);
if self.value == new_value { if self.value == new_value {
return; 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 _ = fs::write(path, bytes);
} }
let version = self.latest_version.fetch_add(1, Ordering::Relaxed) + 1; let version = self.state.version.fetch_add(1, Ordering::Relaxed) + 1;
self.value = new_value.clone(); self.value = current.clone();
self.version = version; self.version = version;
} }
} }