eidetica/backend/database/in_memory/mod.rs
1//! In-memory database backend implementation
2//!
3//! This module provides an in-memory implementation of the Database trait,
4//! suitable for testing, development, or scenarios where data persistence
5//! is not strictly required or is handled externally.
6
7mod cache;
8mod persistence;
9mod storage;
10mod traversal;
11
12use std::{
13 any::Any,
14 collections::{HashMap, HashSet},
15 path::Path,
16 sync::RwLock,
17};
18
19use async_trait::async_trait;
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 Result,
24 backend::{BackendImpl, InstanceMetadata, VerificationStatus, errors::BackendError},
25 entry::{Entry, ID},
26};
27
28/// Grouped tree tips cache: (tree_tips, subtree_name -> subtree_tips)
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub(crate) struct TreeTipsCache {
31 pub(crate) tree_tips: HashSet<ID>,
32 pub(crate) subtree_tips: HashMap<String, HashSet<ID>>,
33}
34
35/// Core data protected by a single lock.
36///
37/// All fields that participate in entry storage and tip tracking are grouped
38/// together to eliminate lock ordering concerns. A single `RwLock` on the
39/// outer `InMemory` struct protects all fields atomically.
40#[derive(Debug)]
41pub(crate) struct InMemoryInner {
42 pub(crate) entries: HashMap<ID, Entry>,
43 pub(crate) verification_status: HashMap<ID, VerificationStatus>,
44 /// Instance metadata containing device key and system database IDs.
45 ///
46 /// When `None`, the backend is uninitialized. When `Some`, contains the
47 /// device key and root IDs for system databases.
48 ///
49 /// **Security Warning**: The device key is stored in memory without encryption.
50 /// This is suitable for development/testing only. Production systems should use
51 /// proper key management with encryption at rest.
52 pub(crate) instance_metadata: Option<InstanceMetadata>,
53 /// Cached tips grouped by tree: tree_id -> (tree_tips, subtree_name -> subtree_tips)
54 pub(crate) tips: HashMap<ID, TreeTipsCache>,
55}
56
57/// A simple in-memory database implementation using a `HashMap` for storage.
58///
59/// This database is suitable for testing, development, or scenarios where
60/// data persistence is not strictly required or is handled externally
61/// (e.g., by saving/loading the entire state to/from a file).
62///
63/// It provides basic persistence capabilities via `save_to_file` and
64/// `load_from_file`, serializing the `HashMap` to JSON.
65///
66/// **Security Note**: The device key is stored in memory in plaintext in this implementation.
67/// This is acceptable for development and testing but should not be used in production
68/// without proper encryption or hardware security module integration.
69#[derive(Debug)]
70pub struct InMemory {
71 /// Core data protected by a single lock for atomic access and
72 /// to eliminate lock ordering concerns between entries, verification
73 /// status, and tips.
74 pub(crate) inner: RwLock<InMemoryInner>,
75 /// CRDT state cache, independent of core data.
76 /// Kept separate because it has no coupling to entries/tips lifecycle.
77 pub(crate) crdt_cache: RwLock<HashMap<String, String>>,
78}
79
80impl InMemory {
81 /// Creates a new, empty `InMemory` database.
82 pub fn new() -> Self {
83 Self {
84 inner: RwLock::new(InMemoryInner {
85 entries: HashMap::new(),
86 verification_status: HashMap::new(),
87 instance_metadata: None,
88 tips: HashMap::new(),
89 }),
90 crdt_cache: RwLock::new(HashMap::new()),
91 }
92 }
93
94 /// Returns a vector containing the IDs of all entries currently stored in the database.
95 pub async fn all_ids(&self) -> Vec<ID> {
96 let inner = self.inner.read().unwrap();
97 inner.entries.keys().cloned().collect()
98 }
99
100 /// Saves the entire database state (all entries) to a specified file as JSON.
101 ///
102 /// # Arguments
103 /// * `path` - The path to the file where the state should be saved.
104 ///
105 /// # Returns
106 /// A `Result` indicating success or an I/O or serialization error.
107 pub async fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
108 persistence::save_to_file(self, path)
109 }
110
111 /// Loads the database state from a specified JSON file.
112 ///
113 /// If the file does not exist, a new, empty `InMemory` database is returned.
114 ///
115 /// # Arguments
116 /// * `path` - The path to the file from which to load the state.
117 ///
118 /// # Returns
119 /// A `Result` containing the loaded `InMemory` database or an I/O or deserialization error.
120 pub async fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
121 persistence::load_from_file(path)
122 }
123
124 /// Sort entries by their height within a tree (exposed for testing)
125 ///
126 /// Heights are stored directly in entries, so this just reads and sorts.
127 ///
128 /// # Arguments
129 /// * `_tree` - The ID of the tree context (unused, kept for API compatibility)
130 /// * `entries` - The vector of entries to be sorted in place
131 pub fn sort_entries_by_height(&self, _tree: &ID, entries: &mut [Entry]) {
132 cache::sort_entries_by_height(entries)
133 }
134
135 /// Sort entries by their height within a subtree (exposed for testing)
136 ///
137 /// Heights are stored directly in entries, so this just reads and sorts.
138 ///
139 /// # Arguments
140 /// * `_tree` - The ID of the tree context (unused, kept for API compatibility)
141 /// * `subtree` - The name of the subtree context
142 /// * `entries` - The vector of entries to be sorted in place
143 pub fn sort_entries_by_subtree_height(&self, _tree: &ID, subtree: &str, entries: &mut [Entry]) {
144 cache::sort_entries_by_subtree_height(subtree, entries)
145 }
146
147 /// Check if an entry is a tip within its tree (exposed for benchmarks)
148 ///
149 /// An entry is a tip if no other entry in the same tree lists it as a parent.
150 ///
151 /// # Arguments
152 /// * `tree` - The ID of the tree to check within
153 /// * `entry_id` - The ID of the entry to check
154 ///
155 /// # Returns
156 /// `true` if the entry is a tip, `false` otherwise
157 pub async fn is_tip(&self, tree: &ID, entry_id: &ID) -> bool {
158 let inner = self.inner.read().unwrap();
159 storage::is_tip(&inner.entries, tree, entry_id)
160 }
161}
162
163impl Default for InMemory {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169#[async_trait]
170impl BackendImpl for InMemory {
171 /// Retrieves an entry by its unique content-addressable ID.
172 ///
173 /// # Arguments
174 /// * `id` - The ID of the entry to retrieve.
175 ///
176 /// # Returns
177 /// A `Result` containing the `Entry` if found, or a `DatabaseError::EntryNotFound` otherwise.
178 /// Returns an owned copy to support concurrent access with internal synchronization.
179 async fn get(&self, id: &ID) -> Result<Entry> {
180 let inner = self.inner.read().unwrap();
181 storage::get(&inner, id)
182 }
183
184 /// Gets the verification status of an entry.
185 ///
186 /// # Arguments
187 /// * `id` - The ID of the entry to check.
188 ///
189 /// # Returns
190 /// A `Result` containing the `VerificationStatus` if the entry exists, or a `DatabaseError::VerificationStatusNotFound` otherwise.
191 async fn get_verification_status(&self, id: &ID) -> Result<VerificationStatus> {
192 let inner = self.inner.read().unwrap();
193 inner
194 .verification_status
195 .get(id)
196 .copied()
197 .ok_or_else(|| BackendError::VerificationStatusNotFound { id: id.clone() }.into())
198 }
199
200 async fn put(&self, verification_status: VerificationStatus, entry: Entry) -> Result<()> {
201 // Validate before acquiring write lock to fail fast
202 entry.validate()?;
203 let mut inner = self.inner.write().unwrap();
204 storage::put(&mut inner, verification_status, entry)
205 }
206
207 /// Updates the verification status of an existing entry.
208 ///
209 /// This allows the authentication system to mark entries as verified or failed
210 /// after they have been stored. Useful for batch verification operations.
211 ///
212 /// # Arguments
213 /// * `id` - The ID of the entry to update
214 /// * `verification_status` - The new verification status
215 ///
216 /// # Returns
217 /// A `Result` indicating success or `DatabaseError::EntryNotFound` if the entry doesn't exist.
218 async fn update_verification_status(
219 &self,
220 id: &ID,
221 verification_status: VerificationStatus,
222 ) -> Result<()> {
223 let mut inner = self.inner.write().unwrap();
224 if inner.verification_status.contains_key(id) {
225 inner
226 .verification_status
227 .insert(id.clone(), verification_status);
228 Ok(())
229 } else {
230 Err(BackendError::EntryNotFound { id: id.clone() }.into())
231 }
232 }
233
234 /// Gets all entries with a specific verification status.
235 ///
236 /// This is useful for finding unverified entries that need authentication
237 /// or for security audits.
238 ///
239 /// # Arguments
240 /// * `status` - The verification status to filter by
241 ///
242 /// # Returns
243 /// A `Result` containing a vector of entry IDs with the specified status.
244 async fn get_entries_by_verification_status(
245 &self,
246 status: VerificationStatus,
247 ) -> Result<Vec<ID>> {
248 let inner = self.inner.read().unwrap();
249 let ids = inner
250 .verification_status
251 .iter()
252 .filter(|&(_, entry_status)| *entry_status == status)
253 .map(|(id, _)| id.clone())
254 .collect();
255 Ok(ids)
256 }
257
258 async fn get_tips(&self, tree: &ID) -> Result<Vec<ID>> {
259 // Fast path: check cache with read lock
260 {
261 let inner = self.inner.read().unwrap();
262 if let Some(cache) = inner.tips.get(tree) {
263 return Ok(cache.tree_tips.iter().cloned().collect());
264 }
265 }
266 // Slow path: compute and cache with write lock
267 let mut inner = self.inner.write().unwrap();
268 traversal::get_tips(&mut inner, tree)
269 }
270
271 async fn get_store_tips(&self, tree: &ID, subtree: &str) -> Result<Vec<ID>> {
272 // Fast path: check cache with read lock
273 {
274 let inner = self.inner.read().unwrap();
275 if let Some(cache) = inner.tips.get(tree)
276 && let Some(subtree_tips) = cache.subtree_tips.get(subtree)
277 {
278 return Ok(subtree_tips.iter().cloned().collect());
279 }
280 }
281 // Slow path: compute and cache with write lock
282 let mut inner = self.inner.write().unwrap();
283 traversal::get_store_tips(&mut inner, tree, subtree)
284 }
285
286 async fn get_store_tips_up_to_entries(
287 &self,
288 tree: &ID,
289 subtree: &str,
290 main_entries: &[ID],
291 ) -> Result<Vec<ID>> {
292 let mut inner = self.inner.write().unwrap();
293 traversal::get_store_tips_up_to_entries(&mut inner, tree, subtree, main_entries)
294 }
295
296 /// Retrieves the IDs of all top-level root entries stored in the database.
297 ///
298 /// Top-level roots are entries that are themselves roots of a tree
299 /// (i.e., `entry.is_root()` is true) and are not part of a larger tree structure
300 /// tracked by the backend. These represent the starting points
301 /// of distinct trees managed by the database.
302 ///
303 /// # Returns
304 /// A `Result` containing a vector of top-level root entry IDs or an error.
305 async fn all_roots(&self) -> Result<Vec<ID>> {
306 let inner = self.inner.read().unwrap();
307 let roots: Vec<ID> = inner
308 .entries
309 .values()
310 .filter(|entry| entry.is_root())
311 .map(|entry| entry.id())
312 .collect();
313 Ok(roots)
314 }
315
316 async fn find_merge_base(&self, tree: &ID, subtree: &str, entry_ids: &[ID]) -> Result<ID> {
317 let inner = self.inner.read().unwrap();
318 traversal::find_merge_base(&inner, tree, subtree, entry_ids)
319 }
320
321 async fn collect_root_to_target(
322 &self,
323 tree: &ID,
324 subtree: &str,
325 target_entry: &ID,
326 ) -> Result<Vec<ID>> {
327 let inner = self.inner.read().unwrap();
328 traversal::collect_root_to_target(&inner, tree, subtree, target_entry)
329 }
330
331 fn as_any(&self) -> &dyn Any {
332 self
333 }
334
335 async fn get_tree(&self, tree: &ID) -> Result<Vec<Entry>> {
336 let inner = self.inner.read().unwrap();
337 storage::get_tree(&inner, tree)
338 }
339
340 async fn get_store(&self, tree: &ID, subtree: &str) -> Result<Vec<Entry>> {
341 let inner = self.inner.read().unwrap();
342 storage::get_store(&inner, tree, subtree)
343 }
344
345 async fn get_tree_from_tips(&self, tree: &ID, tips: &[ID]) -> Result<Vec<Entry>> {
346 let inner = self.inner.read().unwrap();
347 storage::get_tree_from_tips(&inner, tree, tips)
348 }
349
350 async fn get_store_from_tips(
351 &self,
352 tree: &ID,
353 subtree: &str,
354 tips: &[ID],
355 ) -> Result<Vec<Entry>> {
356 let inner = self.inner.read().unwrap();
357 storage::get_store_from_tips(&inner, tree, subtree, tips)
358 }
359
360 async fn get_instance_metadata(&self) -> Result<Option<InstanceMetadata>> {
361 let inner = self.inner.read().unwrap();
362 Ok(inner.instance_metadata.clone())
363 }
364
365 async fn set_instance_metadata(&self, metadata: &InstanceMetadata) -> Result<()> {
366 let mut inner = self.inner.write().unwrap();
367 inner.instance_metadata = Some(metadata.clone());
368 Ok(())
369 }
370
371 async fn get_cached_crdt_state(&self, entry_id: &ID, subtree: &str) -> Result<Option<String>> {
372 cache::get_cached_crdt_state(self, entry_id, subtree)
373 }
374
375 async fn cache_crdt_state(&self, entry_id: &ID, subtree: &str, state: String) -> Result<()> {
376 cache::cache_crdt_state(self, entry_id, subtree, state)
377 }
378
379 async fn clear_crdt_cache(&self) -> Result<()> {
380 cache::clear_crdt_cache(self)
381 }
382
383 async fn get_sorted_store_parents(
384 &self,
385 tree_id: &ID,
386 entry_id: &ID,
387 subtree: &str,
388 ) -> Result<Vec<ID>> {
389 let inner = self.inner.read().unwrap();
390 traversal::get_sorted_store_parents(&inner, tree_id, entry_id, subtree)
391 }
392
393 async fn get_path_from_to(
394 &self,
395 tree_id: &ID,
396 subtree: &str,
397 from_id: &ID,
398 to_ids: &[ID],
399 ) -> Result<Vec<ID>> {
400 let inner = self.inner.read().unwrap();
401 traversal::get_path_from_to(&inner, tree_id, subtree, from_id, to_ids)
402 }
403}