memory.rsannotatedmemory.rssource361 lines · 14.8 KB · raw
1#[cfg(not(target_family = "wasm"))]
2use std::fs;
3#[cfg(not(target_family = "wasm"))]
4use std::path::{Path, PathBuf};
5
6#[cfg(not(target_family = "wasm"))]
7use ::log::{error, warn};
8use ::log::{debug, info, trace};
9
10use serde::{Deserialize, Serialize};
11
12#[cfg(not(target_family = "wasm"))]
13use crate::ports::{Memory, MemoryError};
14use crate::types::{Fact, MemorySnapshot};
15use whiskers_icons::IconId;
16#[cfg(not(target_family = "wasm"))]
17use crate::types::Visibility;
18#[cfg(not(target_family = "wasm"))]
19use crate::types::NewFact;

What Whiskers remembers, with no storage attached: the document every holder of the memory keeps, whether a file on a device, the hub on the service or a Durable Object. next_id is kept beside the facts so an id is never reused after the newest fact is forgotten. Its JSON is the memory file's.

24#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
25pub struct MemoryDoc {
26    next_id: u64,
27    facts: Vec<Fact>,

Identities of facts the parents had forgotten, kept so another device cannot bring them back.

29    #[serde(default)]
30    forgotten: Vec<String>,
31}

A fresh identity, random enough that two devices never pick the same one.

34#[cfg(not(target_family = "wasm"))]
35fn new_gid() -> String {
36    use std::hash::{BuildHasher, Hasher};
37    trace!("minting a fact identity");
38    let (a, b) = (std::collections::hash_map::RandomState::new(), std::collections::hash_map::RandomState::new());
39    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos());
40    let mut ha = a.build_hasher();
41    ha.write_u128(now);
42    let mut hb = b.build_hasher();
43    hb.write_u128(now.rotate_left(64));
44    format!("{:016x}{:016x}", ha.finish(), hb.finish())
45}

Two facts that say the same thing, however it is spaced or capitalised, are one fact.

48fn same_words(text: &str) -> String {
49    text.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()
50}
52impl MemoryDoc {

What this holder knows, for another to merge.

54    pub fn snapshot(&self) -> MemorySnapshot {
55        trace!("memory snapshot: {} facts, {} forgotten", self.facts.len(), self.forgotten.len());
56        MemorySnapshot { facts: self.facts.clone(), forgotten: self.forgotten.clone() }
57    }

Takes in what another holder knows and returns how many facts changed here. Merging in any order, any number of times, gives the same facts: what either side forgot stays forgotten, the same thing learned twice is one fact, and an embedding computed on one side serves both. Pure: it touches no storage, so each holder decides how to make the result durable.

63    pub fn merge(&mut self, remote: MemorySnapshot) -> usize {
64        debug!(
65            "memory merge begins: {} local facts, {} remote facts, {} remote forgotten",
66            self.facts.len(), remote.facts.len(), remote.forgotten.len()
67        );
68        let mut changed = 0;
69        // What either side forgot stays forgotten, here and on the other side after its next merge.
70        for g in remote.forgotten {
71            if !self.forgotten.contains(&g) {
72                self.forgotten.push(g.clone());
73            }
74            let n = self.facts.len();
75            self.facts.retain(|f| f.gid != g);
76            changed += n - self.facts.len();
77        }
78        for mut f in remote.facts {
79            if f.gid.is_empty() || self.forgotten.contains(&f.gid) {
80                trace!("merge skips a remote fact (no identity or forgotten here)");
81                continue;
82            }
83            match self.facts.iter_mut().find(|l| l.gid == f.gid) {
84                Some(l) => {
85                    // The same fact; an embedding computed on one side serves both.
86                    if l.embedding.is_empty() && !f.embedding.is_empty() {
87                        l.embedding = f.embedding;
88                        changed += 1;
89                    }
90                    // A picture chosen on one side serves both; two choices meet at the smaller name, so every
91                    // order agrees.
92                    let icon = pick_icon(l.icon.clone(), f.icon.clone());
93                    if icon != l.icon {
94                        l.icon = icon;
95                        changed += 1;
96                    }
97                    // Whether she put it away is a register: the later write wins on every device.
98                    let v = l.visibility.merged(f.visibility);
99                    if v != l.visibility {
100                        l.visibility = v;
101                        changed += 1;
102                    }
103                }
104                None => {
105                    f.id = self.next_id.max(self.facts.iter().map(|x| x.id + 1).max().unwrap_or(0)).max(1);
106                    self.next_id = f.id + 1;
107                    self.facts.push(f);
108                    changed += 1;
109                }
110            }
111        }
112        // The same thing learned on two devices arrives as two facts. The one with the smaller
113        // identity survives on every device, and the other is forgotten everywhere.
114        let mut seen: Vec<(String, String)> = Vec::new(); // (words, surviving gid)
115        let mut doomed: Vec<(String, String)> = Vec::new(); // (dropped gid, surviving gid)
116        let mut by_gid: Vec<&Fact> = self.facts.iter().collect();
117        by_gid.sort_by(|a, b| a.gid.cmp(&b.gid));
118        for f in by_gid {
119            let w = same_words(&f.text);
120            if let Some((_, survivor)) = seen.iter().find(|(x, _)| *x == w) {
121                doomed.push((f.gid.clone(), survivor.clone()));
122            } else {
123                seen.push((w, f.gid.clone()));
124            }
125        }
126        if !doomed.is_empty() {
127            info!("merge: {} duplicate fact(s) learned on two devices are being dropped", doomed.len());
128        }
129        for (g, survivor) in doomed {
130            // What she did to the dropped copy is not lost: the survivor takes the later of the two.
131            let theirs = self.facts.iter().find(|f| f.gid == g).map(|f| (f.visibility, f.icon.clone()));
132            if let (Some((visibility, icon)), Some(s)) = (theirs, self.facts.iter_mut().find(|f| f.gid == survivor)) {
133                s.visibility = s.visibility.merged(visibility);
134                s.icon = pick_icon(s.icon.take(), icon);
135            }
136            self.facts.retain(|f| f.gid != g);
137            self.forgotten.push(g);
138            changed += 1;
139        }
140        info!("memory merge done: {changed} change(s); {} facts now", self.facts.len());
141        changed
142    }
143}

