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