Skip to main content

piw/
source.rs

1//! Database-backed run views for the local TUI and replay server.
2
3use crate::protocol::PatchOp;
4use crate::state::reader::{list_runs, read_run, LoadedRun};
5use crate::state::types::{
6    DefinitionSnapshot, Manifest, RunState, SessionBinding, SessionCapture, SessionEntryRecord,
7    SessionEventRecord,
8};
9use anyhow::Result;
10use serde_json::{json, Value};
11use std::collections::BTreeMap;
12use std::path::{Path, PathBuf};
13
14pub struct RunEntry {
15    pub dir: PathBuf,
16    pub manifest: Manifest,
17    pub manifest_raw: Value,
18    pub workflow: Value,
19    pub state_raw: Value,
20    pub events: Vec<Value>,
21    pub session_binding: Option<Value>,
22    pub session_entries: Vec<Value>,
23    pub session_events: Vec<Value>,
24    pub session_events_malformed: bool,
25    pub session_events_torn_tail: bool,
26    pub session_capture: Option<Value>,
27    pub state: RunState,
28    pub snapshot: Option<DefinitionSnapshot>,
29    pub live: bool,
30    pub possibly_interrupted: bool,
31    pub revision: u64,
32}
33
34impl RunEntry {
35    pub fn open(database_path: &Path, run_id: &str) -> Result<Self> {
36        Self::from_loaded(database_path, read_run(database_path, run_id)?, 1)
37    }
38
39    fn from_loaded(database_path: &Path, loaded: LoadedRun, revision: u64) -> Result<Self> {
40        let manifest_raw = serde_json::to_value(&loaded.manifest)?;
41        let workflow = loaded
42            .snapshot
43            .as_ref()
44            .map(serde_json::to_value)
45            .transpose()?
46            .unwrap_or(Value::Null);
47        let state_raw = serde_json::to_value(&loaded.state)?;
48        let events = loaded
49            .trace
50            .iter()
51            .map(serde_json::to_value)
52            .collect::<Result<Vec<_>, _>>()?;
53        let session_binding = loaded
54            .session_binding
55            .as_ref()
56            .map(serde_json::to_value)
57            .transpose()?;
58        let session_entries = loaded
59            .session_entries
60            .iter()
61            .map(serde_json::to_value)
62            .collect::<Result<Vec<_>, _>>()?;
63        let session_events = loaded
64            .session_events
65            .iter()
66            .map(serde_json::to_value)
67            .collect::<Result<Vec<_>, _>>()?;
68        let session_capture = loaded
69            .session_capture
70            .as_ref()
71            .map(serde_json::to_value)
72            .transpose()?;
73        let live = !loaded.state.status.is_terminal();
74        Ok(Self {
75            dir: database_path.to_path_buf(),
76            manifest: loaded.manifest,
77            manifest_raw,
78            workflow,
79            state_raw,
80            events,
81            session_binding,
82            session_entries,
83            session_events,
84            session_events_malformed: false,
85            session_events_torn_tail: false,
86            session_capture,
87            state: loaded.state,
88            snapshot: loaded.snapshot,
89            live,
90            possibly_interrupted: loaded.possibly_interrupted,
91            revision,
92        })
93    }
94
95    fn session_value(&self) -> Value {
96        if self.session_binding.is_none()
97            && self.session_entries.is_empty()
98            && self.session_events.is_empty()
99            && self.session_capture.is_none()
100        {
101            Value::Null
102        } else {
103            json!({
104                "binding": self.session_binding,
105                "entries": self.session_entries,
106                "events": self.session_events,
107                "eventsMalformed": self.session_events_malformed,
108                "eventsTornTail": self.session_events_torn_tail,
109                "capture": self.session_capture,
110            })
111        }
112    }
113
114    pub fn view(&self) -> Value {
115        json!({
116            "manifest": self.manifest_raw,
117            "workflow": self.workflow,
118            "state": self.state_raw,
119            "events": self.events,
120            "session": self.session_value(),
121            "live": self.live,
122            "possiblyInterrupted": self.possibly_interrupted,
123        })
124    }
125
126    pub fn summary(&self) -> Value {
127        json!({
128            "manifest": self.manifest_raw,
129            "live": self.live,
130            "possiblyInterrupted": self.possibly_interrupted,
131        })
132    }
133
134    fn refresh(&mut self) -> Option<Vec<PatchOp>> {
135        let next = RunEntry::open(&self.dir, &self.manifest.run_id).ok()?;
136        let old_view = self.view();
137        let next_view = next.view();
138        if old_view == next_view {
139            return None;
140        }
141        let revision = self.revision + 1;
142        *self = Self { revision, ..next };
143        let mut patch = Vec::new();
144        for key in [
145            "manifest",
146            "workflow",
147            "state",
148            "events",
149            "session",
150            "live",
151            "possiblyInterrupted",
152        ] {
153            if old_view.get(key) != next_view.get(key) {
154                patch.push(PatchOp::Replace {
155                    path: format!("/{key}"),
156                    value: next_view.get(key).cloned().unwrap_or(Value::Null),
157                });
158            }
159        }
160        Some(patch)
161    }
162}
163
164pub struct RunSource {
165    database_path: PathBuf,
166    runs: BTreeMap<String, RunEntry>,
167    single_run_id: Option<String>,
168}
169
170pub struct RefreshOutcome {
171    pub patches: Vec<(String, u64, Vec<PatchOp>)>,
172    pub listing_changed: bool,
173}
174
175impl RunSource {
176    pub fn new(database_path: &Path) -> Self {
177        let mut source = Self {
178            database_path: database_path.to_path_buf(),
179            runs: BTreeMap::new(),
180            single_run_id: None,
181        };
182        source.scan();
183        source
184    }
185
186    pub fn single(database_path: &Path, run_id: &str) -> Result<Self> {
187        let entry = RunEntry::open(database_path, run_id)?;
188        let mut runs = BTreeMap::new();
189        runs.insert(run_id.to_string(), entry);
190        Ok(Self {
191            database_path: database_path.to_path_buf(),
192            runs,
193            single_run_id: Some(run_id.to_string()),
194        })
195    }
196
197    pub fn database_path(&self) -> &Path {
198        &self.database_path
199    }
200
201    pub fn get(&self, run_id: &str) -> Option<&RunEntry> {
202        self.runs.get(run_id)
203    }
204
205    pub fn ordered_run_ids(&self) -> Vec<String> {
206        let mut entries: Vec<&RunEntry> = self.runs.values().collect();
207        entries.sort_by(|a, b| {
208            b.manifest
209                .started_at
210                .cmp(&a.manifest.started_at)
211                .then_with(|| b.manifest.run_id.cmp(&a.manifest.run_id))
212        });
213        entries
214            .into_iter()
215            .map(|entry| entry.manifest.run_id.clone())
216            .collect()
217    }
218
219    pub fn summaries(&self) -> Vec<Value> {
220        self.ordered_run_ids()
221            .iter()
222            .filter_map(|id| self.runs.get(id))
223            .map(RunEntry::summary)
224            .collect()
225    }
226
227    pub fn scan(&mut self) -> bool {
228        if self.single_run_id.is_some() {
229            return false;
230        }
231        let found = list_runs(&self.database_path);
232        let mut changed = false;
233        let mut seen = std::collections::HashSet::new();
234        for (run_id, _) in found {
235            seen.insert(run_id.clone());
236            if !self.runs.contains_key(&run_id) {
237                if let Ok(entry) = RunEntry::open(&self.database_path, &run_id) {
238                    self.runs.insert(run_id, entry);
239                    changed = true;
240                }
241            }
242        }
243        let stale: Vec<String> = self
244            .runs
245            .keys()
246            .filter(|id| !seen.contains(*id))
247            .cloned()
248            .collect();
249        for id in stale {
250            self.runs.remove(&id);
251            changed = true;
252        }
253        changed
254    }
255
256    pub fn refresh_all(&mut self) -> RefreshOutcome {
257        let mut listing_changed = self.scan();
258        let mut patches = Vec::new();
259        for (run_id, entry) in &mut self.runs {
260            let live_before = entry.live;
261            if let Some(patch) = entry.refresh() {
262                patches.push((run_id.clone(), entry.revision, patch));
263                if live_before != entry.live {
264                    listing_changed = true;
265                }
266            }
267        }
268        RefreshOutcome {
269            patches,
270            listing_changed,
271        }
272    }
273}
274
275#[allow(dead_code)]
276fn _retain_public_types(
277    _binding: Option<SessionBinding>,
278    _entries: Vec<SessionEntryRecord>,
279    _events: Vec<SessionEventRecord>,
280    _capture: Option<SessionCapture>,
281) {
282}