Skip to main content

memnite_core/
log.rs

1use std::fs::{self, OpenOptions};
2use std::io::Write;
3use std::path::{Path, PathBuf};
4
5use crate::error::CoreError;
6use crate::event::Event;
7
8/// Append-only NDJSON event log. The single source of truth.
9pub struct Log {
10    path: PathBuf,
11}
12
13impl Log {
14    /// Open (creating it if needed) the log directory; the log file is
15    /// `<dir>/events.ndjson`.
16    pub fn open(dir: impl AsRef<Path>) -> Result<Self, CoreError> {
17        let dir = dir.as_ref();
18        fs::create_dir_all(dir)?;
19        Ok(Log {
20            path: dir.join("events.ndjson"),
21        })
22    }
23
24    /// Append one event as a JSON line and fsync. Never rewrites prior lines.
25    pub fn append(&self, ev: &Event) -> Result<(), CoreError> {
26        let line = serde_json::to_string(ev)?;
27        let mut f = OpenOptions::new()
28            .create(true)
29            .append(true)
30            .open(&self.path)?;
31        writeln!(f, "{line}")?;
32        f.sync_all()?;
33        Ok(())
34    }
35
36    /// Read every valid event. Corrupt lines are skipped (warned to stderr),
37    /// never aborting the whole read. Missing file => empty vec.
38    pub fn read_all(&self) -> Result<Vec<Event>, CoreError> {
39        if !self.path.exists() {
40            return Ok(Vec::new());
41        }
42        let content = fs::read_to_string(&self.path)?;
43        let mut events = Vec::new();
44        for (i, line) in content.lines().enumerate() {
45            if line.trim().is_empty() {
46                continue;
47            }
48            match serde_json::from_str::<Event>(line) {
49                Ok(ev) => events.push(ev),
50                Err(e) => eprintln!("memnite: skipping corrupt log line {}: {e}", i + 1),
51            }
52        }
53        Ok(events)
54    }
55}