qualia_client_core/wellfair/
checkpoint_store.rs1use std::fs;
4use std::path::Path;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use qualia_core_db::git_bridge::DagStore;
8use qualia_core_db::NQuin;
9use serde::{Deserialize, Serialize};
10
11pub const CHECKPOINT_DIR: &str = "wellfair/checkpoint";
12pub const META_FILE: &str = "wellfair/checkpoint/meta.json";
13pub const DAG_FILE: &str = "wellfair/checkpoint/dag.bin";
14pub const Q42_FILE: &str = "wellfair/checkpoint/vault.q42";
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct CheckpointMeta {
18 pub last_hash_hex: String,
19 pub dag_node_count: u32,
20 pub graph_quin_count: u32,
21 pub checkpoint_unix: u32,
22}
23
24impl Default for CheckpointMeta {
25 fn default() -> Self {
26 Self {
27 last_hash_hex: String::new(),
28 dag_node_count: 0,
29 graph_quin_count: 0,
30 checkpoint_unix: 0,
31 }
32 }
33}
34
35pub fn load_meta(storage_root: impl AsRef<Path>) -> Option<CheckpointMeta> {
36 let path = storage_root.as_ref().join(META_FILE);
37 let text = fs::read_to_string(path).ok()?;
38 serde_json::from_str(&text).ok()
39}
40
41pub fn save_meta(storage_root: impl AsRef<Path>, meta: &CheckpointMeta) -> std::io::Result<()> {
42 let path = storage_root.as_ref().join(META_FILE);
43 ensure_parent(&path)?;
44 let text =
45 serde_json::to_string_pretty(meta).map_err(|e| std::io::Error::other(e.to_string()))?;
46 fs::write(path, text)
47}
48
49pub fn load_dag(storage_root: impl AsRef<Path>) -> DagStore {
50 let path = storage_root.as_ref().join(DAG_FILE);
51 match fs::read(&path) {
52 Ok(bytes) => DagStore::deserialize(&bytes).unwrap_or_default(),
53 Err(_) => DagStore::new(),
54 }
55}
56
57pub fn save_dag(storage_root: impl AsRef<Path>, dag: &DagStore) -> std::io::Result<()> {
58 let path = storage_root.as_ref().join(DAG_FILE);
59 ensure_parent(&path)?;
60 fs::write(path, dag.serialize())
61}
62
63pub fn persist_checkpoint(
64 storage_root: impl AsRef<Path>,
65 dag: &DagStore,
66 hash: [u8; 32],
67 graph_quin_count: usize,
68 batch_quins: &[NQuin],
69 author_did: u64,
70) -> std::io::Result<()> {
71 save_dag(&storage_root, dag)?;
72 let meta = CheckpointMeta {
73 last_hash_hex: hex::encode(hash),
74 dag_node_count: dag.nodes().len() as u32,
75 graph_quin_count: graph_quin_count as u32,
76 checkpoint_unix: SystemTime::now()
77 .duration_since(UNIX_EPOCH)
78 .map(|d| d.as_secs() as u32)
79 .unwrap_or(0),
80 };
81 save_meta(storage_root.as_ref(), &meta)?;
82 write_q42_checkpoint(storage_root.as_ref(), batch_quins, author_did)
83}
84
85#[cfg(not(target_arch = "wasm32"))]
86pub fn write_q42_checkpoint(
87 storage_root: &Path,
88 quins: &[NQuin],
89 author_did: u64,
90) -> std::io::Result<()> {
91 if quins.is_empty() {
92 return Ok(());
93 }
94 let path = storage_root.join(Q42_FILE);
95 qualia_core_db::q42_volume::write_sorted_quins_volume_with_author(&path, quins, author_did)?;
96 Ok(())
97}
98
99#[cfg(target_arch = "wasm32")]
100pub fn write_q42_checkpoint(
101 _storage_root: &Path,
102 _quins: &[NQuin],
103 _author_did: u64,
104) -> std::io::Result<()> {
105 Ok(())
106}
107
108fn ensure_parent(path: &Path) -> std::io::Result<()> {
109 if let Some(parent) = path.parent() {
110 fs::create_dir_all(parent)?;
111 }
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn checkpoint_meta_round_trip() {
121 let dir = tempfile::tempdir().unwrap();
122 let meta = CheckpointMeta {
123 last_hash_hex: "abc123".into(),
124 dag_node_count: 2,
125 graph_quin_count: 10,
126 checkpoint_unix: 1_700_000_000,
127 };
128 save_meta(dir.path(), &meta).unwrap();
129 assert_eq!(load_meta(dir.path()), Some(meta));
130 }
131
132 #[test]
133 fn dag_persist_round_trip() {
134 let dir = tempfile::tempdir().unwrap();
135 let mut dag = DagStore::new();
136 dag.genesis_node(&[], 42, 1000, "genesis");
137 save_dag(dir.path(), &dag).unwrap();
138 let restored = load_dag(dir.path());
139 assert_eq!(restored.nodes().len(), 1);
140 }
141}