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