Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

crates/mimir-room/src/scope.rs

//! Which part of the log a room is: per repo the consumer ordinal (state as
//! of `seq ≤ N`), plus the universe ring on or off.

use std::collections::BTreeMap;
use std::fmt;

use anyhow::Result;
use mimir_ingest::{EventLog, FoldScope};

/// Why a [`Scope`] does not bind to this log. Typed (wiring map R15): a
/// reader downcasts the `anyhow` error of [`Scope::resolve`] / `Room::open`
/// to tell "nothing of that repo was consumed yet" from a room that failed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScopeError {
    /// The repo has no consumer offset in this log: it was never consumed.
    NotConsumed { repo: String, known: Vec<String> },
    /// The ordinal asked for is past what was consumed.
    PastOffset { repo: String, seq: i64, offset: i64, sha: String },
    /// Ordinals start at 1; 0 is "before the first commit", below is nothing.
    NegativeSeq { repo: String, seq: i64 },
}

impl fmt::Display for ScopeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ScopeError::NotConsumed { repo, known } => {
                write!(f, "repo `{repo}` has no consumer offset in this log (known: {known:?})")
            }
            ScopeError::PastOffset { repo, seq, offset, sha } => {
                write!(f, "repo `{repo}`: seq {seq} is past its offset {offset} ({sha})")
            }
            ScopeError::NegativeSeq { repo, seq } => write!(f, "repo `{repo}`: seq {seq} is negative"),
        }
    }
}

impl std::error::Error for ScopeError {}

/// What the caller asks for. `repos` empty = every repo the log has an
/// offset for; a repo mapped to `None` = its current offset.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Scope {
    pub repos: BTreeMap<String, Option<i64>>,
    /// Fold the universe ring (dependency + toolchain crates) into the room:
    /// walks then continue through dependencies down to the FFI border.
    /// Off, the room holds the repos' own crates and the reference stubs
    /// their edges point at; walks stop at those stubs (the evidence says so).
    pub universe: bool,
}

impl Scope {
    /// Every repo at its current offset, universe off.
    pub fn current() -> Self {
        Self::default()
    }

    /// One repo at `seq` (`None` = its offset).
    pub fn repo(repo: &str, seq: Option<i64>) -> Self {
        Self { repos: [(repo.to_string(), seq)].into(), universe: false }
    }

    pub fn with_universe(mut self, on: bool) -> Self {
        self.universe = on;
        self
    }

    /// Bind to concrete ordinals: unknown repos and ordinals past a repo's
    /// offset are errors (asking for a state that was never consumed is not
    /// an empty answer) — a [`ScopeError`], downcastable from the result.
    pub async fn resolve(&self, log: &EventLog) -> Result<ResolvedScope> {
        let offsets = log.offsets().await?;
        let mut repos = BTreeMap::new();
        let mut shas = BTreeMap::new();
        if self.repos.is_empty() {
            for (r, o) in &offsets {
                repos.insert(r.clone(), o.seq);
                shas.insert(r.clone(), o.sha.clone());
            }
        }
        for (r, want) in &self.repos {
            let Some(o) = offsets.get(r) else {
                return Err(ScopeError::NotConsumed { repo: r.clone(), known: offsets.keys().cloned().collect() }.into());
            };
            let seq = match want {
                Some(n) if *n > o.seq => {
                    return Err(ScopeError::PastOffset { repo: r.clone(), seq: *n, offset: o.seq, sha: o.sha.clone() }.into());
                }
                Some(n) if *n < 0 => return Err(ScopeError::NegativeSeq { repo: r.clone(), seq: *n }.into()),
                Some(n) => *n,
                None => o.seq,
            };
            repos.insert(r.clone(), seq);
            if seq == o.seq {
                shas.insert(r.clone(), o.sha.clone());
            }
        }
        Ok(ResolvedScope { repos, universe: self.universe, offset_shas: shas })
    }
}

/// A scope bound to concrete consumer ordinals.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ResolvedScope {
    pub repos: BTreeMap<String, i64>,
    pub universe: bool,
    /// The offset sha of each repo that sits AT its offset (others are time
    /// travel: their sha is in the ledger).
    pub offset_shas: BTreeMap<String, String>,
}

impl ResolvedScope {
    pub(crate) fn fold(&self) -> FoldScope {
        FoldScope { repos: self.repos.clone(), universe: self.universe }
    }

    /// The cache key part: ordinals + universe (shas follow from them).
    pub(crate) fn key(&self) -> (BTreeMap<String, i64>, bool) {
        (self.repos.clone(), self.universe)
    }
}

impl fmt::Display for ResolvedScope {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let parts: Vec<String> = self
            .repos
            .iter()
            .map(|(r, s)| match self.offset_shas.get(r) {
                Some(sha) => format!("{r}@{s} ({})", &sha[..sha.len().min(10)]),
                None => format!("{r}@{s} (time travel)"),
            })
            .collect();
        write!(f, "{} · universe {}", if parts.is_empty() { "no repos".to_string() } else { parts.join(", ") }, if self.universe { "on" } else { "off" })
    }
}