1use crate::{Action, Source};
4use fs2::FileExt;
5use nedb_engine::Db;
6use serde_json::{json, Value};
7use std::{fs::File, path::Path};
8
9#[derive(Clone, Debug, serde::Serialize)]
10pub struct Receipt {
11 pub seq: u64,
12 pub hash: String,
13 pub head: String,
14}
15pub struct Journal {
16 db: Db,
17 _lock: File,
18}
19impl Journal {
20 pub fn open(path: &Path) -> Result<Self, String> {
23 std::fs::create_dir_all(path).map_err(|e| e.to_string())?;
24 let lock = std::fs::OpenOptions::new()
25 .create(true)
26 .truncate(false)
27 .read(true)
28 .write(true)
29 .open(path.join("forge.lock"))
30 .map_err(|e| e.to_string())?;
31 lock.try_lock_exclusive()
32 .map_err(|e| format!("store already in use or unavailable: {e}"))?;
33 let db = Db::open(path, None).map_err(|e| e.to_string())?;
34 let (_, bad) = db.verify();
35 if !bad.is_empty() {
36 return Err(format!(
37 "integrity check failed; original files preserved: {bad:?}"
38 ));
39 }
40 Ok(Self { db, _lock: lock })
41 }
42 pub fn load(&self) -> Option<Value> {
43 self.db
44 .get("forge_state", "app")
45 .map(|n| n.data["state"].clone())
46 }
47 pub fn latest(&self) -> Option<Receipt> {
48 self.db.get("forge_state", "app").map(|n| Receipt {
49 seq: n.seq,
50 hash: n.hash,
51 head: self.db.head(),
52 })
53 }
54 pub fn commit(&self, state: Value, action: &Action, source: Source) -> Result<Receipt, String> {
55 let parents = self
56 .db
57 .get("forge_state", "app")
58 .map(|n| vec![n.hash])
59 .unwrap_or_default();
60 let node = self
61 .db
62 .put(
63 "forge_state",
64 "app",
65 json!({"schema":1,"state":state,"action":action,"source":source}),
66 parents,
67 None,
68 None,
69 )
70 .map_err(|e| e.to_string())?;
71 self.db
73 .try_flush_all()
74 .map_err(|e| format!("state not durably acknowledged: {e}"))?;
75 self.db
76 .try_flush_manifest()
77 .map_err(|e| format!("manifest flush failed: {e}"))?;
78 let read = self
79 .db
80 .get_by_hash(&node.hash)
81 .ok_or("read-after-write integrity check failed")?;
82 if read.data != node.data {
83 return Err("read-after-write data mismatch".into());
84 }
85 Ok(Receipt {
86 seq: node.seq,
87 hash: node.hash,
88 head: self.db.head(),
89 })
90 }
91 pub fn history(&self, limit: usize) -> Vec<Value> {
92 let Some(tip) = self.db.get("forge_state", "app") else {
93 return vec![];
94 };
95 let mut nodes = self.db.trace(&tip.hash, false, limit);
96 nodes.sort_by_key(|n| std::cmp::Reverse(n.seq));
97 nodes
98 .into_iter()
99 .map(|n| json!({"seq":n.seq,"hash":n.hash,"caused_by":n.caused_by,"data":n.data}))
100 .collect()
101 }
102 pub fn state_as_of(&self, seq: u64) -> Option<Value> {
103 self.db
104 .get_as_of("forge_state", "app", seq)
105 .map(|n| n.data["state"].clone())
106 }
107 pub fn verify(&self) -> Result<usize, String> {
108 let (count, bad) = self.db.verify();
109 if bad.is_empty() {
110 Ok(count)
111 } else {
112 Err(format!("integrity errors: {bad:?}"))
113 }
114 }
115}