The picture two copies of a fact agree on: whichever has one, and the smaller name if both do.

146fn pick_icon(a: Option<IconId>, b: Option<IconId>) -> Option<IconId> {
147    match (a, b) {
148        (Some(a), Some(b)) => Some(a.min(b)),
149        (a, b) => a.or(b),
150    }
151}
153impl MemoryDoc {

Gives a fact its picture, if it has none yet. Returns the fact, or None if there is no such fact.

155    pub fn give_icon(&mut self, id: u64, icon: IconId) -> Option<Fact> {
156        let f = self.facts.iter_mut().find(|f| f.id == id)?;
157        if f.icon.is_none() {
158            f.icon = Some(icon);
159        }
160        Some(f.clone())
161    }

She puts a fact away at now_ms. Returns the fact as it now stands, or None if there is no such fact. Putting away what is already away changes nothing.

165    pub fn hide(&mut self, id: u64, now_ms: u64) -> Option<Fact> {
166        let f = self.facts.iter_mut().find(|f| f.id == id)?;
167        if !f.visibility.is_hidden() {
168            f.visibility = f.visibility.hidden_after(now_ms);
169        }
170        Some(f.clone())
171    }

A parent restores a fact she put away. Restoring what is not away changes nothing.

174    pub fn restore(&mut self, id: u64, now_ms: u64) -> Option<Fact> {
175        let f = self.facts.iter_mut().find(|f| f.id == id)?;
176        if f.visibility.is_hidden() {
177            f.visibility = f.visibility.restored_after(now_ms);
178        }
179        Some(f.clone())
180    }
181}

What Whiskers remembers, as one JSON file replaced atomically on every change.

184#[cfg(not(target_family = "wasm"))]
185pub struct JsonMemory {
186    path: PathBuf,
187    saved: MemoryDoc,
188}
190#[cfg(not(target_family = "wasm"))]
191impl JsonMemory {
192    pub fn open(path: &Path) -> Result<Self, MemoryError> {
193        let saved = match fs::read_to_string(path) {
194            Ok(text) => serde_json::from_str(&text).map_err(|e| {
195                warn!("memory file {} does not parse: {e}", path.display());
196                MemoryError(format!("memory file is damaged: {e}"))
197            })?,
198            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
199                debug!("no memory file at {}; starting empty", path.display());
200                MemoryDoc::default()
201            }
202            Err(e) => {
203                error!("memory file {} unreadable: {e}", path.display());
204                return Err(MemoryError(e.to_string()));
205            }
206        };
207        debug!("memory opened: {} facts, {} forgotten", saved.facts.len(), saved.forgotten.len());
208        Ok(Self { path: path.to_owned(), saved })
209    }

Like open, but a file that cannot be read is moved aside (and returned) and memory starts empty: the app must not be unable to start because of one bad file.

213    pub fn open_or_set_aside(path: &Path) -> Result<(Self, Option<PathBuf>), MemoryError> {
214        match Self::open(path) {
215            Ok(m) => Ok((m, None)),
216            Err(e) if e.0.contains("damaged") => {
217                warn!("memory file {} is damaged ({}); moving it aside and starting empty", path.display(), e.0);
218                let aside = crate::atomic::set_aside(path).map_err(|e| {
219                    error!("could not move the damaged memory file aside: {e}");
220                    MemoryError(e.to_string())
221                })?;
222                info!("damaged memory file kept as {}", aside.display());
223                Ok((Self::open(path)?, Some(aside)))
224            }
225            Err(e) => {
226                error!("memory cannot be opened: {}", e.0);
227                Err(e)
228            }
229        }
230    }

Applies f to the document and keeps the result; the saved copy is put back if the save fails.

