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