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}