Skip to main content

qualia_client_core/wellfair/
checkpoint_store.rs

1//! Durable WellFair checkpoint metadata, DAG history, and `.q42` volume snapshots.
2
3use 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}