Skip to main content

ryo_storage/storage/
state.rs

1//! State reference and storage for content-addressed state management.
2//!
3//! This module provides the foundation for deterministic replay by enabling
4//! content-addressed storage of file states.
5//!
6//! # Design
7//!
8//! ```text
9//! StateRef ─────── immutable reference to state (content hash)
10//!     │
11//!     └──→ StateStore ─── content-addressed storage
12//!              │
13//!              ├── InMemoryStateStore (for testing/short sessions)
14//!              └── (Future) PersistentStateStore
15//! ```
16//!
17//! # Example
18//!
19//! ```ignore
20//! use ryo_core::storage::{StateRef, InMemoryStateStore, StateStore};
21//!
22//! let mut store = InMemoryStateStore::new();
23//!
24//! // Store content, get reference
25//! let content = "fn main() {}";
26//! let state_ref = store.store(content);
27//!
28//! // Same content = same reference (content-addressed)
29//! let same_ref = store.store(content);
30//! assert_eq!(state_ref, same_ref);
31//!
32//! // Load content back
33//! let loaded = store.load(&state_ref).unwrap();
34//! assert_eq!(loaded, content);
35//! ```
36
37use serde::{Deserialize, Serialize};
38use std::collections::hash_map::DefaultHasher;
39use std::collections::HashMap;
40use std::fmt;
41use std::hash::{Hash, Hasher};
42
43// ============================================================================
44// StateRef
45// ============================================================================
46
47/// Immutable reference to a state (content hash).
48///
49/// This is the core abstraction for content-addressed storage.
50/// Two identical states will have the same `StateRef`.
51///
52/// # Implementation
53///
54/// Currently uses BLAKE3 hash for fast, secure hashing.
55/// The hash is stored as a hex string for JSON compatibility.
56#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
57pub struct StateRef(String);
58
59impl StateRef {
60    /// Create a new StateRef from content.
61    ///
62    /// Uses a hash of the content for addressing.
63    /// Note: Currently uses std hash; production should use blake3 or sha256.
64    pub fn from_content(content: &str) -> Self {
65        use std::hash::Hash;
66        let mut hasher = DefaultHasher::new();
67        content.hash(&mut hasher);
68        let hash = hasher.finish();
69        Self(format!("{:016x}", hash))
70    }
71
72    /// Create a StateRef from an existing hash string.
73    ///
74    /// Use this when deserializing from storage.
75    pub fn from_hash(hash: String) -> Self {
76        Self(hash)
77    }
78
79    /// Get the hash string.
80    pub fn hash(&self) -> &str {
81        &self.0
82    }
83
84    /// Get a short prefix for display (first 8 chars).
85    pub fn short(&self) -> &str {
86        &self.0[..8.min(self.0.len())]
87    }
88
89    /// Check if this is an empty/null reference.
90    pub fn is_empty(&self) -> bool {
91        self.0.is_empty()
92    }
93}
94
95impl fmt::Debug for StateRef {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "StateRef({}...)", self.short())
98    }
99}
100
101impl fmt::Display for StateRef {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "{}", self.short())
104    }
105}
106
107impl Hash for StateRef {
108    fn hash<H: Hasher>(&self, state: &mut H) {
109        self.0.hash(state);
110    }
111}
112
113// ============================================================================
114// StateStore Trait
115// ============================================================================
116
117/// Trait for content-addressed state storage.
118///
119/// Implementations must guarantee:
120/// 1. `store(content)` returns the same `StateRef` for identical content
121/// 2. `load(ref)` returns `Some(content)` if previously stored
122/// 3. Content is immutable once stored
123pub trait StateStore: Send + Sync {
124    /// Store content and return its reference.
125    ///
126    /// If the content already exists, returns the existing reference
127    /// without storing again (deduplication).
128    fn store(&mut self, content: &str) -> StateRef;
129
130    /// Load content by reference.
131    ///
132    /// Returns `None` if the reference is not found.
133    fn load(&self, state_ref: &StateRef) -> Option<String>;
134
135    /// Check if a reference exists in the store.
136    fn exists(&self, state_ref: &StateRef) -> bool {
137        self.load(state_ref).is_some()
138    }
139
140    /// Get the number of stored states.
141    fn len(&self) -> usize;
142
143    /// Check if the store is empty.
144    fn is_empty(&self) -> bool {
145        self.len() == 0
146    }
147}
148
149// ============================================================================
150// InMemoryStateStore
151// ============================================================================
152
153/// In-memory implementation of StateStore.
154///
155/// Suitable for:
156/// - Testing
157/// - Short-lived sessions
158/// - Sessions where persistence is not required
159///
160/// For persistent storage across sessions, use `PersistentStateStore` (future).
161#[derive(Debug, Default)]
162pub struct InMemoryStateStore {
163    states: HashMap<StateRef, String>,
164}
165
166impl InMemoryStateStore {
167    /// Create a new empty store.
168    pub fn new() -> Self {
169        Self::default()
170    }
171
172    /// Get all stored state references.
173    pub fn refs(&self) -> impl Iterator<Item = &StateRef> {
174        self.states.keys()
175    }
176
177    /// Clear all stored states.
178    pub fn clear(&mut self) {
179        self.states.clear();
180    }
181
182    /// Get total size in bytes (approximate).
183    pub fn size_bytes(&self) -> usize {
184        self.states.values().map(|s| s.len()).sum()
185    }
186}
187
188impl StateStore for InMemoryStateStore {
189    fn store(&mut self, content: &str) -> StateRef {
190        let state_ref = StateRef::from_content(content);
191
192        // Deduplication: don't store if already exists
193        if !self.states.contains_key(&state_ref) {
194            self.states.insert(state_ref.clone(), content.to_string());
195        }
196
197        state_ref
198    }
199
200    fn load(&self, state_ref: &StateRef) -> Option<String> {
201        self.states.get(state_ref).cloned()
202    }
203
204    fn exists(&self, state_ref: &StateRef) -> bool {
205        self.states.contains_key(state_ref)
206    }
207
208    fn len(&self) -> usize {
209        self.states.len()
210    }
211}
212
213// ============================================================================
214// FileStateSnapshot
215// ============================================================================
216
217/// Snapshot of a file's state at a point in time.
218///
219/// This captures both the file identity (path) and its content reference.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct FileStateSnapshot {
222    /// Relative path within the project.
223    pub path: String,
224    /// Reference to the content.
225    pub state_ref: StateRef,
226}
227
228impl FileStateSnapshot {
229    /// Create a new snapshot.
230    pub fn new(path: impl Into<String>, state_ref: StateRef) -> Self {
231        Self {
232            path: path.into(),
233            state_ref,
234        }
235    }
236}
237
238/// Snapshot of multiple files (e.g., workspace state).
239#[derive(Debug, Clone, Default, Serialize, Deserialize)]
240pub struct WorkspaceSnapshot {
241    /// File snapshots, keyed by path.
242    pub files: HashMap<String, StateRef>,
243    /// Optional name for this snapshot (like a Git tag).
244    pub name: Option<String>,
245    /// Timestamp when snapshot was taken.
246    pub timestamp_ms: u64,
247}
248
249impl WorkspaceSnapshot {
250    /// Create an empty snapshot.
251    pub fn new() -> Self {
252        Self::default()
253    }
254
255    /// Create a named snapshot.
256    pub fn named(name: impl Into<String>) -> Self {
257        Self {
258            name: Some(name.into()),
259            ..Default::default()
260        }
261    }
262
263    /// Add a file to the snapshot.
264    pub fn add_file(&mut self, path: impl Into<String>, state_ref: StateRef) {
265        self.files.insert(path.into(), state_ref);
266    }
267
268    /// Get the state reference for a file.
269    pub fn get_file(&self, path: &str) -> Option<&StateRef> {
270        self.files.get(path)
271    }
272
273    /// Number of files in the snapshot.
274    pub fn file_count(&self) -> usize {
275        self.files.len()
276    }
277}
278
279// ============================================================================
280// Tests
281// ============================================================================
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_state_ref_from_content() {
289        let content = "fn main() {}";
290        let ref1 = StateRef::from_content(content);
291        let ref2 = StateRef::from_content(content);
292
293        // Same content = same reference
294        assert_eq!(ref1, ref2);
295
296        // Different content = different reference
297        let ref3 = StateRef::from_content("fn main() { println!(); }");
298        assert_ne!(ref1, ref3);
299    }
300
301    #[test]
302    fn test_state_ref_display() {
303        let content = "fn main() {}";
304        let state_ref = StateRef::from_content(content);
305
306        // Short display shows first 8 chars
307        assert_eq!(state_ref.short().len(), 8);
308        assert!(format!("{}", state_ref).len() == 8);
309        assert!(format!("{:?}", state_ref).contains("..."));
310    }
311
312    #[test]
313    fn test_in_memory_store_roundtrip() {
314        let mut store = InMemoryStateStore::new();
315
316        let content = "fn main() {}";
317        let state_ref = store.store(content);
318
319        let loaded = store.load(&state_ref).unwrap();
320        assert_eq!(loaded, content);
321    }
322
323    #[test]
324    fn test_in_memory_store_deduplication() {
325        let mut store = InMemoryStateStore::new();
326
327        let content = "fn main() {}";
328        store.store(content);
329        store.store(content);
330        store.store(content);
331
332        // Should only store once
333        assert_eq!(store.len(), 1);
334    }
335
336    #[test]
337    fn test_in_memory_store_multiple_files() {
338        let mut store = InMemoryStateStore::new();
339
340        let ref1 = store.store("content 1");
341        let ref2 = store.store("content 2");
342        let ref3 = store.store("content 3");
343
344        assert_eq!(store.len(), 3);
345        assert_ne!(ref1, ref2);
346        assert_ne!(ref2, ref3);
347
348        assert_eq!(store.load(&ref1), Some("content 1".to_string()));
349        assert_eq!(store.load(&ref2), Some("content 2".to_string()));
350        assert_eq!(store.load(&ref3), Some("content 3".to_string()));
351    }
352
353    #[test]
354    fn test_workspace_snapshot() {
355        let mut store = InMemoryStateStore::new();
356        let mut snapshot = WorkspaceSnapshot::named("v1.0");
357
358        let ref1 = store.store("fn main() {}");
359        let ref2 = store.store("mod lib;");
360
361        snapshot.add_file("src/main.rs", ref1.clone());
362        snapshot.add_file("src/lib.rs", ref2.clone());
363
364        assert_eq!(snapshot.file_count(), 2);
365        assert_eq!(snapshot.get_file("src/main.rs"), Some(&ref1));
366        assert_eq!(snapshot.get_file("src/lib.rs"), Some(&ref2));
367    }
368
369    #[test]
370    fn test_state_ref_serialization() {
371        let state_ref = StateRef::from_content("test content");
372
373        let json = serde_json::to_string(&state_ref).unwrap();
374        let deserialized: StateRef = serde_json::from_str(&json).unwrap();
375
376        assert_eq!(state_ref, deserialized);
377    }
378}