Skip to main content

energyml_cache/
lib.rs

1//! What a package keeps of itself, layer by layer.
2//!
3//! A face - the front end's session, the command line, Python - holds one
4//! [`Cache`] per open store. Every layer is optional and lazy: [`Cache::off`]
5//! keeps nothing, and a function that can use a cache takes one rather than
6//! existing twice, so the fast answer and the slow one are the same code
7//! (ADR-0010).
8//!
9//! This crate holds what a WebAssembly front end used to keep for
10//! itself alone:
11//!
12//! | layer | kept | module |
13//! |---|---|---|
14//! | `index` | the package's links and property sheets, exact for the store's revision | [`index`] |
15//! | `colours` | the colour walks of the objects asked, and the set they read | [`colours`] |
16//! | `attachments` | the listings of what is attached to an object | [`Cache::attachments_of`] |
17//!
18//! # One implementation, a cache that keeps nothing
19//!
20//! A layer that is off is not a second code path: the same function runs, over
21//! a slot that is thrown away when the call returns. A question to
22//! [`Cache::off`] therefore builds the index it needs and drops it - slower,
23//! never different. `tests/` holds the proof: every answer, cache on and cache
24//! off, side by side.
25//!
26//! The colours and the attachments are **derived from the index** (they read
27//! its links, and what its updates disturb is what they drop), so turning
28//! either on turns the index on ([`Layers::normalised`]).
29//!
30//! # What this crate does not hold yet
31//!
32//! Identity is still the uuid and the uri the store gives (`ObjKey` is not
33//! there yet), and there is no dependency registry (`Ledger`). The
34//! creation pieces and the section view's work are still held by
35//! that front end.
36//!
37//! The tests of `energyml-cache` are one binary (one crate, one test
38//! binary, ADR-0016): a new file is a `mod` line in `tests/main.rs`, or it does
39//! not run.
40
41use std::cell::RefCell;
42
43use energyml_domain::attachments::{self, CacheStats, Listing};
44use energyml_domain::package_index::{PackageIndex, Progress};
45use energyml_storage::EpcStore;
46
47pub mod colours;
48pub mod index;
49
50pub use index::{Consumer, Disturbed, IndexSlot};
51
52/// What can go wrong asking a [`Cache`] a question.
53#[derive(Debug, thiserror::Error)]
54pub enum Error {
55    /// The store refused an object.
56    #[error("{0}")]
57    Store(#[from] energyml_storage::Error),
58    /// A domain reader refused.
59    #[error("{0}")]
60    Domain(#[from] energyml_domain::Error),
61}
62
63/// Which layers are on. Every one is off by default and turned on by name.
64///
65/// ```
66/// # use energyml_cache::Layers;
67/// let layers = Layers::none().with_index().with_colours();
68/// assert!(layers.index && layers.colours && !layers.attachments);
69/// ```
70#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
71pub struct Layers {
72    /// The package index.
73    pub index: bool,
74    /// The colour walks (derived from the index).
75    pub colours: bool,
76    /// The attachment listings (derived from the index).
77    pub attachments: bool,
78}
79
80impl Layers {
81    /// Nothing on.
82    pub const fn none() -> Self {
83        Self {
84            index: false,
85            colours: false,
86            attachments: false,
87        }
88    }
89
90    /// Every layer there is - what a long-lived session wants.
91    pub const fn all() -> Self {
92        Self {
93            index: true,
94            colours: true,
95            attachments: true,
96        }
97    }
98
99    /// Turn the index on.
100    pub const fn with_index(mut self) -> Self {
101        self.index = true;
102        self
103    }
104
105    /// Turn the colour walks on.
106    pub const fn with_colours(mut self) -> Self {
107        self.colours = true;
108        self
109    }
110
111    /// Turn the attachment listings on.
112    pub const fn with_attachments(mut self) -> Self {
113        self.attachments = true;
114        self
115    }
116
117    /// The layers with what they stand on turned on too: a layer derived from
118    /// the index has no index to be exact for when the index is off.
119    pub const fn normalised(mut self) -> Self {
120        self.index = self.index || self.colours || self.attachments;
121        self
122    }
123}
124
125/// What a [`Cache`] holds right now - a count, so that a test can say "nothing"
126/// without a clock.
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
128pub struct Held {
129    /// Objects the index holds a node for.
130    pub index_nodes: usize,
131    /// Colour walks held.
132    pub colour_walks: usize,
133    /// Attachment listings held.
134    pub listings: usize,
135}
136
137impl Held {
138    /// Whether nothing at all is held.
139    pub fn is_empty(&self) -> bool {
140        *self == Held::default()
141    }
142}
143
144/// What a package keeps of itself: one per open store, with the layers it was
145/// made with.
146///
147/// Methods take `&self` - a face's methods do, and wasm is single-threaded -
148/// and no borrow is held across a call that could come back in: a question
149/// asked while another is being answered finds the slot held and answers from
150/// one of its own, as [`IndexSlot::with`] does.
151pub struct Cache {
152    layers: Layers,
153    pub(crate) index: IndexSlot,
154    pub(crate) colours: RefCell<colours::ColourSlot>,
155    listings: RefCell<attachments::Cache>,
156}
157
158impl Cache {
159    /// A cache that keeps nothing: every lookup is a branch, every answer is
160    /// computed from the store.
161    pub fn off() -> Self {
162        Self::new(Layers::none())
163    }
164
165    /// A cache with these layers on (see [`Layers::normalised`]).
166    pub fn new(layers: Layers) -> Self {
167        Self {
168            layers: layers.normalised(),
169            index: IndexSlot::default(),
170            colours: RefCell::default(),
171            listings: RefCell::default(),
172        }
173    }
174
175    /// Which layers are on.
176    pub fn layers(&self) -> Layers {
177        self.layers
178    }
179
180    /// What is held now.
181    pub fn held(&self) -> Held {
182        Held {
183            index_nodes: self.index.node_count().unwrap_or(0),
184            colour_walks: self.colours.try_borrow().map(|slot| slot.len()).unwrap_or(0),
185            listings: self.listings.try_borrow().map(|cache| cache.len()).unwrap_or(0),
186        }
187    }
188
189    // -- the index ----------------------------------------------------------
190
191    /// Run `f` on an index that is exact for `store` - **the invalidation rule
192    /// of ADR-0010** (see [`index`]). With the index layer off, the index is
193    /// built for this call and dropped after it.
194    pub fn with_index<R>(&self, store: &EpcStore, f: impl FnOnce(&PackageIndex) -> R) -> R {
195        if self.layers.index {
196            self.index.with(store, f)
197        } else {
198            f(&PackageIndex::build(store, store.revision()))
199        }
200    }
201
202    /// Advance the index's build by `n` objects, and say how far it is. A cache
203    /// whose index layer is off builds nothing between questions and answers
204    /// *nothing to do*.
205    pub fn index_step(&self, store: &EpcStore, n: usize) -> Progress {
206        if self.layers.index {
207            self.index.step(store, n)
208        } else {
209            Progress { done: 0, total: 0 }
210        }
211    }
212
213    /// The slot holding the index - for a face's negative controls (`tamper`,
214    /// `revision`, `take_disturbed`). It exists whatever the layers; only
215    /// [`Cache::with_index`] decides whether it is used.
216    #[doc(hidden)]
217    pub fn index_slot(&self) -> &IndexSlot {
218        &self.index
219    }
220
221    /// Say that the store is **not the whole package** - see
222    /// [`IndexSlot::set_partial`].
223    pub fn set_partial(&self, partial: bool) {
224        self.index.set_partial(partial);
225    }
226
227    // -- the attachments ----------------------------------------------------
228
229    /// The listing of what is attached to each of `uuids`, in order.
230    ///
231    /// **Why the listing cache is held from `energyml-domain`, not moved**:
232    /// what a listing reads, and what drops it, is the domain's algorithm
233    /// (`attachments::Cache` and its own tests); this crate decides only
234    /// whether a cache is kept between questions, and which account of the
235    /// index's disturbances it reads. With the layer off the cache is a fresh
236    /// one, dropped after the call.
237    pub fn attachments_of(&self, store: &EpcStore, uuids: &[&str]) -> Vec<Listing> {
238        let mut fresh = attachments::Cache::new();
239        let mut held = self.listings.try_borrow_mut();
240        // A question asked while another is being answered finds the slot held:
241        // it answers from a cache of its own rather than panic.
242        let (cache, kept): (&mut attachments::Cache, bool) = match held.as_deref_mut() {
243            Ok(cache) if self.layers.attachments => (cache, true),
244            _ => (&mut fresh, false),
245        };
246        self.with_index(store, |index| {
247            if kept {
248                // The index is exact for the store here: drop the listings that
249                // read what its update disturbed, and nothing else.
250                let dirty = self.index.take_disturbed(Consumer::Attachments);
251                if dirty.everything {
252                    cache.clear();
253                } else {
254                    cache.disturb(&dirty.uuids);
255                }
256            }
257            cache.listings(store, store.files_revision(), index, uuids)
258        })
259    }
260
261    /// What the listing cache did so far - listings built and dropped, files
262    /// read. **Work, not time.**
263    pub fn attachment_stats(&self) -> CacheStats {
264        self.listings
265            .try_borrow()
266            .map(|cache| cache.stats())
267            .unwrap_or_default()
268    }
269
270    /// Whether the cache holds the listing of this object.
271    #[doc(hidden)]
272    pub fn attachment_listing_held(&self, uuid: &str) -> bool {
273        self.listings
274            .try_borrow()
275            .map(|cache| cache.holds(uuid))
276            .unwrap_or(false)
277    }
278}