Skip to main content

artifactize_core/monitor/live/
loader.rs

1//! One consistent read of the live branches and of the Run the Main area shows. Call
2//! [`Loader::load`] on a blocking thread; it never writes. What the change hints since the last
3//! read name is read again: the whole catalog of saved Runs, or the rows of the Runs named, and
4//! the open Run when it is one of them. Git is asked again which worktrees exist on a rescan.
5use super::{Branch, Discovery, Labels, Place, branches};
6use crate::{
7    changes::Changed,
8    monitor::{Node, Origin, model},
9    platform::Footprint,
10    store::{self, CatalogRun, RequestView, RunView, Unreadable, reader::Reader},
11    types::RunId,
12};
13use std::{path::PathBuf, sync::Arc};
14use time::OffsetDateTime;
15
16#[cfg(test)]
17mod tests;
18
19/// What one read needs.
20#[derive(Debug, Clone, Default)]
21pub struct Input {
22    pub state: PathBuf,
23    /// The folder the app was started for: its branch is listed even before its first Run.
24    pub launch: Option<PathBuf>,
25    /// The Run the Main area shows, read in full.
26    pub open: Option<RunId>,
27    /// Read the open Run again even when no hint named it.
28    pub force: bool,
29    /// Ask Git again which worktrees exist.
30    pub rescan: bool,
31    /// What the change hints since the last read named.
32    pub changed: Changed,
33}
34
35/// The open Run: its view, its requests, their projected tree and a share in the Run's memory.
36/// Keep the share with the rest, after it, so the memory goes back once nothing shows the Run.
37pub type Opened = (RunView, Vec<RequestView>, Arc<Vec<Node>>, Footprint);
38
39/// One consistent read.
40pub struct Snapshot {
41    pub origin: Origin,
42    /// The live branches, the most recent Run first.
43    pub branches: Vec<Branch>,
44    /// The project and branch of every readable saved Run.
45    pub labels: Labels,
46    /// The branch of the folder the app was started for, when it lives.
47    pub launch: Option<Place>,
48    /// Saved Run records that cannot be read, and records of the open Run that cannot.
49    pub unreadable: Vec<Unreadable>,
50    /// The open Run when it was read again, `None` when its records did not change since the
51    /// last read (or no Run is open, or it is gone: see [`Snapshot::open`]).
52    pub opened: Option<Opened>,
53    /// The open Run still exists and reads.
54    pub open: bool,
55}
56
57/// A persistent read-only worker with its caches.
58#[derive(Default)]
59pub struct Loader {
60    reader: Reader,
61    discovery: Discovery,
62    /// The catalog last read, kept until a hint names a change.
63    facts: Option<(Arc<Vec<CatalogRun>>, Vec<Unreadable>)>,
64}
65
66impl Loader {
67    pub fn load(&mut self, input: Input) -> Result<Snapshot, String> {
68        if input.rescan {
69            self.discovery.rescan();
70        }
71        let database = input.state.join(store::DATABASE);
72        if !database.try_exists().map_err(|error| error.to_string())? {
73            // Empty state is a normal baseline; its file safety checks still run here.
74            store::check_probe_files(&input.state)?;
75            self.reader.reset();
76            self.facts = None;
77            return Ok(self.empty(&input, Origin::default()));
78        }
79        let facts = &mut self.facts;
80        let saved = self
81            .reader
82            .snapshot_optional(&input.state, |db, cache, state| {
83                let changed = input.changed.or_written(cache.changed());
84                match (facts.as_mut(), &changed) {
85                    (Some((runs, unreadable)), Changed::Runs(ids)) if cache.has_catalog() => {
86                        let ids: Vec<_> = ids.iter().cloned().collect();
87                        let rows = cache.catalog_rows(db, state, &ids)?;
88                        *runs = Arc::new(merged(runs, &ids, rows.runs));
89                        unreadable.retain(|record| !ids.iter().any(|id| record.id == id.as_str()));
90                        unreadable.extend(rows.unreadable);
91                    }
92                    (Some(_), Changed::Nothing) if cache.has_catalog() => {}
93                    _ => {
94                        let runs = cache.catalog(db, state)?;
95                        *facts = Some((Arc::new(runs), cache.unreadable_runs()));
96                    }
97                }
98                let (runs, _) = facts.as_ref().expect("read above");
99                let open = input
100                    .open
101                    .as_ref()
102                    .filter(|id| runs.iter().any(|run| &run.id == *id));
103                if open.is_none() {
104                    // Nothing shows a Run: the one held goes, and with its last share its memory.
105                    cache.close();
106                }
107                // The open Run is read when it is new here or a hint named it.
108                let read = open.filter(|id| input.force || !cache.holds(id) || changed.run(id));
109                let opened = match read {
110                    Some(id) => match cache.selected(db, state, id, input.force) {
111                        Ok(opened) => Ok(opened.map(|(run, requests)| {
112                            let footprint = cache.footprint().expect("the Run just read is held");
113                            (run, requests, footprint)
114                        })),
115                        // The Run's definitions do not decode: it is listed with why, not opened.
116                        Err(store::receipts::Error::Unreadable(record)) => Err(record),
117                        Err(error) => return Err(error),
118                    },
119                    None => Ok(None),
120                };
121                Ok((cache.identity(db)?, open.is_some(), opened))
122            })?;
123        let Some((identity, open, opened)) = saved else {
124            self.facts = None;
125            return Ok(self.empty(&input, Origin::new(None, self.reader.database())));
126        };
127        let (opened, unopened) = match opened {
128            Ok(opened) => (opened, None),
129            Err(record) => (None, Some(record)),
130        };
131        // No read transaction survives heavy projection or Git discovery.
132        let opened = opened.map(|(run, requests, footprint)| {
133            let nodes = Arc::new(model::tree(&run, &requests, OffsetDateTime::now_utc()));
134            (run, requests, nodes, footprint)
135        });
136        let (runs, mut unreadable) = self.facts.clone().expect("read above");
137        if let Some((run, _, _, _)) = &opened {
138            unreadable.extend(run.unreadable.iter().cloned());
139        }
140        unreadable.extend(unopened);
141        let listing = branches(&runs, input.launch.as_deref(), &mut self.discovery);
142        self.reader.require_current(&input.state)?;
143        Ok(Snapshot {
144            origin: Origin::new(identity, self.reader.database()),
145            branches: listing.branches,
146            labels: listing.labels,
147            launch: listing.launch,
148            unreadable,
149            opened,
150            open,
151        })
152    }
153
154    fn empty(&mut self, input: &Input, origin: Origin) -> Snapshot {
155        let listing = branches(&[], input.launch.as_deref(), &mut self.discovery);
156        Snapshot {
157            origin,
158            branches: listing.branches,
159            labels: listing.labels,
160            launch: listing.launch,
161            unreadable: Vec::new(),
162            opened: None,
163            open: false,
164        }
165    }
166}
167
168/// `runs` with the rows of the Runs `ids` replaced by `rows`: a Run without a row is gone, and
169/// a new one is the newest, listed last.
170fn merged(runs: &[CatalogRun], ids: &[RunId], rows: Vec<CatalogRun>) -> Vec<CatalogRun> {
171    let mut rows: std::collections::BTreeMap<_, _> =
172        rows.into_iter().map(|run| (run.id.clone(), run)).collect();
173    let mut merged: Vec<_> = runs
174        .iter()
175        .filter_map(|run| {
176            if ids.contains(&run.id) {
177                rows.remove(&run.id)
178            } else {
179                Some(run.clone())
180            }
181        })
182        .collect();
183    merged.extend(rows.into_values());
184    merged
185}