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::{debug, error, info, warn}; 8use ::log::trace; 9use serde::{Deserialize, Serialize}; 10 11#[cfg(not(target_family = "wasm"))] 12use crate::ports::{ChatError, ChatStore}; 13use crate::types::{Speaker, Turn};
One line of the running chat: words only.
The chat as it persists between launches: a rolling summary of everything older, and the recent turns word for word. It never grows past the compression threshold.
When anything last happened, so a relaunch within the hour does not say hello again.
Counts every change, wherever it was made, so the newest copy of the one session can be told.
38impl ChatState {
Whether this copy of the session is the one to keep over other: the more changed, and
on a tie the more recently active.
41 pub fn is_newer_than(&self, other: &ChatState) -> bool { 42 let newer = (self.version, self.last_active_ms) > (other.version, other.last_active_ms); 43 trace!( 44 "chat compare: mine v{} at {}, other v{} at {}, mine newer = {newer}", 45 self.version, self.last_active_ms, other.version, other.last_active_ms 46 ); 47 newer 48 }
Takes in another copy of the one session if it is the newer; otherwise changes nothing. Returns whether it did. Pure: where the result is kept is the holder's business.
The chat in one JSON file, replaced atomically on every change.
Like open, but a chat file that cannot be read is moved aside (and
returned) and the chat starts empty, rather than the app refusing to start.
81 pub fn open_or_set_aside(path: &Path) -> Result<(Self, Option<PathBuf>), ChatError> { 82 let chat = Self::open(path); 83 match chat.load() { 84 Ok(_) => Ok((chat, None)), 85 Err(e) if e.0.contains("damaged") => { 86 warn!("chat file {} is damaged ({}); moving it aside and starting empty", path.display(), e.0); 87 let aside = crate::atomic::set_aside(path).map_err(|e| { 88 error!("could not move the damaged chat file aside: {e}"); 89 ChatError(e.to_string()) 90 })?; 91 info!("damaged chat file kept as {}", aside.display()); 92 Ok((Self::open(path), Some(aside))) 93 } 94 Err(e) => { 95 error!("chat file {} cannot be opened: {}", path.display(), e.0); 96 Err(e) 97 } 98 } 99 } 100}
102#[cfg(not(target_family = "wasm"))] 103impl ChatStore for JsonChat { 104 fn load(&self) -> Result<ChatState, ChatError> { 105 match fs::read_to_string(&self.path) { 106 Ok(text) => { 107 let state: ChatState = serde_json::from_str(&text).map_err(|e| { 108 warn!("chat file {} does not parse: {e}", self.path.display()); 109 ChatError(format!("chat file is damaged: {e}")) 110 })?; 111 debug!("chat loaded: {} turns, version {}, summary {} bytes", state.turns.len(), state.version, state.summary.len()); 112 Ok(state) 113 } 114 Err(e) if e.kind() == std::io::ErrorKind::NotFound => { 115 debug!("no chat file at {}; starting empty", self.path.display()); 116 Ok(ChatState::default()) 117 } 118 Err(e) => { 119 error!("chat file {} unreadable: {e}", self.path.display()); 120 Err(ChatError(e.to_string())) 121 } 122 } 123 } 124 125 fn save(&mut self, state: &ChatState) -> Result<(), ChatError> { 126 let text = serde_json::to_string(state).map_err(|e| { 127 error!("chat state does not serialize: {e}"); 128 ChatError(e.to_string()) 129 })?; 130 debug!("saving chat: {} turns, version {}, {} bytes", state.turns.len(), state.version, text.len()); 131 crate::atomic::write_atomic(&self.path, text.as_bytes()).map_err(|e| ChatError(e.to_string())) 132 } 133}