Transform the single binary crate into a 9-crate workspace monorepo: - Root Cargo.toml as [workspace] manager with resolver = "2" - zesdex-entities: Domain entity types (session, settings, store, message, etc.) - zesdex-utils: Pure utility functions (error, logger, pagination, slug, clipboard) - zesdex-dto: Data Transfer Objects for LLM provider API communication - zesdex-ipc: Unix-socket IPC layer (client/server/framing/protocol) - zesdex-iam: Identity & Access Management (Clean Architecture: domain/application/infrastructure) - zesdex-cms: Content Management (Clean Architecture: domain/application/infrastructure) - zesdex-middleware: HTTP middleware (Auth, CORS, Rate Limiting) - zesdex-libs: Composition root (AppContext, DB init, JWT, Argon2) - zesdex-backend: Main binary entry point + seed/migrate binaries - DevOps: Dockerfile, docker-compose, Nix (flake/shell/default), CI/CD updates - Remove dead root src/ and src-misc/ directories All crate re-exports maintain backward compatibility with original crate::model::*, crate::dto::*, crate::ipc::* module paths. Feature crates enforce strict layer separation: domain -> application -> infrastructure with generic trait-based dependency injection.
107 lines
3.5 KiB
Rust
107 lines
3.5 KiB
Rust
//! JSONL file–backed `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()
|
||
}
|
||
}
|