Files
zesdex/crates/zesdex-cms/src/infrastructure/persistence/edit_log_repo.rs
T

107 lines
3.5 KiB
Rust
Raw Normal View History

//! JSONL filebacked `EditLogRepository`.
//!
//! Path: `<session_dir>/edits.jsonl`
//!
//! Append-only log: new entries are appended to the file, never rewritten.
//! In-memory cache is capped at 10K entries to prevent unbounded growth.
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use anyhow::{Context, Result};
use crate::domain::edit_log::{EditLog, EditLogEntry, MAX_MEMORY_ENTRIES};
use crate::domain::repository::EditLogRepository;
/// Persists `EditLog` as an append-only JSONL file at `<session_dir>/edits.jsonl`.
#[derive(Debug, Clone, Default)]
pub struct JsonlEditLogRepository;
impl JsonlEditLogRepository {
/// Create a new repository instance.
pub fn new() -> Self {
Self
}
/// Read existing entries from disk into memory, capped at `MAX_MEMORY_ENTRIES`.
fn load_from_disk(path: &Path) -> Vec<EditLogEntry> {
let Ok(file) = std::fs::File::open(path) else {
return Vec::new();
};
let reader = BufReader::new(file);
let mut entries: Vec<EditLogEntry> = Vec::new();
for line in reader.lines() {
let Ok(line) = line else {
continue;
};
if let Ok(entry) = serde_json::from_str::<EditLogEntry>(&line) {
if entries.len() >= MAX_MEMORY_ENTRIES {
entries.remove(0);
}
entries.push(entry);
}
}
entries
}
}
impl EditLogRepository for JsonlEditLogRepository {
fn open(&self, session_dir: &Path) -> Result<EditLog> {
let path = session_dir.join("edits.jsonl");
// Ensure parent dir exists
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create session dir '{}'", parent.display()))?;
}
let entries = Self::load_from_disk(&path);
// Touch the file if it doesn't exist yet
if !path.exists() {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("failed to create edits.jsonl at '{}'", path.display()))?;
}
Ok(EditLog { entries })
}
fn append(&self, session_dir: &Path, log: &mut EditLog, entry: EditLogEntry) -> Result<()> {
let path = session_dir.join("edits.jsonl");
let line = serde_json::to_string(&entry)
.context("failed to serialize edit log entry")?
+ "\n";
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create session dir '{}'", parent.display()))?;
}
{
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("failed to open edits.jsonl at '{}'", path.display()))?;
file.write_all(line.as_bytes())
.context("failed to write edit log entry")?;
file.sync_all()
.context("failed to fsync edit log")?;
}
log.entries.push(entry);
// Enforce in-memory cap
if log.entries.len() > MAX_MEMORY_ENTRIES {
log.entries.remove(0);
}
Ok(())
}
fn entries(&self, log: &EditLog) -> Vec<EditLogEntry> {
log.entries.clone()
}
}