use std::fs::{self, File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::chain::ChainEvent;
use crate::pack_bridge;
use crate::TheaterId;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunMeta {
pub actor_id: String,
pub actor_name: Option<String>,
pub manifest_path: Option<String>,
pub started_at: u64,
}
pub struct ChainWriter {
writer: BufWriter<File>,
path: PathBuf,
}
impl ChainWriter {
pub fn new(actor_id: &TheaterId) -> Result<Self> {
let chains_dir = Self::chains_dir();
fs::create_dir_all(&chains_dir)
.with_context(|| format!("Failed to create chains directory: {:?}", chains_dir))?;
let path = chains_dir.join(format!("{}.jsonl", actor_id));
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("Failed to open chain file: {:?}", path))?;
Ok(Self {
writer: BufWriter::new(file),
path,
})
}
pub fn chains_dir() -> PathBuf {
PathBuf::from("/tmp/theater/chains")
}
pub fn write_meta(actor_id: &TheaterId, meta: &RunMeta) -> Result<()> {
let chains_dir = Self::chains_dir();
fs::create_dir_all(&chains_dir)?;
let path = chains_dir.join(format!("{}.meta.json", actor_id));
let json = serde_json::to_string_pretty(meta)?;
fs::write(&path, json)?;
Ok(())
}
pub fn append(&mut self, event: &ChainEvent) -> Result<()> {
let data_json = if let Ok(value) = pack_bridge::decode_value(&event.data) {
serde_json::to_value(&value).unwrap_or(serde_json::Value::Null)
} else {
serde_json::Value::String(hex::encode(&event.data))
};
let entry = serde_json::json!({
"hash": hex::encode(&event.hash),
"parent_hash": event.parent_hash.as_ref().map(hex::encode),
"event_type": event.event_type,
"data": data_json,
});
serde_json::to_writer(&mut self.writer, &entry)?;
writeln!(self.writer)?;
self.writer.flush()?;
Ok(())
}
pub fn path(&self) -> &PathBuf {
&self.path
}
}
impl Drop for ChainWriter {
fn drop(&mut self) {
let _ = self.writer.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
#[test]
fn test_chain_writer_append() {
let actor_id = TheaterId::generate();
let mut writer = ChainWriter::new(&actor_id).unwrap();
let event = ChainEvent {
hash: vec![0x1a, 0x2b, 0x3c],
parent_hash: None,
event_type: "test".to_string(),
data: b"not valid cgrf".to_vec(),
};
writer.append(&event).unwrap();
drop(writer);
let path = ChainWriter::chains_dir().join(format!("{}.jsonl", actor_id));
let mut file = File::open(&path).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let parsed: serde_json::Value = serde_json::from_str(contents.trim()).unwrap();
assert_eq!(parsed["hash"], "1a2b3c");
assert_eq!(parsed["parent_hash"], serde_json::Value::Null);
assert_eq!(parsed["event_type"], "test");
assert!(parsed["data"].is_string());
fs::remove_file(&path).ok();
}
}