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