cover.rsannotatedcover.rssource156 lines · 6.0 KB · raw

Which of a memory's pictures is the card's, kept as a last-writer-wins register on the fact.

A fact can carry several pictures (Fact::pictures). The card shows one of them. With no choice made it is the first; anyone, the child included, can star another, and that choice reaches every device the way Visibility does. The register carries its own time, at_ms, and the later write wins; on an exact tie the smaller picture name wins, so the answer does not depend on which copy was merged first.

The choice is a typed enum, so "chosen, with no picture" and "chosen, with no time" cannot be written down. A choice is only ever made through [Fact::choose_cover], which refuses a picture the fact does not have, and a stored or merged cover that names one anyway (a damaged file, a duplicate that carried other pictures) is ignored by [Fact::card_picture] rather than shown.

13use serde::{Deserialize, Serialize};
15use crate::types::{Fact, PictureId};

Which picture is the card's, and the moment that was decided.

18#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
19pub enum Cover {

Nobody has starred a picture: the card shows the first one.

21    #[default]
22    Unchosen,

A picture was starred at at_ms.

24    Chosen { picture: PictureId, at_ms: u64 },
25}
27impl Cover {

True for the state every new fact has, which is left out of the stored and wire forms so a fact nobody has touched is byte for byte what it was before this state existed.

30    pub fn is_default(&self) -> bool {
31        *self == Cover::Unchosen
32    }
34    fn at_ms(&self) -> u64 {
35        match self {
36            Cover::Unchosen => 0,
37            Cover::Chosen { at_ms, .. } => *at_ms,
38        }
39    }

Starring picture at now_ms. The write is made later than the one it follows even if this device's clock is behind, so a causally later star always wins the merge.

43    fn chosen_after(&self, picture: PictureId, now_ms: u64) -> Cover {
44        Cover::Chosen { picture, at_ms: now_ms.max(self.at_ms().saturating_add(1)) }
45    }

The register's merge: the later write, and on a tie the smaller picture name. Commutative, associative and idempotent.

49    pub fn merged(self, other: Cover) -> Cover {
50        if other.order_key() > self.order_key() { other } else { self }
51    }

Later time first, then the smaller name; Unchosen is below every choice.

54    fn order_key(&self) -> (bool, u64, std::cmp::Reverse<&str>) {
55        match self {
56            Cover::Unchosen => (false, 0, std::cmp::Reverse("")),
57            Cover::Chosen { picture, at_ms } => (true, *at_ms, std::cmp::Reverse(picture.0.as_str())),
58        }
59    }
60}

A star on a picture the memory does not have.

63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct NotThisMemorysPicture;
66impl Fact {

The picture the card shows: the starred one if the fact has it, otherwise the first, otherwise none (the card then shows its pixel icon).

69    pub fn card_picture(&self) -> Option<&PictureId> {
70        match &self.cover {
71            Cover::Chosen { picture, .. } if self.pictures.contains(picture) => Some(picture),
72            _ => self.pictures.first(),
73        }
74    }

Stars one of this fact's pictures at now_ms. Refuses a picture the fact does not have.

77    pub fn choose_cover(&mut self, picture: &PictureId, now_ms: u64) -> Result<(), NotThisMemorysPicture> {
78        if !self.pictures.contains(picture) {
79            return Err(NotThisMemorysPicture);
80        }
81        self.cover = self.cover.chosen_after(picture.clone(), now_ms);
82        Ok(())
83    }

Takes in another copy's star. A cover naming a picture this copy does not have is left out, so the register never holds what the fact cannot show. Returns whether this copy changed.

87    pub(crate) fn merge_cover(&mut self, theirs: Cover) -> bool {
88        if let Cover::Chosen { picture, .. } = &theirs
89            && !self.pictures.contains(picture)
90        {
91            ::log::warn!("merge ignores a cover that names a picture this fact does not have");
92            return false;
93        }
94        let merged = self.cover.clone().merged(theirs);
95        let changed = merged != self.cover;
96        self.cover = merged;
97        changed
98    }
99}
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    fn p(n: &str) -> PictureId {
106        PictureId(n.to_owned())
107    }
108
109    fn all() -> Vec<Cover> {
110        let mut v = vec![Cover::Unchosen];
111        for t in [0, 1, 4] {
112            for n in ["a.jpg", "b.jpg"] {
113                v.push(Cover::Chosen { picture: p(n), at_ms: t });
114            }
115        }
116        v
117    }
118
119    #[test]
120    fn the_merge_is_commutative_associative_and_idempotent() {
121        for a in all() {
122            assert_eq!(a.clone().merged(a.clone()), a);
123            for b in all() {
124                assert_eq!(a.clone().merged(b.clone()), b.clone().merged(a.clone()), "{a:?} {b:?}");
125                for c in all() {
126                    assert_eq!(
127                        a.clone().merged(b.clone()).merged(c.clone()),
128                        a.clone().merged(b.clone().merged(c.clone())),
129                        "{a:?} {b:?} {c:?}"
130                    );
131                }
132            }
133        }
134    }
135
136    #[test]
137    fn a_tie_goes_to_the_smaller_name_in_either_order() {
138        let (a, b) = (Cover::Chosen { picture: p("a.jpg"), at_ms: 3 }, Cover::Chosen { picture: p("b.jpg"), at_ms: 3 });
139        assert_eq!(a.clone().merged(b.clone()), a);
140        assert_eq!(b.merged(a.clone()), a);
141    }
142
143    #[test]
144    fn a_later_star_beats_the_one_it_follows_even_from_a_slow_clock() {
145        let first = Cover::Unchosen.chosen_after(p("a.jpg"), 1_000);
146        let second = first.chosen_after(p("b.jpg"), 10);
147        assert_eq!(first.clone().merged(second.clone()), second);
148        assert_eq!(second.clone().merged(first), second);
149    }
150
151    #[test]
152    fn the_default_is_left_out_of_the_stored_form() {
153        assert!(Cover::default().is_default());
154        assert!(!Cover::Unchosen.chosen_after(p("a.jpg"), 1).is_default());
155    }
156}