Skip to main content

aspero_engine/
versions.rs

1//! Following newer versions to the latest: Cochrane reviews are superseded
2//! `.pub6` → `.pub7` → …, each version's record naming only the next.
3
4use aspero_core::{FieldSet, ProviderId, SourceRecord, Usage, WorkId};
5
6use crate::Engine;
7
8/// Most versions followed per work; a loop guard, never reached in
9/// practice.
10const MAX_HOPS: usize = 20;
11
12#[derive(Debug, Clone, Default)]
13pub struct VersionsReport {
14    /// Per work, in order: the latest version found, if the work has a
15    /// newer one. When the chain breaks (a version can't be looked up), the
16    /// last one found.
17    pub latest: Vec<Option<SourceRecord>>,
18    pub usage: Vec<(ProviderId, Usage)>,
19}
20
21impl Engine {
22    /// The latest version of each work its record says is superseded
23    /// ([`Work::newer_version`](aspero_core::Work::newer_version)), asking
24    /// the providers that report notices. One batched lookup per step along
25    /// the chains.
26    pub async fn latest_versions(&self, works: &[&SourceRecord]) -> VersionsReport {
27        let mut latest: Vec<Option<SourceRecord>> = vec![None; works.len()];
28        let mut usage = Vec::new();
29        let Some(entry) = self
30            .entries
31            .iter()
32            .find(|e| e.provider.capabilities().notices)
33        else {
34            return VersionsReport { latest, usage };
35        };
36        // (work index, next version, versions seen) per chain still going.
37        let mut frontier: Vec<(usize, WorkId, Vec<WorkId>)> = works
38            .iter()
39            .enumerate()
40            .filter_map(|(i, r)| Some((i, r.work.newer_version()?.clone(), r.ids.clone())))
41            .collect();
42        for _ in 0..MAX_HOPS {
43            if frontier.is_empty() {
44                break;
45            }
46            let ids: Vec<WorkId> = frontier.iter().map(|(_, id, _)| id.clone()).collect();
47            let answers = self.lookup_at(entry, &ids, FieldSet::ALL, &mut usage).await;
48            let mut next = Vec::new();
49            for ((i, id, mut seen), (_, record)) in frontier.into_iter().zip(answers) {
50                let Some(record) = record else { continue };
51                seen.push(id);
52                if let Some(newer) = record.work.newer_version()
53                    && !seen.contains(newer)
54                {
55                    next.push((i, newer.clone(), seen));
56                }
57                latest[i] = Some(record);
58            }
59            frontier = next;
60        }
61        VersionsReport { latest, usage }
62    }
63}