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}