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.
Identities of facts the parents had forgotten, kept so another device cannot bring them back.
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.
52impl MemoryDoc {
What this holder knows, for another to merge.
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.
153impl MemoryDoc {
Gives a fact its picture, if it has none yet. Returns the fact, or None if there is no such fact.
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.
A parent restores a fact she put away. Restoring what is not away changes nothing.
What Whiskers remembers, as one JSON file replaced atomically on every change.
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}