1use 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}