1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use tokio::fs::File;
7use tokio::sync::Mutex;
8use uuid::Uuid;
9
10use crate::error::{FlowError, Result};
11use crate::jsonl::{append_jsonl_record, load_jsonl, repair_jsonl_tail, LoadedJsonl};
12use crate::model::{project_run, validate_run_id, FlowEvent, FlowEventEnvelope};
13
14use super::{
15 retention::{plan_history_retention, required_linked_flow_run_id, FlowHistoryRetentionPolicy},
16 FlowEventStore,
17};
18
19#[derive(Debug, Clone)]
29pub struct LocalFileEventStore {
30 root: PathBuf,
31 lock: Arc<Mutex<()>>,
32}
33
34impl LocalFileEventStore {
35 pub fn new(root: impl Into<PathBuf>) -> Self {
37 Self {
38 root: root.into(),
39 lock: Arc::new(Mutex::new(())),
40 }
41 }
42
43 pub fn root(&self) -> &Path {
45 &self.root
46 }
47
48 fn run_path(&self, run_id: &str) -> Result<PathBuf> {
49 if !is_safe_run_id(run_id) {
50 return Err(FlowError::Store(format!(
51 "run id {run_id:?} is not safe for local file storage"
52 )));
53 }
54 Ok(self.root.join(format!("{run_id}.jsonl")))
55 }
56
57 async fn load_inner(
58 &self,
59 run_id: &str,
60 missing_is_empty: bool,
61 ) -> Result<LoadedJsonl<FlowEventEnvelope>> {
62 let path = self.run_path(run_id)?;
63 let file = match File::open(&path).await {
64 Ok(file) => file,
65 Err(err) if err.kind() == std::io::ErrorKind::NotFound && missing_is_empty => {
66 return Ok(LoadedJsonl::empty());
67 }
68 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
69 return Err(FlowError::RunNotFound(run_id.to_string()));
70 }
71 Err(err) => return Err(FlowError::Io(err)),
72 };
73 let loaded: LoadedJsonl<FlowEventEnvelope> = load_jsonl(file, &path, "event").await?;
74 for (index, envelope) in loaded.records.iter().enumerate() {
75 if envelope.run_id != run_id {
76 return Err(FlowError::Store(format!(
77 "event line {} in {} belongs to run {}, not {run_id}",
78 index + 1,
79 path.display(),
80 envelope.run_id
81 )));
82 }
83 }
84 Ok(loaded)
85 }
86
87 async fn list_inner(
88 &self,
89 run_id: &str,
90 missing_is_empty: bool,
91 ) -> Result<Vec<FlowEventEnvelope>> {
92 Ok(self.load_inner(run_id, missing_is_empty).await?.records)
93 }
94
95 fn validate_existing_log(&self, run_id: &str, events: &[FlowEventEnvelope]) -> Result<()> {
96 if events.is_empty() {
97 return Ok(());
98 }
99 project_run(run_id, events)?;
100 Ok(())
101 }
102
103 async fn append_inner(&self, run_id: &str, event: FlowEvent) -> Result<FlowEventEnvelope> {
104 tokio::fs::create_dir_all(&self.root).await?;
105 self.ensure_linked_flow_run_exists(&event).await?;
106
107 let LoadedJsonl {
108 records: events,
109 tail_repair,
110 } = self.load_inner(run_id, true).await?;
111 self.validate_existing_log(run_id, &events)?;
112 let envelope = FlowEventEnvelope {
113 run_id: run_id.to_string(),
114 sequence: events.last().map_or(1, |event| event.sequence + 1),
115 event_id: Uuid::new_v4(),
116 timestamp: Utc::now(),
117 event,
118 };
119
120 let path = self.run_path(run_id)?;
121 repair_jsonl_tail(&path, tail_repair).await?;
122 append_jsonl_record(&path, &envelope).await?;
123 Ok(envelope)
124 }
125
126 async fn append_if_sequence_inner(
127 &self,
128 run_id: &str,
129 expected_sequence: u64,
130 event: FlowEvent,
131 ) -> Result<FlowEventEnvelope> {
132 tokio::fs::create_dir_all(&self.root).await?;
133 self.ensure_linked_flow_run_exists(&event).await?;
134
135 let LoadedJsonl {
136 records: events,
137 tail_repair,
138 } = self.load_inner(run_id, true).await?;
139 self.validate_existing_log(run_id, &events)?;
140 let actual_sequence = events.last().map_or(0, |event| event.sequence);
141 if actual_sequence != expected_sequence {
142 return Err(FlowError::EventConflict {
143 run_id: run_id.to_string(),
144 expected_sequence,
145 actual_sequence,
146 });
147 }
148
149 let envelope = FlowEventEnvelope {
150 run_id: run_id.to_string(),
151 sequence: actual_sequence + 1,
152 event_id: Uuid::new_v4(),
153 timestamp: Utc::now(),
154 event,
155 };
156
157 let path = self.run_path(run_id)?;
158 repair_jsonl_tail(&path, tail_repair).await?;
159 append_jsonl_record(&path, &envelope).await?;
160 Ok(envelope)
161 }
162
163 async fn ensure_linked_flow_run_exists(&self, event: &FlowEvent) -> Result<()> {
164 let Some(linked_run_id) = required_linked_flow_run_id(event) else {
165 return Ok(());
166 };
167 let events = self.list_inner(linked_run_id, false).await?;
168 if events.is_empty() {
169 return Err(FlowError::RunNotFound(linked_run_id.to_string()));
170 }
171 self.validate_existing_log(linked_run_id, &events)
172 }
173
174 async fn list_run_ids_inner(&self) -> Result<Vec<String>> {
175 let mut ids = Vec::new();
176
177 let mut dir = match tokio::fs::read_dir(&self.root).await {
178 Ok(dir) => dir,
179 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(ids),
180 Err(err) => return Err(FlowError::Io(err)),
181 };
182
183 while let Some(entry) = dir.next_entry().await? {
184 let path = entry.path();
185 if path.extension().and_then(|ext| ext.to_str()) != Some("jsonl") {
186 continue;
187 }
188 let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
189 continue;
190 };
191 if is_safe_run_id(stem) {
192 ids.push(stem.to_string());
193 }
194 }
195
196 ids.sort();
197 Ok(ids)
198 }
199
200 pub async fn prune_terminal_runs_older_than(
208 &self,
209 terminal_before: DateTime<Utc>,
210 ) -> Result<Vec<String>> {
211 let _guard = self.lock.lock().await;
212 let mut histories = BTreeMap::new();
213 for run_id in self.list_run_ids_inner().await? {
214 let events = self.list_inner(&run_id, false).await?;
215 self.validate_existing_log(&run_id, &events)?;
216 histories.insert(run_id, events);
217 }
218 let mut plan = plan_history_retention(
219 &histories,
220 &BTreeSet::new(),
221 &FlowHistoryRetentionPolicy::new(terminal_before),
222 "local file",
223 )?;
224
225 let mut removed = Vec::new();
226 for run_id in &plan.deletable_run_ids {
227 let path = self.run_path(run_id)?;
228 match tokio::fs::remove_file(&path).await {
229 Ok(()) => removed.push(run_id.clone()),
230 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
231 Err(err) => return Err(FlowError::Io(err)),
232 }
233 }
234
235 plan.report.deleted_run_ids = removed;
236 Ok(plan.report.deleted_run_ids)
237 }
238}
239
240#[async_trait]
241impl FlowEventStore for LocalFileEventStore {
242 async fn append(&self, run_id: &str, event: FlowEvent) -> Result<FlowEventEnvelope> {
243 let _guard = self.lock.lock().await;
244 self.append_inner(run_id, event).await
245 }
246
247 async fn append_if_sequence(
248 &self,
249 run_id: &str,
250 expected_sequence: u64,
251 event: FlowEvent,
252 ) -> Result<FlowEventEnvelope> {
253 let _guard = self.lock.lock().await;
254 self.append_if_sequence_inner(run_id, expected_sequence, event)
255 .await
256 }
257
258 async fn list(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>> {
259 let _guard = self.lock.lock().await;
260 self.list_inner(run_id, false).await
261 }
262
263 async fn list_run_ids(&self) -> Result<Vec<String>> {
264 let _guard = self.lock.lock().await;
265 self.list_run_ids_inner().await
266 }
267}
268
269fn is_safe_run_id(run_id: &str) -> bool {
270 validate_run_id(run_id).is_ok()
271}