use automerge::{transaction::Transactable, ActorId, Automerge, ReadDoc, ScalarValue, ROOT};
use std::path::PathBuf;
use std::sync::Mutex;
const COUNTER_FIELD: &str = "liters";
const GENESIS_ACTOR: &[u8; 16] = b"peatwatergenesis";
pub(crate) struct WaterCounter {
doc: Mutex<Automerge>,
path: PathBuf,
}
impl WaterCounter {
pub(crate) fn load_or_init(path: PathBuf) -> Self {
let mut doc = Self::new_doc();
if let Ok(bytes) = std::fs::read(&path) {
if let Ok(mut persisted) = Automerge::load(&bytes) {
let _ = doc.merge(&mut persisted);
}
}
WaterCounter {
doc: Mutex::new(doc),
path,
}
}
fn new_doc() -> Automerge {
let mut doc = Automerge::new();
doc.set_actor(ActorId::from(&GENESIS_ACTOR[..]));
let _ = doc.transact::<_, _, automerge::AutomergeError>(|tx| {
tx.put(ROOT, COUNTER_FIELD, ScalarValue::counter(0))?;
Ok(())
});
doc.set_actor(ActorId::random());
doc
}
fn read_value(doc: &Automerge) -> i64 {
match doc.get(ROOT, COUNTER_FIELD) {
Ok(Some((automerge::Value::Scalar(s), _))) => match s.as_ref() {
ScalarValue::Counter(c) => i64::from(c),
ScalarValue::Int(i) => *i,
ScalarValue::Uint(u) => *u as i64,
_ => 0,
},
_ => 0,
}
}
fn persist(&self, doc: &mut Automerge) {
if let Some(parent) = self.path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&self.path, doc.save());
}
pub(crate) fn value(&self) -> i64 {
let doc = self.doc.lock().unwrap_or_else(|e| e.into_inner());
Self::read_value(&doc)
}
pub(crate) fn increment(&self, delta: i64) -> Vec<u8> {
let mut doc = self.doc.lock().unwrap_or_else(|e| e.into_inner());
let _ = doc.transact::<_, _, automerge::AutomergeError>(|tx| {
tx.increment(ROOT, COUNTER_FIELD, delta)?;
Ok(())
});
let bytes = doc.save();
self.persist(&mut doc);
bytes
}
pub(crate) fn merge(&self, bytes: &[u8]) -> i64 {
let mut incoming = match Automerge::load(bytes) {
Ok(d) => d,
Err(_) => return self.value(),
};
let mut doc = self.doc.lock().unwrap_or_else(|e| e.into_inner());
let _ = doc.merge(&mut incoming);
let v = Self::read_value(&doc);
self.persist(&mut doc);
v
}
pub(crate) fn snapshot(&self) -> Vec<u8> {
let mut doc = self.doc.lock().unwrap_or_else(|e| e.into_inner());
doc.save()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"peat-water-test-{name}-{:?}.automerge",
std::thread::current().id()
));
let _ = std::fs::remove_file(&p);
p
}
#[test]
fn concurrent_increments_merge_to_sum() {
let a = WaterCounter::load_or_init(tmp("a"));
let b = WaterCounter::load_or_init(tmp("b"));
let a_bytes = a.increment(5); let b_bytes = b.increment(7);
let a_val = a.merge(&b_bytes); let b_val = b.merge(&a_bytes);
assert_eq!(a_val, 12, "A should see 5+7");
assert_eq!(b_val, 12, "B should see 5+7");
assert_eq!(a.value(), 12);
assert_eq!(b.value(), 12);
}
#[test]
fn merge_is_idempotent() {
let a = WaterCounter::load_or_init(tmp("idem-a"));
let b = WaterCounter::load_or_init(tmp("idem-b"));
let b_bytes = b.increment(9);
assert_eq!(a.merge(&b_bytes), 9);
assert_eq!(
a.merge(&b_bytes),
9,
"second merge of same bytes is a no-op"
);
assert_eq!(a.merge(&b_bytes), 9, "third merge too");
}
#[test]
fn decrement_and_then_merge() {
let a = WaterCounter::load_or_init(tmp("dec-a"));
let b = WaterCounter::load_or_init(tmp("dec-b"));
a.increment(10);
b.increment(-3); let v = a.merge(&b.snapshot());
assert_eq!(v, 7, "10 + (-3)");
}
#[test]
fn value_survives_reload() {
let path = tmp("reload");
{
let c = WaterCounter::load_or_init(path.clone());
c.increment(4);
c.increment(3);
assert_eq!(c.value(), 7);
}
let c2 = WaterCounter::load_or_init(path);
assert_eq!(c2.value(), 7, "value persisted across reload");
}
}