Don't lean on filesystem so much
This commit is contained in:
+67
-43
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user