Skip to main content

onetaskgraph_plugin_api/
cache.rs

1//! What a source tells the store's local cache of remote sources.
2//!
3//! The cache itself is the engine's: it keys an item on the [`Task::version`] its source
4//! gives, serves it only at that version or a later one, and holds nothing a source did not
5//! hand it. A source says three things here, and nothing else about the cache reaches it:
6//!
7//! - which parts of a task, a project and a document its change token vouches for —
8//!   [`VersionCoverage`], [`ProjectVersionCoverage`] and [`DocumentVersionCoverage`] — so a part
9//!   it does not cover is never served from the cache;
10//! - what a write that expects one version of a task may take instead of reading it — an
11//!   opaque [`TaskSource::write_context`](crate::TaskSource::write_context) and
12//!   [`TaskSource::board_context`](crate::TaskSource::board_context), handed back through
13//!   [`TaskSource::prime_write`](crate::TaskSource::prime_write);
14//! - what each request of such a write did — a [`WriteJournal`] — so the engine can report
15//!   every part's outcome from the mutation's own response, and advance its entry only to
16//!   content that response vouches for.
17//!
18//! [`Task::version`]: crate::Task::version
19
20use std::collections::BTreeMap;
21
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25
26use crate::{Asset, Comment, DependencyEdge, Task};
27
28/// Whether a source's change token vouches for one part of a task.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
30#[serde(tag = "coverage", rename_all = "kebab-case")]
31pub enum Coverage {
32    /// Two equal tokens say this part did not change.
33    Covered,
34    /// The token says nothing about this part, so a cached copy of it is never served.
35    Uncovered {
36        /// Why, as a reader of a read that went to the source is told.
37        reason: String,
38    },
39}
40
41impl Coverage {
42    /// The reason this part is not covered, or `None` when it is.
43    #[must_use]
44    pub fn reason(&self) -> Option<&str> {
45        match self {
46            Self::Covered => None,
47            Self::Uncovered { reason } => Some(reason),
48        }
49    }
50}
51
52/// What a member of a coverage reads as when the value it is read from names none — one written
53/// before that member existed — so a part nobody declared is never served.
54fn undeclared() -> Coverage {
55    Coverage::Uncovered {
56        reason: "the source did not declare that its change token covers this part".to_owned(),
57    }
58}
59
60/// Which parts of a task a source's change token vouches for.
61///
62/// Three parts, because they are read apart: a task's own fields — title, content, status,
63/// priority, labels, metadata and the image assets its content names — its comments, and its
64/// forward dependency edges, native and recorded. A member absent from the value it is read
65/// from is [`Coverage::Uncovered`].
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
67pub struct VersionCoverage {
68    /// The task's own fields.
69    #[serde(default = "undeclared")]
70    pub fields: Coverage,
71    /// Its comments.
72    #[serde(default = "undeclared")]
73    pub comments: Coverage,
74    /// Its forward dependency edges: what it depends on, natively and as recorded.
75    #[serde(default = "undeclared")]
76    pub forward_edges: Coverage,
77}
78
79impl VersionCoverage {
80    /// A token covering every part of a task.
81    #[must_use]
82    pub fn whole() -> Self {
83        Self {
84            fields: Coverage::Covered,
85            comments: Coverage::Covered,
86            forward_edges: Coverage::Covered,
87        }
88    }
89
90    /// A token covering no part of a task, for `reason`.
91    #[must_use]
92    pub fn none(reason: impl Into<String>) -> Self {
93        let reason = reason.into();
94        Self {
95            fields: Coverage::Uncovered {
96                reason: reason.clone(),
97            },
98            comments: Coverage::Uncovered {
99                reason: reason.clone(),
100            },
101            forward_edges: Coverage::Uncovered { reason },
102        }
103    }
104}
105
106/// Which parts of a project a source's change token — its [`Project::version`] — vouches for.
107///
108/// A member absent from the value it is read from is [`Coverage::Uncovered`].
109///
110/// [`Project::version`]: crate::Project::version
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
112pub struct ProjectVersionCoverage {
113    /// The project's own fields: title, content, status, labels and metadata.
114    #[serde(default = "undeclared")]
115    pub fields: Coverage,
116    /// Its forward dependency edges.
117    #[serde(default = "undeclared")]
118    pub forward_edges: Coverage,
119}
120
121impl ProjectVersionCoverage {
122    /// A token covering every part of a project.
123    #[must_use]
124    pub fn whole() -> Self {
125        Self {
126            fields: Coverage::Covered,
127            forward_edges: Coverage::Covered,
128        }
129    }
130
131    /// A token covering no part of a project, for `reason`.
132    #[must_use]
133    pub fn none(reason: impl Into<String>) -> Self {
134        let reason = reason.into();
135        Self {
136            fields: Coverage::Uncovered {
137                reason: reason.clone(),
138            },
139            forward_edges: Coverage::Uncovered { reason },
140        }
141    }
142}
143
144/// Which parts of a document a source's change token — its [`Document::version`] — vouches
145/// for.
146///
147/// A member absent from the value it is read from is [`Coverage::Uncovered`].
148///
149/// [`Document::version`]: crate::Document::version
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
151pub struct DocumentVersionCoverage {
152    /// The document's own fields: title, content, project, labels and metadata.
153    #[serde(default = "undeclared")]
154    pub fields: Coverage,
155    /// The manifest of the image assets it holds — each one's name, digest and type. The
156    /// bytes are served only against a digest a covered manifest named, and re-hashed.
157    #[serde(default = "undeclared")]
158    pub assets: Coverage,
159}
160
161impl DocumentVersionCoverage {
162    /// A token covering every part of a document.
163    #[must_use]
164    pub fn whole() -> Self {
165        Self {
166            fields: Coverage::Covered,
167            assets: Coverage::Covered,
168        }
169    }
170
171    /// A token covering no part of a document, for `reason`.
172    #[must_use]
173    pub fn none(reason: impl Into<String>) -> Self {
174        let reason = reason.into();
175        Self {
176            fields: Coverage::Uncovered {
177                reason: reason.clone(),
178            },
179            assets: Coverage::Uncovered { reason },
180        }
181    }
182}
183
184/// What one request a write sent came to.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
186#[serde(tag = "outcome", rename_all = "kebab-case")]
187pub enum PartOutcome {
188    /// The backend answered that it was applied.
189    Landed,
190    /// The backend answered that it was refused.
191    Rejected {
192        /// What the backend said.
193        error: String,
194        /// Whether it was refused because an id the write took from a primed context — an
195        /// item's node id, a field's or an option's — no longer resolves. Such a part is
196        /// sent again once, after one read.
197        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
198        #[schemars(!skip_serializing_if)]
199        stale: bool,
200    },
201    /// The request left and no answer came back, so whether it was applied cannot be known.
202    /// Never sent again: doing so could apply it twice.
203    Unknown {
204        /// What went wrong.
205        error: String,
206    },
207}
208
209/// What one request of a write was.
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
211#[serde(rename_all = "kebab-case")]
212pub enum RequestKind {
213    /// A read the write needed: the write fell back to reading its target.
214    Read,
215    /// A mutation: a part of the write itself.
216    Mutation,
217}
218
219/// One task as a write's own response showed it whole: everything an entry of the cache
220/// holds, at the version that response determines.
221///
222/// Built only by [`WholeTask::new`], and read back through the same check, so a whole task
223/// always carries a version: one without is not a task a cache can key.
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
225#[serde(try_from = "WholeTaskShape")]
226pub struct WholeTask {
227    task: Task,
228    comments: Vec<Comment>,
229    assets: Vec<Asset>,
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    edges: Option<Vec<DependencyEdge>>,
232}
233
234/// [`WholeTask`] as it is spelled, before its version is checked.
235#[derive(Deserialize, JsonSchema)]
236struct WholeTaskShape {
237    task: Task,
238    comments: Vec<Comment>,
239    assets: Vec<Asset>,
240    #[serde(default)]
241    edges: Option<Vec<DependencyEdge>>,
242}
243
244impl TryFrom<WholeTaskShape> for WholeTask {
245    type Error = String;
246
247    fn try_from(shape: WholeTaskShape) -> Result<Self, Self::Error> {
248        Self::new(shape.task, shape.comments, shape.assets, shape.edges)
249            .ok_or_else(|| "a whole task carries the version it is at".to_owned())
250    }
251}
252
253impl WholeTask {
254    /// The task whole — its comments, its assets and, when known, its forward edges — or
255    /// `None` for a task without a [`Task::version`].
256    #[must_use]
257    pub fn new(
258        task: Task,
259        comments: Vec<Comment>,
260        assets: Vec<Asset>,
261        edges: Option<Vec<DependencyEdge>>,
262    ) -> Option<Self> {
263        task.version.as_ref()?;
264        Some(Self {
265            task,
266            comments,
267            assets,
268            edges,
269        })
270    }
271
272    /// The task, its [`Task::version`] the version the response determines.
273    #[must_use]
274    pub fn task(&self) -> &Task {
275        &self.task
276    }
277
278    /// The version it is at.
279    #[must_use]
280    pub fn version(&self) -> &str {
281        self.task.version.as_deref().unwrap_or_default()
282    }
283
284    /// Every comment it holds, oldest first.
285    #[must_use]
286    pub fn comments(&self) -> &[Comment] {
287        &self.comments
288    }
289
290    /// The image assets it holds.
291    #[must_use]
292    pub fn assets(&self) -> &[Asset] {
293        &self.assets
294    }
295
296    /// Its forward dependency edges, when the response carried them whole.
297    #[must_use]
298    pub fn edges(&self) -> Option<&[DependencyEdge]> {
299        self.edges.as_deref()
300    }
301}
302
303/// One request a source sent while a journal was open.
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
305pub struct JournalEntry {
306    /// Which part of the write it was, in the source's own words — `fields`, `issue`,
307    /// `comment` — or, for a read, what was read.
308    pub part: String,
309    /// Whether it was a read or a mutation.
310    pub kind: RequestKind,
311    /// What it came to.
312    #[serde(flatten)]
313    pub outcome: PartOutcome,
314    /// Each field the part set, with the value the response shows it holding.
315    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
316    pub fields: BTreeMap<String, Value>,
317    /// What the response showed of the task afterwards.
318    #[serde(default)]
319    pub shown: Shown,
320}
321
322/// What one response showed of the task a write left behind.
323///
324/// One value rather than a version beside an optional whole task, so a whole task always
325/// carries the version it is at: [`Shown::Whole`]'s own `task.version`.
326#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
327#[serde(tag = "shown", rename_all = "kebab-case")]
328pub enum Shown {
329    /// Nothing that determines the task's version.
330    #[default]
331    Nothing,
332    /// The task's version, and not every part an entry holds at it.
333    Version {
334        /// The version.
335        version: String,
336    },
337    /// The whole task, every part an entry holds, at its own `task.version` — never built for
338    /// a task without one.
339    Whole {
340        /// The task.
341        task: Box<WholeTask>,
342    },
343}
344
345impl Shown {
346    /// The version the response determines, if it does.
347    #[must_use]
348    pub fn version(&self) -> Option<&str> {
349        match self {
350            Self::Nothing => None,
351            Self::Version { version } => Some(version),
352            Self::Whole { task } => Some(task.version()),
353        }
354    }
355
356    /// The whole task, when the response carried it.
357    #[must_use]
358    pub fn whole(&self) -> Option<&WholeTask> {
359        match self {
360            Self::Whole { task } => Some(task),
361            Self::Nothing | Self::Version { .. } => None,
362        }
363    }
364}
365
366/// Every request a source sent between [`TaskSource::begin_journal`] and
367/// [`TaskSource::end_journal`], in the order sent.
368///
369/// [`TaskSource::begin_journal`]: crate::TaskSource::begin_journal
370/// [`TaskSource::end_journal`]: crate::TaskSource::end_journal
371#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
372pub struct WriteJournal {
373    /// The requests, oldest first.
374    pub requests: Vec<JournalEntry>,
375}
376
377impl WriteJournal {
378    /// Whether any request was a read.
379    #[must_use]
380    pub fn read(&self) -> bool {
381        self.requests
382            .iter()
383            .any(|entry| entry.kind == RequestKind::Read)
384    }
385
386    /// The mutations, oldest first.
387    pub fn mutations(&self) -> impl Iterator<Item = &JournalEntry> {
388        self.requests
389            .iter()
390            .filter(|entry| entry.kind == RequestKind::Mutation)
391    }
392
393    /// Whether a mutation was refused for a stale primed id.
394    #[must_use]
395    pub fn stale(&self) -> bool {
396        self.mutations()
397            .any(|entry| matches!(entry.outcome, PartOutcome::Rejected { stale: true, .. }))
398    }
399
400    /// Whether a mutation's outcome cannot be known.
401    #[must_use]
402    pub fn unknown(&self) -> bool {
403        self.mutations()
404            .any(|entry| matches!(entry.outcome, PartOutcome::Unknown { .. }))
405    }
406}