artifactize_core/monitor/
loader.rs1use super::{Node, Scope, catalog, model};
7use crate::{
8 store::{self, RequestView, RunSummary, RunView, reader::Reader},
9 types::RunId,
10};
11use std::{path::PathBuf, sync::Arc};
12use time::OffsetDateTime;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Page {
17 pub scope: Scope,
18 pub open: Option<RunId>,
20 pub offset: u32,
21 pub tail: bool,
22 pub limit: u32,
23}
24
25pub fn older_offset(offset: u32, limit: u32, shown: u32) -> Option<u32> {
27 (shown >= limit).then(|| offset.saturating_add(limit))
28}
29
30pub fn newer_offset(offset: u32, limit: u32) -> Option<u32> {
32 (offset > 0).then(|| offset.saturating_sub(limit))
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Selection {
38 pub state: PathBuf,
39 pub page: Page,
40 pub revision: u64,
41}
42
43pub struct Input {
45 pub state: PathBuf,
46 pub initial: Option<PathBuf>,
48 pub catalog: catalog::Catalog,
49 pub initialized: bool,
51 pub page: Page,
52 pub full: bool,
54 pub displayed: Option<RunId>,
56 pub runs: Option<Vec<RunSummary>>,
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
62pub struct Origin {
63 identity: Option<crate::types::StateId>,
64 database: Option<crate::platform::FileIdentity>,
65}
66impl Origin {
67 pub fn replaces(&self, previous: &Self) -> bool {
70 previous.database.is_some() && previous.database != self.database
71 || previous.identity.is_some() && previous.identity != self.identity
72 }
73 pub fn identity(&self) -> Option<&crate::types::StateId> {
74 self.identity.as_ref()
75 }
76 pub fn read(state: &std::path::Path) -> Result<Self, String> {
80 let path = state.join(store::DATABASE);
81 let database =
82 match crate::platform::open_no_follow(std::fs::File::options().read(true), &path) {
83 Ok(file) => Some(crate::platform::file_identity(&file).map_err(|e| e.to_string())?),
84 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
85 Err(error) => return Err(error.to_string()),
86 };
87 Ok(Self {
88 identity: store::read_state_id(state)?,
89 database,
90 })
91 }
92}
93
94pub struct Snapshot {
96 pub origin: Origin,
97 pub catalog: catalog::Catalog,
98 pub scope: Scope,
99 pub runs: Vec<RunSummary>,
100 pub selected_id: Option<RunId>,
101 pub selected: Option<(RunView, Vec<RequestView>, Arc<Vec<Node>>)>,
103}
104
105#[derive(Default)]
108pub struct Loader {
109 reader: Reader,
110 reset_git: bool,
112}
113impl Loader {
114 #[cfg(any(test, feature = "test-support"))]
116 pub fn traced_statements(&self) -> Vec<String> {
117 self.reader.traced_statements()
118 }
119
120 pub fn load(&mut self, mut input: Input) -> Result<Snapshot, String> {
121 if self.reset_git {
122 input.catalog.reset_git();
123 }
124 let result = load(&mut self.reader, input);
125 if result.is_ok() {
126 self.reset_git = false;
127 }
128 result
129 }
130 pub fn reset(&mut self) {
132 self.reader.reset();
133 self.reset_git = true;
134 }
135}
136
137pub struct Completed {
138 pub selection: Selection,
139 pub loader: Loader,
140 pub result: Result<Snapshot, String>,
141 #[cfg(any(test, feature = "test-support"))]
143 pub queries: Vec<String>,
144}
145impl Completed {
146 pub fn run(selection: Selection, mut loader: Loader, input: Input) -> Self {
148 #[cfg(any(test, feature = "test-support"))]
149 loader.traced_statements();
150 let result = loader.load(input);
151 #[cfg(any(test, feature = "test-support"))]
152 let queries = loader.traced_statements();
153 Self {
154 selection,
155 loader,
156 result,
157 #[cfg(any(test, feature = "test-support"))]
158 queries,
159 }
160 }
161}
162
163fn load(reader: &mut Reader, input: Input) -> Result<Snapshot, String> {
164 let mut catalog = input.catalog;
165 let mut scope = input.page.scope.clone();
166 if !input
168 .state
169 .join(store::DATABASE)
170 .try_exists()
171 .map_err(|error| error.to_string())?
172 {
173 store::check_probe_files(&input.state)?;
174 catalog.update(&[], input.initial.as_deref());
175 if !input.initialized {
176 scope = catalog.initial(input.initial.as_deref());
177 }
178 reader.reset();
179 return Ok(Snapshot {
180 origin: Origin::default(),
181 catalog,
182 scope,
183 runs: Vec::new(),
184 selected_id: None,
185 selected: None,
186 });
187 }
188 let saved = reader.snapshot_optional(&input.state, |db, cache, state| {
189 let reuse_page = input.runs.is_some()
190 && !input.full
191 && input.initialized
192 && cache.has_catalog()
193 && !cache.changed();
194 if input.initialized {
195 catalog.selected = Some(scope.clone());
196 }
197 if input.full || !input.initialized || !cache.has_catalog() || cache.changed() {
198 let facts = cache.catalog(db, state)?;
199 if input.initialized {
200 catalog.selected = Some(scope.clone());
201 }
202 catalog.update(&facts, input.initial.as_deref());
203 if !input.initialized {
204 scope = catalog.initial(input.initial.as_deref());
205 catalog.selected = Some(scope.clone());
206 catalog.update(&facts, input.initial.as_deref());
207 }
208 }
209 if !input.full && input.initialized {
210 catalog.rebuild();
211 }
212 let paths = catalog.paths(&scope);
213 let runs = if reuse_page {
214 input.runs.clone().expect("accepted same-database page")
215 } else {
216 cache.page(
217 db,
218 state,
219 paths.as_deref(),
220 input.page.limit,
221 input.page.offset,
222 input.page.open.as_ref(),
223 )?
224 };
225 let selected = input
226 .page
227 .open
228 .as_ref()
229 .and_then(|id| runs.iter().find(|run| &run.id == id))
230 .or_else(|| {
231 if input.page.tail {
232 runs.iter().take(input.page.limit as usize).next_back()
233 } else {
234 runs.first()
235 }
236 });
237 let selected_id = selected.map(|run| run.id.clone());
238 let selected = match selected.filter(|run| run.readable()) {
239 Some(run) => cache.selected(
240 db,
241 state,
242 &run.id,
243 Some(run.unreadable.clone()),
244 input.displayed.as_ref() != Some(&run.id),
245 )?,
246 None => None,
247 };
248 Ok((cache.identity(db)?, runs, selected_id, selected))
249 })?;
250 let Some((identity, runs, selected_id, selected)) = saved else {
251 catalog.update(&[], input.initial.as_deref());
252 if !input.initialized {
253 scope = catalog.initial(input.initial.as_deref());
254 }
255 return Ok(Snapshot {
256 origin: Origin {
257 identity: None,
258 database: reader.database(),
259 },
260 catalog,
261 scope,
262 runs: Vec::new(),
263 selected_id: None,
264 selected: None,
265 });
266 };
267 let selected = selected.map(|(run, requests)| {
269 let nodes = Arc::new(model::tree(&run, &requests, OffsetDateTime::now_utc()));
270 (run, requests, nodes)
271 });
272 reader.require_current(&input.state)?;
273 Ok(Snapshot {
274 origin: Origin {
275 identity,
276 database: reader.database(),
277 },
278 catalog,
279 scope,
280 runs,
281 selected_id,
282 selected,
283 })
284}