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}