Skip to main content

edda_transcript/
cursor.rs

1use serde::{Deserialize, Serialize};
2use std::path::Path;
3
4#[derive(Debug, Serialize, Deserialize, Clone)]
5pub struct TranscriptCursor {
6    pub offset: u64,
7    pub file_size: u64,
8    pub mtime_unix: i64,
9    pub updated_at_unix: i64,
10}
11
12impl TranscriptCursor {
13    pub fn load(state_dir: &Path, session_id: &str) -> anyhow::Result<Option<Self>> {
14        let path = state_dir.join(format!("transcript_cursor.{session_id}.json"));
15        if !path.exists() {
16            return Ok(None);
17        }
18        let content = std::fs::read_to_string(&path)?;
19        let cursor: Self = serde_json::from_str(&content)?;
20        Ok(Some(cursor))
21    }
22
23    pub fn save(&self, state_dir: &Path, session_id: &str) -> anyhow::Result<()> {
24        let path = state_dir.join(format!("transcript_cursor.{session_id}.json"));
25        let data = serde_json::to_string_pretty(self)?;
26        edda_store::write_atomic(&path, data.as_bytes())
27    }
28
29    /// Check for truncation: if file shrank, reset offset to 0.
30    pub fn detect_truncation(&mut self, current_file_size: u64) {
31        if current_file_size < self.offset {
32            self.offset = 0;
33        }
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn cursor_save_and_load() {
43        let tmp = tempfile::tempdir().unwrap();
44        let cursor = TranscriptCursor {
45            offset: 100,
46            file_size: 5000,
47            mtime_unix: 1700000000,
48            updated_at_unix: 1700000001,
49        };
50        cursor.save(tmp.path(), "sess1").unwrap();
51        let loaded = TranscriptCursor::load(tmp.path(), "sess1")
52            .unwrap()
53            .unwrap();
54        assert_eq!(loaded.offset, 100);
55        assert_eq!(loaded.file_size, 5000);
56    }
57
58    #[test]
59    fn cursor_load_missing_returns_none() {
60        let tmp = tempfile::tempdir().unwrap();
61        let result = TranscriptCursor::load(tmp.path(), "missing").unwrap();
62        assert!(result.is_none());
63    }
64
65    #[test]
66    fn truncation_detection() {
67        let mut cursor = TranscriptCursor {
68            offset: 5000,
69            file_size: 5000,
70            mtime_unix: 0,
71            updated_at_unix: 0,
72        };
73        cursor.detect_truncation(3000);
74        assert_eq!(cursor.offset, 0);
75    }
76}