233    fn change(&mut self, id: u64, what: &str, f: impl FnOnce(&mut MemoryDoc) -> Option<Fact>) -> Result<Fact, MemoryError> {
234        let before = self.saved.clone();
235        let Some(fact) = f(&mut self.saved) else {
236            warn!("{what}: no fact {id}");
237            return Err(MemoryError(format!("no fact {id}")));
238        };
239        if self.saved != before
240            && let Err(e) = self.save()
241        {
242            error!("{what} of fact {id} not kept, the save failed: {}", e.0);
243            self.saved = before;
244            return Err(e);
245        }
246        info!("fact {id}: {what} (now hidden = {})", fact.visibility.is_hidden());
247        Ok(fact)
248    }
250    fn save(&self) -> Result<(), MemoryError> {
251        let text = serde_json::to_string(&self.saved).map_err(|e| {
252            error!("memory does not serialize: {e}");
253            MemoryError(e.to_string())
254        })?;
255        trace!("saving memory: {} facts, {} bytes", self.saved.facts.len(), text.len());
256        crate::atomic::write_atomic(&self.path, text.as_bytes()).map_err(|e| MemoryError(e.to_string()))
257    }
258}
259
260#[cfg(not(target_family = "wasm"))]
261impl Memory for JsonMemory {
262    fn facts(&self) -> Vec<Fact> {
263        trace!("memory: {} facts listed", self.saved.facts.len());
264        self.saved.facts.clone()
265    }
266
267    fn add(&mut self, new: NewFact, at_ms: u64) -> Result<Fact, MemoryError> {
268        let id = self.saved.next_id.max(self.saved.facts.iter().map(|f| f.id + 1).max().unwrap_or(0)).max(1);
269        let fact = Fact {
270            id,
271            text: new.text,
272            learned_at_ms: at_ms,
273            kind: new.kind,
274            who: new.who,
275            place: new.place,
276            when: new.when,
277            pictures: new.pictures,
278            embedding: Vec::new(),
279            gid: new_gid(),
280            visibility: Visibility::default(),
281            icon: None,
282        };
283        let before = self.saved.next_id;
284        self.saved.next_id = id + 1;
285        self.saved.facts.push(fact.clone());
286        if let Err(e) = self.save() {
287            error!("fact {id} not kept, the save failed: {}", e.0);
288            self.saved.facts.pop();
289            self.saved.next_id = before;
290            return Err(e);
291        }
292        info!("fact {id} added ({} chars); {} facts now", fact.text.len(), self.saved.facts.len());
293        Ok(fact)
294    }
295
296    fn set_embedding(&mut self, id: u64, embedding: Vec<f32>) -> Result<(), MemoryError> {
297        let at = self.saved.facts.iter().position(|f| f.id == id).ok_or_else(|| {
298            warn!("set_embedding: no fact {id}");
299            MemoryError(format!("no fact {id}"))
300        })?;
301        debug!("fact {id} gets an embedding of {} numbers", embedding.len());
302        let before = std::mem::replace(&mut self.saved.facts[at].embedding, embedding);
303        if let Err(e) = self.save() {
304            error!("embedding for fact {id} not kept, the save failed: {}", e.0);
305            self.saved.facts[at].embedding = before;
306            return Err(e);
307        }
308        Ok(())
309    }
310
311    fn forget(&mut self, id: u64) -> Result<Fact, MemoryError> {
312        let at = self.saved.facts.iter().position(|f| f.id == id).ok_or_else(|| {
313            warn!("forget: no fact {id}");
314            MemoryError(format!("no fact {id}"))
315        })?;
316        let gone = self.saved.facts.remove(at);
317        let tombstoned = !gone.gid.is_empty() && !self.saved.forgotten.contains(&gone.gid);
318        if tombstoned {
319            self.saved.forgotten.push(gone.gid.clone());
320        }
321        if let Err(e) = self.save() {
322            error!("forgetting fact {id} not kept, the save failed: {}", e.0);
323            self.saved.facts.insert(at, gone);
324            if tombstoned {
325                self.saved.forgotten.pop();
326            }
327            return Err(e);
328        }
329        info!("fact {id} forgotten (tombstoned = {tombstoned}); {} facts left", self.saved.facts.len());
330        Ok(gone)
331    }
332
333    fn give_icon(&mut self, id: u64, icon: IconId) -> Result<Fact, MemoryError> {
334        self.change(id, "give an icon to", |d| d.give_icon(id, icon))
335    }
336
337    fn hide(&mut self, id: u64, at_ms: u64) -> Result<Fact, MemoryError> {
338        self.change(id, "hide", |d| d.hide(id, at_ms))
339    }
340
341    fn restore(&mut self, id: u64, at_ms: u64) -> Result<Fact, MemoryError> {
342        self.change(id, "restore", |d| d.restore(id, at_ms))
343    }
344
345    fn snapshot(&self) -> MemorySnapshot {
346        self.saved.snapshot()
347    }
348
349    fn merge(&mut self, remote: MemorySnapshot) -> Result<usize, MemoryError> {
350        let before = self.saved.clone();
351        let changed = self.saved.merge(remote);
352        if (self.saved.facts != before.facts || self.saved.forgotten != before.forgotten)
353            && let Err(e) = self.save()
354        {
355            error!("memory merge not kept, the save failed: {}", e.0);
356            self.saved = before;
357            return Err(e);
358        }
359        Ok(changed)
360    }
361}