Skip to main content

repolith_core/
cache.rs

1//! Cache trait — abstract storage for build events.
2//!
3//! Defines the contract that any backend (sqlite, in-memory, redis…) must
4//! satisfy. Concrete implementations live in dependent crates (e.g.
5//! `repolith-cache::SqliteCache`). The trait lives here in `repolith-core`
6//! so consumers like [`crate::plan::Plan`] can depend on the abstraction
7//! without pulling in any specific backend.
8
9use crate::types::{ActionId, BuildEvent, BuildRecord};
10use async_trait::async_trait;
11use thiserror::Error;
12
13/// Errors that can occur during cache operations.
14#[derive(Debug, Error)]
15pub enum CacheError {
16    /// An I/O error occurred while accessing the cache.
17    #[error("io: {0}")]
18    Io(#[from] std::io::Error),
19    /// A backend-specific error occurred (driver-level, schema, lock, …).
20    #[error("backend: {0}")]
21    Backend(String),
22}
23
24/// A specialized `Result` type for cache operations.
25pub type Result<T> = std::result::Result<T, CacheError>;
26
27/// Persistent store for `BuildEvent`s, keyed by [`ActionId`].
28///
29/// The cache lets the [planner](crate::plan::Plan) compare current input
30/// hashes against the last recorded build, and lets the orchestrator persist
31/// new events as they fire.
32#[async_trait]
33pub trait Cache: Send + Sync {
34    /// Last recorded event for `id`, or `None` if the action has never run.
35    async fn last_build(&self, id: &ActionId) -> Option<BuildEvent>;
36
37    /// Same as [`Self::last_build`], plus whatever storage metadata the
38    /// backend tracks — currently just the write time.
39    ///
40    /// This is the query path for human-facing output (`repolith status`);
41    /// planning uses [`Self::last_build`], because *when* a build happened
42    /// must never influence *whether* it is stale — only its input hash may.
43    ///
44    /// The default implementation delegates to [`Self::last_build`] and
45    /// reports `recorded_at: None`, so backends that do not track write
46    /// times need not implement anything.
47    async fn last_record(&self, id: &ActionId) -> Option<BuildRecord> {
48        self.last_build(id).await.map(|event| BuildRecord {
49            event,
50            recorded_at: None,
51        })
52    }
53
54    /// Persist `event`. After success, [`Self::last_build`] for the event's
55    /// id must return this event.
56    async fn record(&mut self, event: BuildEvent) -> Result<()>;
57
58    /// Persist `events` atomically — either every event lands or none.
59    ///
60    /// The orchestrator calls this at the end of each layer so a crash
61    /// mid-batch doesn't leave half the layer marked Success and the
62    /// other half un-persisted (which would re-run those actions on
63    /// the next sync, contradicting the cached Success).
64    ///
65    /// The default implementation is a non-atomic loop over [`Self::record`]
66    /// that suffices for in-memory backends; persistent backends
67    /// (`SQLite`, etc.) should override with a transaction.
68    async fn record_batch(&mut self, events: Vec<BuildEvent>) -> Result<()> {
69        for event in events {
70            self.record(event).await?;
71        }
72        Ok(())
73    }
74}