Don't lean on filesystem so much

This commit is contained in:
2026-10-31 12:33:12 -04:00
parent 55d3cf737d
commit bdec3baa74
+67 -43
View File
@@ -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<PathBuf>,
versions: Arc<Mutex<HashMap<String, Arc<AtomicU64>>>>,
states: Arc<Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>>,
}
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<T: ConfigCompatible>(&self, file: &str) -> Config<T> {
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> {
ProjectDirs::from("com", "virtual-boy", "Lemur")
}
pub trait ConfigCompatible: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a> {}
impl<T: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>> ConfigCompatible for T {}
pub trait ConfigCompatible:
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.
// Cheap to dereference, easy to sync changes to all copies.
struct ConfigState<T> {
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> {
value: T,
version: u64,
latest_version: Arc<AtomicU64>,
path: Option<PathBuf>,
state: Arc<ConfigState<T>>,
}
impl<T> Deref for Config<T> {
@@ -112,32 +138,30 @@ impl<T> Deref for Config<T> {
impl<T: ConfigCompatible> Config<T> {
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;
}
}