Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/backend/database/in_memory/
persistence.rs

1//! Persistence operations for InMemory database
2//!
3//! This module handles serialization and file I/O for saving/loading
4//! the in-memory database state to/from JSON files.
5
6use std::{collections::HashMap, path::Path, sync::RwLock};
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::{InMemory, InMemoryInner, TreeTipsCache};
11use crate::{
12    Error, Result,
13    backend::{InstanceMetadata, VerificationStatus, errors::BackendError},
14    entry::{Entry, ID},
15};
16
17/// The current persistence file format version.
18/// v0 indicates this is an unstable format subject to breaking changes.
19const PERSISTENCE_VERSION: u8 = 0;
20
21/// Helper to check if version is default (0) for serde skip_serializing_if
22fn is_v0(v: &u8) -> bool {
23    *v == 0
24}
25
26/// Validates the persistence version during deserialization.
27fn validate_persistence_version<'de, D>(deserializer: D) -> std::result::Result<u8, D::Error>
28where
29    D: Deserializer<'de>,
30{
31    use serde::Deserialize;
32    let version = u8::deserialize(deserializer)?;
33    if version != PERSISTENCE_VERSION {
34        return Err(serde::de::Error::custom(format!(
35            "unsupported persistence version {version}; only version {PERSISTENCE_VERSION} is supported"
36        )));
37    }
38    Ok(version)
39}
40
41/// Serializable version of InMemory database for persistence
42#[derive(Serialize, Deserialize)]
43struct SerializableDatabase {
44    /// File format version for compatibility checking
45    #[serde(
46        rename = "_v",
47        default,
48        skip_serializing_if = "is_v0",
49        deserialize_with = "validate_persistence_version"
50    )]
51    version: u8,
52    entries: HashMap<ID, Entry>,
53    #[serde(default)]
54    verification_status: HashMap<ID, VerificationStatus>,
55    /// Instance metadata containing device key and system database IDs
56    #[serde(default)]
57    instance_metadata: Option<InstanceMetadata>,
58    /// Generic key-value cache (not serialized - cache is rebuilt on load)
59    #[serde(default)]
60    cache: HashMap<String, String>,
61    /// Cached tips grouped by tree
62    #[serde(default)]
63    tips: HashMap<ID, TreeTipsCache>,
64}
65
66impl Serialize for InMemory {
67    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
68    where
69        S: Serializer,
70    {
71        // Clone data under locks, then release before serializing
72        let serializable = {
73            let inner = self.inner.read().unwrap();
74            let crdt_cache = self.crdt_cache.read().unwrap();
75            SerializableDatabase {
76                version: PERSISTENCE_VERSION,
77                entries: inner.entries.clone(),
78                verification_status: inner.verification_status.clone(),
79                instance_metadata: inner.instance_metadata.clone(),
80                cache: crdt_cache.clone(),
81                tips: inner.tips.clone(),
82            }
83        };
84
85        serializable.serialize(serializer)
86    }
87}
88
89impl<'de> Deserialize<'de> for InMemory {
90    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
91    where
92        D: Deserializer<'de>,
93    {
94        // Version validation happens via deserialize_with on SerializableDatabase._v
95        let serializable = SerializableDatabase::deserialize(deserializer)?;
96
97        Ok(InMemory {
98            inner: RwLock::new(InMemoryInner {
99                entries: serializable.entries,
100                verification_status: serializable.verification_status,
101                instance_metadata: serializable.instance_metadata,
102                tips: serializable.tips,
103            }),
104            crdt_cache: RwLock::new(serializable.cache),
105        })
106    }
107}
108
109/// Saves the entire database state (all entries) to a specified file as JSON.
110///
111/// # Arguments
112/// * `backend` - The InMemory database to save
113/// * `path` - The path to the file where the state should be saved.
114///
115/// # Returns
116/// A `Result` indicating success or an I/O or serialization error.
117pub(crate) fn save_to_file<P: AsRef<Path>>(backend: &InMemory, path: P) -> Result<()> {
118    // Clone data under locks, then release before file I/O
119    let serializable = {
120        let inner = backend.inner.read().unwrap();
121        let crdt_cache = backend.crdt_cache.read().unwrap();
122        SerializableDatabase {
123            version: PERSISTENCE_VERSION,
124            entries: inner.entries.clone(),
125            verification_status: inner.verification_status.clone(),
126            instance_metadata: inner.instance_metadata.clone(),
127            cache: crdt_cache.clone(),
128            tips: inner.tips.clone(),
129        }
130    };
131
132    let json = serde_json::to_string_pretty(&serializable)
133        .map_err(|e| -> Error { BackendError::SerializationFailed { source: e }.into() })?;
134    std::fs::write(path, json).map_err(|e| -> Error { BackendError::FileIo { source: e }.into() })
135}
136
137/// Loads the database state from a specified JSON file.
138///
139/// If the file does not exist, a new, empty `InMemory` database is returned.
140///
141/// # Arguments
142/// * `path` - The path to the file from which to load the state.
143///
144/// # Returns
145/// A `Result` containing the loaded `InMemory` database or an I/O or deserialization error.
146pub(crate) fn load_from_file<P: AsRef<Path>>(path: P) -> Result<InMemory> {
147    match std::fs::read_to_string(path) {
148        Ok(json) => {
149            let database: InMemory = serde_json::from_str(&json).map_err(|e| -> Error {
150                BackendError::DeserializationFailed { source: e }.into()
151            })?;
152            Ok(database)
153        }
154        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(InMemory::new()),
155        Err(e) => Err(BackendError::FileIo { source: e }.into()),
156    }
157}