Skip to main content

ftui_runtime/
state_persistence.rs

1//! Widget state persistence for save/restore across sessions.
2//!
3//! This module provides the [`StateRegistry`] and [`StorageBackend`] infrastructure
4//! for persisting widget state. It works with the [`Stateful`] trait from `ftui-widgets`.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌──────────────────────────────────────────────────────────────┐
10//! │                      StateRegistry                            │
11//! │   - In-memory cache of widget states                          │
12//! │   - Delegates to StorageBackend for persistence               │
13//! │   - Provides load/save/clear operations                       │
14//! └──────────────────────────────────────────────────────────────┘
15//!                              │
16//!                              ▼
17//! ┌──────────────────────────────────────────────────────────────┐
18//! │                     StorageBackend                            │
19//! │   - MemoryStorage: in-memory (testing, ephemeral)             │
20//! │   - FileStorage: JSON file (requires state-persistence)       │
21//! └──────────────────────────────────────────────────────────────┘
22//! ```
23//!
24//! # Design Invariants
25//!
26//! 1. **Graceful degradation**: Storage failures never panic; operations return `Result`.
27//! 2. **Atomic writes**: File storage uses write-rename pattern to prevent corruption.
28//! 3. **Partial load tolerance**: Missing or corrupt entries use `Default::default()`.
29//! 4. **Type safety**: Registry is type-erased internally but type-safe at boundaries.
30//!
31//! # Failure Modes
32//!
33//! | Failure | Cause | Behavior |
34//! |---------|-------|----------|
35//! | `StorageError::Io` | File I/O failure | Returns error, cache unaffected |
36//! | `StorageError::Serialization` | JSON encode/decode | Entry skipped, logged |
37//! | `StorageError::Corruption` | Invalid file format | Load returns partial data |
38//! | Missing entry | First run, key changed | `Default::default()` used |
39//!
40//! # Feature Gates
41//!
42//! - `state-persistence`: Enables `FileStorage` with JSON serialization.
43//!   Without this feature, only `MemoryStorage` is available.
44//!
45//! [`Stateful`]: ftui_widgets::stateful::Stateful
46
47use std::collections::HashMap;
48use std::fmt;
49use std::sync::{Arc, RwLock};
50
51// ─────────────────────────────────────────────────────────────────────────────
52// Error Types
53// ─────────────────────────────────────────────────────────────────────────────
54
55/// Errors that can occur during state storage operations.
56#[derive(Debug)]
57pub enum StorageError {
58    /// I/O error during file operations.
59    Io(std::io::Error),
60    /// Serialization or deserialization error.
61    #[cfg(feature = "state-persistence")]
62    Serialization(String),
63    /// Storage file is corrupted or invalid format.
64    Corruption(String),
65    /// Backend is not available (e.g., file storage without feature).
66    Unavailable(String),
67}
68
69impl fmt::Display for StorageError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            StorageError::Io(e) => write!(f, "I/O error: {e}"),
73            #[cfg(feature = "state-persistence")]
74            StorageError::Serialization(msg) => write!(f, "serialization error: {msg}"),
75            StorageError::Corruption(msg) => write!(f, "storage corruption: {msg}"),
76            StorageError::Unavailable(msg) => write!(f, "storage unavailable: {msg}"),
77        }
78    }
79}
80
81impl std::error::Error for StorageError {
82    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
83        match self {
84            StorageError::Io(e) => Some(e),
85            #[cfg(feature = "state-persistence")]
86            StorageError::Serialization(_) => None,
87            StorageError::Corruption(_) => None,
88            StorageError::Unavailable(_) => None,
89        }
90    }
91}
92
93impl From<std::io::Error> for StorageError {
94    fn from(e: std::io::Error) -> Self {
95        StorageError::Io(e)
96    }
97}
98
99/// Result type for storage operations.
100pub type StorageResult<T> = Result<T, StorageError>;
101
102// ─────────────────────────────────────────────────────────────────────────────
103// Storage Backend Trait
104// ─────────────────────────────────────────────────────────────────────────────
105
106/// A serialized state entry with version metadata.
107///
108/// This is the storage format used by backends. The actual state data
109/// is serialized to bytes by the caller.
110#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct StoredEntry {
112    /// The canonical state key (widget_type::instance_id).
113    pub key: String,
114    /// Schema version from `Stateful::state_version()`.
115    pub version: u32,
116    /// Serialized state data (JSON bytes with `state-persistence` feature).
117    pub data: Vec<u8>,
118}
119
120/// Trait for pluggable state storage backends.
121///
122/// Implementations must be thread-safe (`Send + Sync`) to support
123/// concurrent access from the registry.
124///
125/// # Implementation Notes
126///
127/// - `load_all` should be resilient to partial corruption.
128/// - `save_all` should be atomic (write-then-rename pattern for files).
129/// - `clear` should remove all stored state for the application.
130pub trait StorageBackend: Send + Sync {
131    /// Human-readable name for logging.
132    fn name(&self) -> &str;
133
134    /// Load all stored state entries.
135    ///
136    /// Returns an empty map if no state exists (first run).
137    /// Skips corrupted entries rather than failing entirely.
138    fn load_all(&self) -> StorageResult<HashMap<String, StoredEntry>>;
139
140    /// Save all state entries atomically.
141    ///
142    /// This should replace all existing state (not merge).
143    fn save_all(&self, entries: &HashMap<String, StoredEntry>) -> StorageResult<()>;
144
145    /// Clear all stored state.
146    fn clear(&self) -> StorageResult<()>;
147
148    /// Check if the backend is available and functional.
149    fn is_available(&self) -> bool {
150        true
151    }
152}
153
154// ─────────────────────────────────────────────────────────────────────────────
155// Memory Storage (always available)
156// ─────────────────────────────────────────────────────────────────────────────
157
158/// In-memory storage backend for testing and ephemeral state.
159///
160/// State is lost when the process exits. Useful for:
161/// - Unit testing widget persistence logic
162/// - Applications that don't need cross-session persistence
163/// - Development/debugging without file I/O
164#[derive(Default)]
165pub struct MemoryStorage {
166    data: RwLock<HashMap<String, StoredEntry>>,
167}
168
169impl MemoryStorage {
170    /// Create a new empty memory storage.
171    #[must_use]
172    pub fn new() -> Self {
173        Self::default()
174    }
175
176    /// Create memory storage pre-populated with entries.
177    #[must_use]
178    pub fn with_entries(entries: HashMap<String, StoredEntry>) -> Self {
179        Self {
180            data: RwLock::new(entries),
181        }
182    }
183}
184
185impl StorageBackend for MemoryStorage {
186    fn name(&self) -> &str {
187        "MemoryStorage"
188    }
189
190    fn load_all(&self) -> StorageResult<HashMap<String, StoredEntry>> {
191        let guard = self
192            .data
193            .read()
194            .map_err(|_| StorageError::Corruption("lock poisoned".into()))?;
195        Ok(guard.clone())
196    }
197
198    fn save_all(&self, entries: &HashMap<String, StoredEntry>) -> StorageResult<()> {
199        let mut guard = self
200            .data
201            .write()
202            .map_err(|_| StorageError::Corruption("lock poisoned".into()))?;
203        *guard = entries.clone();
204        Ok(())
205    }
206
207    fn clear(&self) -> StorageResult<()> {
208        let mut guard = self
209            .data
210            .write()
211            .map_err(|_| StorageError::Corruption("lock poisoned".into()))?;
212        guard.clear();
213        Ok(())
214    }
215}
216
217impl fmt::Debug for MemoryStorage {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        let count = self.data.read().map(|g| g.len()).unwrap_or(0);
220        f.debug_struct("MemoryStorage")
221            .field("entries", &count)
222            .finish()
223    }
224}
225
226// ─────────────────────────────────────────────────────────────────────────────
227// File Storage (requires state-persistence feature)
228// ─────────────────────────────────────────────────────────────────────────────
229
230#[cfg(feature = "state-persistence")]
231mod file_storage {
232    use super::*;
233    use serde::{Deserialize, Serialize};
234    use std::fs::{self, File};
235    use std::io::{BufReader, BufWriter, Write};
236    use std::path::{Path, PathBuf};
237
238    /// File format for stored state (JSON).
239    #[derive(Serialize, Deserialize)]
240    struct StateFile {
241        /// Format version for future migrations.
242        format_version: u32,
243        /// Map of canonical key -> entry.
244        entries: HashMap<String, FileEntry>,
245    }
246
247    /// Serialized entry in the state file.
248    #[derive(Serialize, Deserialize)]
249    struct FileEntry {
250        version: u32,
251        /// Base64-encoded data for binary safety.
252        data_base64: String,
253    }
254
255    impl StateFile {
256        const FORMAT_VERSION: u32 = 1;
257
258        fn new() -> Self {
259            Self {
260                format_version: Self::FORMAT_VERSION,
261                entries: HashMap::new(),
262            }
263        }
264    }
265
266    /// File-based storage backend using JSON.
267    ///
268    /// State is persisted to a JSON file with atomic write-rename pattern.
269    /// Suitable for applications that need cross-session persistence.
270    ///
271    /// # File Format
272    ///
273    /// ```json
274    /// {
275    ///   "format_version": 1,
276    ///   "entries": {
277    ///     "ScrollView::main": {
278    ///       "version": 1,
279    ///       "data_base64": "eyJzY3JvbGxfb2Zmc2V0IjogNDJ9"
280    ///     }
281    ///   }
282    /// }
283    /// ```
284    ///
285    /// # Atomic Writes
286    ///
287    /// Writes use a temporary file + rename pattern to prevent corruption:
288    /// 1. Write to `{path}.tmp`
289    /// 2. Flush and sync
290    /// 3. Rename `{path}.tmp` -> `{path}`
291    pub struct FileStorage {
292        path: PathBuf,
293    }
294
295    impl FileStorage {
296        /// Create a file storage at the given path.
297        ///
298        /// The file does not need to exist; it will be created on first save.
299        #[must_use]
300        pub fn new(path: impl AsRef<Path>) -> Self {
301            Self {
302                path: path.as_ref().to_path_buf(),
303            }
304        }
305
306        /// Create storage at the default location for the application.
307        ///
308        /// Uses `$XDG_STATE_HOME/ftui/{app_name}/state.json` on Linux,
309        /// or platform-appropriate equivalents.
310        #[must_use]
311        pub fn default_for_app(app_name: &str) -> Self {
312            let base = dirs_or_fallback();
313            let path = base.join("ftui").join(app_name).join("state.json");
314            Self { path }
315        }
316
317        fn temp_path(&self) -> PathBuf {
318            let mut tmp = self.path.clone();
319            tmp.set_extension("json.tmp");
320            tmp
321        }
322    }
323
324    /// Get state directory, falling back to current dir if unavailable.
325    fn dirs_or_fallback() -> PathBuf {
326        // Try XDG_STATE_HOME first
327        if let Ok(state_home) = std::env::var("XDG_STATE_HOME") {
328            return PathBuf::from(state_home);
329        }
330        // Fall back to ~/.local/state
331        if let Ok(home) = std::env::var("HOME") {
332            return PathBuf::from(home).join(".local").join("state");
333        }
334        // Last resort: current directory
335        PathBuf::from(".")
336    }
337
338    impl StorageBackend for FileStorage {
339        fn name(&self) -> &str {
340            "FileStorage"
341        }
342
343        fn load_all(&self) -> StorageResult<HashMap<String, StoredEntry>> {
344            if !self.path.exists() {
345                // First run - no state yet
346                return Ok(HashMap::new());
347            }
348
349            let file = File::open(&self.path)?;
350            let reader = BufReader::new(file);
351
352            let state_file: StateFile = serde_json::from_reader(reader).map_err(|e| {
353                StorageError::Serialization(format!("failed to parse state file: {e}"))
354            })?;
355
356            // Validate format version
357            if state_file.format_version != StateFile::FORMAT_VERSION {
358                tracing::warn!(
359                    stored = state_file.format_version,
360                    expected = StateFile::FORMAT_VERSION,
361                    "state file format version mismatch, ignoring stored state"
362                );
363                return Ok(HashMap::new());
364            }
365
366            // Convert file entries to StoredEntry
367            let mut result = HashMap::new();
368            for (key, entry) in state_file.entries {
369                use base64::Engine;
370                let data = match base64::engine::general_purpose::STANDARD
371                    .decode(&entry.data_base64)
372                {
373                    Ok(d) => d,
374                    Err(e) => {
375                        tracing::warn!(key = %key, error = %e, "failed to decode state entry, skipping");
376                        continue;
377                    }
378                };
379                result.insert(
380                    key.clone(),
381                    StoredEntry {
382                        key,
383                        version: entry.version,
384                        data,
385                    },
386                );
387            }
388
389            Ok(result)
390        }
391
392        fn save_all(&self, entries: &HashMap<String, StoredEntry>) -> StorageResult<()> {
393            use base64::Engine;
394
395            // Ensure parent directory exists
396            if let Some(parent) = self.path.parent() {
397                fs::create_dir_all(parent)?;
398            }
399
400            // Build file content
401            let mut state_file = StateFile::new();
402            for (key, entry) in entries {
403                state_file.entries.insert(
404                    key.clone(),
405                    FileEntry {
406                        version: entry.version,
407                        data_base64: base64::engine::general_purpose::STANDARD.encode(&entry.data),
408                    },
409                );
410            }
411
412            // Write to temp file first (atomic pattern)
413            let tmp_path = self.temp_path();
414            {
415                let file = File::create(&tmp_path)?;
416                let mut writer = BufWriter::new(file);
417                serde_json::to_writer_pretty(&mut writer, &state_file).map_err(|e| {
418                    StorageError::Serialization(format!("failed to serialize state: {e}"))
419                })?;
420                writer.flush()?;
421                writer.get_ref().sync_all()?;
422            }
423
424            // Atomic rename
425            fs::rename(&tmp_path, &self.path)?;
426
427            tracing::debug!(
428                path = %self.path.display(),
429                entries = entries.len(),
430                "saved widget state"
431            );
432
433            Ok(())
434        }
435
436        fn clear(&self) -> StorageResult<()> {
437            if self.path.exists() {
438                fs::remove_file(&self.path)?;
439            }
440            Ok(())
441        }
442
443        fn is_available(&self) -> bool {
444            // Check if we can write to the directory
445            if let Some(parent) = self.path.parent() {
446                if !parent.exists() {
447                    return std::fs::create_dir_all(parent).is_ok();
448                }
449                // Check write permission (try to create temp file)
450                let test_path = parent.join(".ftui_test_write");
451                if std::fs::write(&test_path, b"test").is_ok() {
452                    let _ = std::fs::remove_file(&test_path);
453                    return true;
454                }
455            }
456            false
457        }
458    }
459
460    impl fmt::Debug for FileStorage {
461        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462            f.debug_struct("FileStorage")
463                .field("path", &self.path)
464                .finish()
465        }
466    }
467}
468
469#[cfg(feature = "state-persistence")]
470pub use file_storage::FileStorage;
471
472// ─────────────────────────────────────────────────────────────────────────────
473// State Registry
474// ─────────────────────────────────────────────────────────────────────────────
475
476/// Central registry for widget state persistence.
477///
478/// The registry maintains an in-memory cache of widget states and delegates
479/// to a [`StorageBackend`] for persistence. It provides the main API for
480/// save/restore operations.
481///
482/// # Thread Safety
483///
484/// The registry is `Send + Sync` and uses internal locking for thread-safe access.
485///
486/// # Example
487///
488/// ```ignore
489/// use ftui_runtime::state_persistence::{StateRegistry, MemoryStorage};
490///
491/// // Create registry with memory storage
492/// let registry = StateRegistry::new(Box::new(MemoryStorage::new()));
493///
494/// // Load state for a widget
495/// if let Some(entry) = registry.get("ScrollView::main") {
496///     // Deserialize and restore...
497/// }
498///
499/// // Save state
500/// registry.set("ScrollView::main", 1, serialized_data);
501/// registry.flush()?;
502/// ```
503pub struct StateRegistry {
504    backend: Box<dyn StorageBackend>,
505    cache: RwLock<HashMap<String, StoredEntry>>,
506    dirty: RwLock<bool>,
507}
508
509impl StateRegistry {
510    /// Create a new registry with the given storage backend.
511    ///
512    /// Does not automatically load from storage; call [`load`](Self::load) first.
513    #[must_use]
514    pub fn new(backend: Box<dyn StorageBackend>) -> Self {
515        Self {
516            backend,
517            cache: RwLock::new(HashMap::new()),
518            dirty: RwLock::new(false),
519        }
520    }
521
522    /// Create a registry with memory storage (ephemeral, for testing).
523    #[must_use]
524    pub fn in_memory() -> Self {
525        Self::new(Box::new(MemoryStorage::new()))
526    }
527
528    /// Create a registry with file storage at the given path.
529    #[cfg(feature = "state-persistence")]
530    #[must_use]
531    pub fn with_file(path: impl AsRef<std::path::Path>) -> Self {
532        Self::new(Box::new(FileStorage::new(path)))
533    }
534
535    /// Load all state from the storage backend.
536    ///
537    /// This replaces the in-memory cache with stored data.
538    /// Safe to call multiple times; later calls refresh the cache.
539    pub fn load(&self) -> StorageResult<usize> {
540        let entries = self.backend.load_all()?;
541        let count = entries.len();
542
543        let mut cache = self
544            .cache
545            .write()
546            .map_err(|_| StorageError::Corruption("cache lock poisoned".into()))?;
547        *cache = entries;
548
549        let mut dirty = self
550            .dirty
551            .write()
552            .map_err(|_| StorageError::Corruption("dirty lock poisoned".into()))?;
553        *dirty = false;
554
555        tracing::debug!(backend = %self.backend.name(), count, "loaded widget state");
556        Ok(count)
557    }
558
559    /// Flush dirty state to the storage backend.
560    ///
561    /// Only writes if changes have been made since last flush.
562    /// Returns `Ok(true)` if data was written, `Ok(false)` if no changes.
563    pub fn flush(&self) -> StorageResult<bool> {
564        let dirty = {
565            let guard = self
566                .dirty
567                .read()
568                .map_err(|_| StorageError::Corruption("dirty lock poisoned".into()))?;
569            *guard
570        };
571
572        if !dirty {
573            return Ok(false);
574        }
575
576        let cache_snapshot = {
577            let cache_guard = self
578                .cache
579                .read()
580                .map_err(|_| StorageError::Corruption("cache lock poisoned".into()))?;
581            cache_guard.clone()
582        };
583
584        self.backend.save_all(&cache_snapshot)?;
585
586        let cache_guard = self
587            .cache
588            .read()
589            .map_err(|_| StorageError::Corruption("cache lock poisoned".into()))?;
590        let mut dirty_guard = self
591            .dirty
592            .write()
593            .map_err(|_| StorageError::Corruption("dirty lock poisoned".into()))?;
594        *dirty_guard = *cache_guard != cache_snapshot;
595
596        Ok(true)
597    }
598
599    /// Get a stored state entry by canonical key.
600    ///
601    /// Returns `None` if no state exists for the key.
602    #[must_use]
603    pub fn get(&self, key: &str) -> Option<StoredEntry> {
604        let cache = self.cache.read().ok()?;
605        cache.get(key).cloned()
606    }
607
608    /// Set a state entry.
609    ///
610    /// Marks the registry as dirty; call [`flush`](Self::flush) to persist.
611    pub fn set(&self, key: impl Into<String>, version: u32, data: Vec<u8>) {
612        let key = key.into();
613        if let Ok(mut cache) = self.cache.write() {
614            cache.insert(key.clone(), StoredEntry { key, version, data });
615            if let Ok(mut dirty) = self.dirty.write() {
616                *dirty = true;
617            }
618        }
619    }
620
621    /// Remove a state entry.
622    ///
623    /// Returns the removed entry if it existed.
624    pub fn remove(&self, key: &str) -> Option<StoredEntry> {
625        let result = self.cache.write().ok()?.remove(key);
626        if result.is_some()
627            && let Ok(mut dirty) = self.dirty.write()
628        {
629            *dirty = true;
630        }
631        result
632    }
633
634    /// Clear all state from both cache and storage.
635    pub fn clear(&self) -> StorageResult<()> {
636        self.backend.clear()?;
637        if let Ok(mut cache) = self.cache.write() {
638            cache.clear();
639        }
640        if let Ok(mut dirty) = self.dirty.write() {
641            *dirty = false;
642        }
643        Ok(())
644    }
645
646    /// Get the number of cached entries.
647    #[must_use]
648    pub fn len(&self) -> usize {
649        self.cache.read().map(|c| c.len()).unwrap_or(0)
650    }
651
652    /// Check if the cache is empty.
653    #[must_use]
654    pub fn is_empty(&self) -> bool {
655        self.len() == 0
656    }
657
658    /// Check if there are unsaved changes.
659    #[must_use]
660    pub fn is_dirty(&self) -> bool {
661        self.dirty.read().map(|d| *d).unwrap_or(false)
662    }
663
664    /// Get the backend name for logging.
665    #[must_use]
666    pub fn backend_name(&self) -> &str {
667        self.backend.name()
668    }
669
670    /// Check if the storage backend is available.
671    #[must_use]
672    pub fn is_available(&self) -> bool {
673        self.backend.is_available()
674    }
675
676    /// Get all cached keys.
677    #[must_use]
678    pub fn keys(&self) -> Vec<String> {
679        self.cache
680            .read()
681            .map(|c| c.keys().cloned().collect())
682            .unwrap_or_default()
683    }
684}
685
686impl fmt::Debug for StateRegistry {
687    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688        f.debug_struct("StateRegistry")
689            .field("backend", &self.backend.name())
690            .field("entries", &self.len())
691            .field("dirty", &self.is_dirty())
692            .finish()
693    }
694}
695
696// Make it Arc-able for shared ownership
697impl StateRegistry {
698    /// Wrap in Arc for shared ownership.
699    #[must_use]
700    pub fn shared(self) -> Arc<Self> {
701        Arc::new(self)
702    }
703}
704
705// ─────────────────────────────────────────────────────────────────────────────
706// Statistics and Diagnostics
707// ─────────────────────────────────────────────────────────────────────────────
708
709/// Statistics about the state registry.
710#[derive(Clone, Debug, Default)]
711pub struct RegistryStats {
712    /// Number of entries in cache.
713    pub entry_count: usize,
714    /// Total bytes of state data.
715    pub total_bytes: usize,
716    /// Whether there are unsaved changes.
717    pub dirty: bool,
718    /// Backend name.
719    pub backend: String,
720}
721
722impl StateRegistry {
723    /// Get statistics about the registry.
724    #[must_use]
725    pub fn stats(&self) -> RegistryStats {
726        let (entry_count, total_bytes) = self
727            .cache
728            .read()
729            .map(|c| {
730                let count = c.len();
731                let bytes: usize = c.values().map(|e| e.data.len()).sum();
732                (count, bytes)
733            })
734            .unwrap_or((0, 0));
735
736        RegistryStats {
737            entry_count,
738            total_bytes,
739            dirty: self.is_dirty(),
740            backend: self.backend.name().to_string(),
741        }
742    }
743}
744
745// ─────────────────────────────────────────────────────────────────────────────
746// Tests
747// ─────────────────────────────────────────────────────────────────────────────
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752    use std::sync::atomic::{AtomicBool, Ordering};
753    use std::sync::{Mutex, Weak};
754    use std::thread;
755    use web_time::Duration;
756
757    #[derive(Default)]
758    struct ReentrantFlushBackendState {
759        registry: Mutex<Option<Weak<StateRegistry>>>,
760        injected_during_save: AtomicBool,
761        saved_entries: RwLock<HashMap<String, StoredEntry>>,
762    }
763
764    impl ReentrantFlushBackendState {
765        fn bind_registry(&self, registry: &Arc<StateRegistry>) {
766            *self.registry.lock().unwrap_or_else(|e| e.into_inner()) =
767                Some(Arc::downgrade(registry));
768        }
769
770        fn saved_entries(&self) -> HashMap<String, StoredEntry> {
771            self.saved_entries
772                .read()
773                .unwrap_or_else(|e| e.into_inner())
774                .clone()
775        }
776    }
777
778    #[derive(Clone)]
779    struct ReentrantFlushBackend {
780        state: Arc<ReentrantFlushBackendState>,
781    }
782
783    impl StorageBackend for ReentrantFlushBackend {
784        fn name(&self) -> &str {
785            "ReentrantFlushBackend"
786        }
787
788        fn load_all(&self) -> StorageResult<HashMap<String, StoredEntry>> {
789            Ok(self.state.saved_entries())
790        }
791
792        fn save_all(&self, entries: &HashMap<String, StoredEntry>) -> StorageResult<()> {
793            *self
794                .state
795                .saved_entries
796                .write()
797                .map_err(|_| StorageError::Corruption("saved entries lock poisoned".into()))? =
798                entries.clone();
799
800            if !self.state.injected_during_save.swap(true, Ordering::SeqCst)
801                && let Some(registry) = self
802                    .state
803                    .registry
804                    .lock()
805                    .unwrap_or_else(|e| e.into_inner())
806                    .as_ref()
807                    .and_then(Weak::upgrade)
808            {
809                registry.set("backend::late", 2, b"late".to_vec());
810            }
811
812            Ok(())
813        }
814
815        fn clear(&self) -> StorageResult<()> {
816            self.state
817                .saved_entries
818                .write()
819                .map_err(|_| StorageError::Corruption("saved entries lock poisoned".into()))?
820                .clear();
821            Ok(())
822        }
823    }
824
825    #[test]
826    fn memory_storage_basic_operations() {
827        let storage = MemoryStorage::new();
828
829        // Initially empty
830        let entries = storage.load_all().unwrap();
831        assert!(entries.is_empty());
832
833        // Save some entries
834        let mut data = HashMap::new();
835        data.insert(
836            "key1".to_string(),
837            StoredEntry {
838                key: "key1".to_string(),
839                version: 1,
840                data: b"hello".to_vec(),
841            },
842        );
843        storage.save_all(&data).unwrap();
844
845        // Load back
846        let loaded = storage.load_all().unwrap();
847        assert_eq!(loaded.len(), 1);
848        assert_eq!(loaded["key1"].data, b"hello");
849
850        // Clear
851        storage.clear().unwrap();
852        assert!(storage.load_all().unwrap().is_empty());
853    }
854
855    #[test]
856    fn memory_storage_with_entries() {
857        let mut entries = HashMap::new();
858        entries.insert(
859            "test".to_string(),
860            StoredEntry {
861                key: "test".to_string(),
862                version: 2,
863                data: vec![1, 2, 3],
864            },
865        );
866        let storage = MemoryStorage::with_entries(entries);
867
868        let loaded = storage.load_all().unwrap();
869        assert_eq!(loaded.len(), 1);
870        assert_eq!(loaded["test"].version, 2);
871    }
872
873    #[test]
874    fn registry_basic_operations() {
875        let registry = StateRegistry::in_memory();
876
877        // Initially empty
878        assert!(registry.is_empty());
879        assert!(!registry.is_dirty());
880
881        // Set an entry
882        registry.set("widget::1", 1, b"data".to_vec());
883        assert_eq!(registry.len(), 1);
884        assert!(registry.is_dirty());
885
886        // Get the entry
887        let entry = registry.get("widget::1").unwrap();
888        assert_eq!(entry.version, 1);
889        assert_eq!(entry.data, b"data");
890
891        // Get non-existent
892        assert!(registry.get("widget::99").is_none());
893
894        // Flush
895        assert!(registry.flush().unwrap());
896        assert!(!registry.is_dirty());
897
898        // No-op flush when clean
899        assert!(!registry.flush().unwrap());
900
901        // Remove
902        let removed = registry.remove("widget::1").unwrap();
903        assert_eq!(removed.data, b"data");
904        assert!(registry.is_empty());
905        assert!(registry.is_dirty());
906    }
907
908    #[test]
909    fn registry_load_and_flush() {
910        let storage = MemoryStorage::new();
911        let mut initial = HashMap::new();
912        initial.insert(
913            "pre::existing".to_string(),
914            StoredEntry {
915                key: "pre::existing".to_string(),
916                version: 5,
917                data: b"old".to_vec(),
918            },
919        );
920        storage.save_all(&initial).unwrap();
921
922        let registry = StateRegistry::new(Box::new(storage));
923
924        // Load existing data
925        let count = registry.load().unwrap();
926        assert_eq!(count, 1);
927        assert!(!registry.is_dirty());
928
929        let entry = registry.get("pre::existing").unwrap();
930        assert_eq!(entry.version, 5);
931    }
932
933    #[test]
934    fn registry_clear() {
935        let registry = StateRegistry::in_memory();
936        registry.set("a", 1, vec![]);
937        registry.set("b", 1, vec![]);
938        assert_eq!(registry.len(), 2);
939
940        registry.clear().unwrap();
941        assert!(registry.is_empty());
942        assert!(!registry.is_dirty());
943    }
944
945    #[test]
946    fn registry_keys() {
947        let registry = StateRegistry::in_memory();
948        registry.set("widget::a", 1, vec![]);
949        registry.set("widget::b", 1, vec![]);
950
951        let mut keys = registry.keys();
952        keys.sort();
953        assert_eq!(keys, vec!["widget::a", "widget::b"]);
954    }
955
956    #[test]
957    fn registry_stats() {
958        let registry = StateRegistry::in_memory();
959        registry.set("x", 1, vec![1, 2, 3, 4, 5]);
960        registry.set("y", 1, vec![6, 7, 8]);
961
962        let stats = registry.stats();
963        assert_eq!(stats.entry_count, 2);
964        assert_eq!(stats.total_bytes, 8);
965        assert!(stats.dirty);
966        assert_eq!(stats.backend, "MemoryStorage");
967    }
968
969    #[test]
970    fn registry_shared() {
971        let registry = StateRegistry::in_memory().shared();
972        registry.set("test", 1, vec![42]);
973
974        let registry2 = Arc::clone(&registry);
975        assert_eq!(registry2.get("test").unwrap().data, vec![42]);
976    }
977
978    #[test]
979    fn storage_error_display() {
980        let io_err = StorageError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
981        assert!(io_err.to_string().contains("I/O error"));
982
983        let corrupt = StorageError::Corruption("bad data".into());
984        assert!(corrupt.to_string().contains("corruption"));
985
986        let unavail = StorageError::Unavailable("no backend".into());
987        assert!(unavail.to_string().contains("unavailable"));
988    }
989
990    // ── StorageError ────────────────────────────────────────────────────
991
992    #[test]
993    fn storage_error_source_io() {
994        let err = StorageError::Io(std::io::Error::new(
995            std::io::ErrorKind::BrokenPipe,
996            "broken",
997        ));
998        let source = std::error::Error::source(&err);
999        assert!(source.is_some());
1000    }
1001
1002    #[test]
1003    fn storage_error_source_corruption_none() {
1004        let err = StorageError::Corruption("test".into());
1005        assert!(std::error::Error::source(&err).is_none());
1006    }
1007
1008    #[test]
1009    fn storage_error_source_unavailable_none() {
1010        let err = StorageError::Unavailable("test".into());
1011        assert!(std::error::Error::source(&err).is_none());
1012    }
1013
1014    #[test]
1015    fn storage_error_from_io_error() {
1016        let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
1017        let err: StorageError = io_err.into();
1018        match err {
1019            StorageError::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::TimedOut),
1020            _ => panic!("expected Io variant"),
1021        }
1022    }
1023
1024    #[test]
1025    fn storage_error_debug_format() {
1026        let err = StorageError::Corruption("test".into());
1027        let dbg = format!("{:?}", err);
1028        assert!(dbg.contains("Corruption"));
1029    }
1030
1031    // ── MemoryStorage ───────────────────────────────────────────────────
1032
1033    #[test]
1034    fn memory_storage_name() {
1035        let storage = MemoryStorage::new();
1036        assert_eq!(storage.name(), "MemoryStorage");
1037    }
1038
1039    #[test]
1040    fn memory_storage_is_available() {
1041        let storage = MemoryStorage::new();
1042        assert!(storage.is_available());
1043    }
1044
1045    #[test]
1046    fn memory_storage_debug_format() {
1047        let storage = MemoryStorage::new();
1048        let dbg = format!("{:?}", storage);
1049        assert!(dbg.contains("MemoryStorage"));
1050        assert!(dbg.contains("entries"));
1051    }
1052
1053    #[test]
1054    fn memory_storage_debug_shows_count() {
1055        let mut entries = HashMap::new();
1056        entries.insert(
1057            "a".to_string(),
1058            StoredEntry {
1059                key: "a".to_string(),
1060                version: 1,
1061                data: vec![],
1062            },
1063        );
1064        let storage = MemoryStorage::with_entries(entries);
1065        let dbg = format!("{:?}", storage);
1066        assert!(dbg.contains("1"));
1067    }
1068
1069    #[test]
1070    fn memory_storage_save_replaces_all() {
1071        let storage = MemoryStorage::new();
1072
1073        let mut data1 = HashMap::new();
1074        data1.insert(
1075            "old".to_string(),
1076            StoredEntry {
1077                key: "old".to_string(),
1078                version: 1,
1079                data: vec![],
1080            },
1081        );
1082        storage.save_all(&data1).unwrap();
1083
1084        let mut data2 = HashMap::new();
1085        data2.insert(
1086            "new".to_string(),
1087            StoredEntry {
1088                key: "new".to_string(),
1089                version: 2,
1090                data: vec![],
1091            },
1092        );
1093        storage.save_all(&data2).unwrap();
1094
1095        let loaded = storage.load_all().unwrap();
1096        assert_eq!(loaded.len(), 1);
1097        assert!(loaded.contains_key("new"));
1098        assert!(!loaded.contains_key("old"));
1099    }
1100
1101    // ── StateRegistry ───────────────────────────────────────────────────
1102
1103    #[test]
1104    fn registry_backend_name() {
1105        let registry = StateRegistry::in_memory();
1106        assert_eq!(registry.backend_name(), "MemoryStorage");
1107    }
1108
1109    #[test]
1110    fn registry_is_available() {
1111        let registry = StateRegistry::in_memory();
1112        assert!(registry.is_available());
1113    }
1114
1115    #[test]
1116    fn registry_debug_format() {
1117        let registry = StateRegistry::in_memory();
1118        registry.set("x", 1, vec![]);
1119        let dbg = format!("{:?}", registry);
1120        assert!(dbg.contains("StateRegistry"));
1121        assert!(dbg.contains("MemoryStorage"));
1122        assert!(dbg.contains("dirty"));
1123    }
1124
1125    #[test]
1126    fn registry_set_overwrites() {
1127        let registry = StateRegistry::in_memory();
1128        registry.set("k", 1, b"first".to_vec());
1129        registry.set("k", 2, b"second".to_vec());
1130
1131        assert_eq!(registry.len(), 1);
1132        let entry = registry.get("k").unwrap();
1133        assert_eq!(entry.version, 2);
1134        assert_eq!(entry.data, b"second");
1135    }
1136
1137    #[test]
1138    fn registry_remove_nonexistent_returns_none() {
1139        let registry = StateRegistry::in_memory();
1140        assert!(registry.remove("nonexistent").is_none());
1141    }
1142
1143    #[test]
1144    fn registry_load_replaces_cache() {
1145        let storage = MemoryStorage::new();
1146        let mut initial = HashMap::new();
1147        initial.insert(
1148            "backend_key".to_string(),
1149            StoredEntry {
1150                key: "backend_key".to_string(),
1151                version: 1,
1152                data: b"from_backend".to_vec(),
1153            },
1154        );
1155        storage.save_all(&initial).unwrap();
1156
1157        let registry = StateRegistry::new(Box::new(storage));
1158        registry.set("local_key", 1, b"local".to_vec());
1159        assert!(registry.get("local_key").is_some());
1160
1161        // Load replaces entire cache
1162        registry.load().unwrap();
1163        assert!(registry.get("local_key").is_none());
1164        assert!(registry.get("backend_key").is_some());
1165    }
1166
1167    #[test]
1168    fn registry_load_clears_dirty_flag() {
1169        let registry = StateRegistry::in_memory();
1170        registry.set("x", 1, vec![]);
1171        assert!(registry.is_dirty());
1172
1173        registry.load().unwrap();
1174        assert!(!registry.is_dirty());
1175    }
1176
1177    #[test]
1178    fn registry_flush_persists_to_backend() {
1179        let registry = StateRegistry::in_memory();
1180        registry.set("widget::foo", 3, b"bar".to_vec());
1181        registry.flush().unwrap();
1182
1183        // Load fresh and verify data survived
1184        let count = registry.load().unwrap();
1185        assert_eq!(count, 1);
1186        let entry = registry.get("widget::foo").unwrap();
1187        assert_eq!(entry.version, 3);
1188        assert_eq!(entry.data, b"bar");
1189    }
1190
1191    #[test]
1192    fn registry_flush_drops_cache_lock_before_backend_save() {
1193        let backend_state = Arc::new(ReentrantFlushBackendState::default());
1194        let registry = Arc::new(StateRegistry::new(Box::new(ReentrantFlushBackend {
1195            state: Arc::clone(&backend_state),
1196        })));
1197        backend_state.bind_registry(&registry);
1198
1199        registry.set("widget::foo", 1, b"bar".to_vec());
1200
1201        let (done_tx, done_rx) = std::sync::mpsc::channel();
1202        let registry_for_thread = Arc::clone(&registry);
1203        let handle = thread::spawn(move || {
1204            let result = registry_for_thread.flush();
1205            done_tx.send(result).expect("flush result");
1206        });
1207
1208        done_rx
1209            .recv_timeout(Duration::from_secs(1))
1210            .expect("flush should complete without deadlocking")
1211            .expect("flush succeeds");
1212        handle.join().expect("flush thread");
1213
1214        let saved_entries = backend_state.saved_entries();
1215        assert!(saved_entries.contains_key("widget::foo"));
1216    }
1217
1218    #[test]
1219    fn registry_flush_preserves_dirty_when_backend_mutates_registry() {
1220        let backend_state = Arc::new(ReentrantFlushBackendState::default());
1221        let registry = Arc::new(StateRegistry::new(Box::new(ReentrantFlushBackend {
1222            state: Arc::clone(&backend_state),
1223        })));
1224        backend_state.bind_registry(&registry);
1225
1226        registry.set("widget::foo", 1, b"bar".to_vec());
1227        assert!(registry.flush().unwrap());
1228
1229        let first_saved = backend_state.saved_entries();
1230        assert!(first_saved.contains_key("widget::foo"));
1231        assert!(!first_saved.contains_key("backend::late"));
1232        assert!(registry.is_dirty());
1233        assert_eq!(registry.get("backend::late").unwrap().data, b"late");
1234
1235        assert!(registry.flush().unwrap());
1236
1237        let second_saved = backend_state.saved_entries();
1238        assert!(second_saved.contains_key("backend::late"));
1239        assert!(!registry.is_dirty());
1240    }
1241
1242    #[test]
1243    fn registry_multiple_keys() {
1244        let registry = StateRegistry::in_memory();
1245        registry.set("a", 1, vec![1]);
1246        registry.set("b", 2, vec![2]);
1247        registry.set("c", 3, vec![3]);
1248
1249        assert_eq!(registry.len(), 3);
1250        assert!(!registry.is_empty());
1251
1252        let mut keys = registry.keys();
1253        keys.sort();
1254        assert_eq!(keys, vec!["a", "b", "c"]);
1255    }
1256
1257    #[test]
1258    fn registry_remove_marks_dirty() {
1259        let registry = StateRegistry::in_memory();
1260        registry.set("x", 1, vec![]);
1261        registry.flush().unwrap();
1262        assert!(!registry.is_dirty());
1263
1264        registry.remove("x");
1265        assert!(registry.is_dirty());
1266    }
1267
1268    #[test]
1269    fn registry_clear_after_set_and_flush() {
1270        let registry = StateRegistry::in_memory();
1271        registry.set("a", 1, vec![]);
1272        registry.flush().unwrap();
1273        registry.clear().unwrap();
1274
1275        assert!(registry.is_empty());
1276        assert!(!registry.is_dirty());
1277
1278        // Backend is also cleared
1279        let count = registry.load().unwrap();
1280        assert_eq!(count, 0);
1281    }
1282
1283    // ── RegistryStats ───────────────────────────────────────────────────
1284
1285    #[test]
1286    fn registry_stats_default() {
1287        let stats = RegistryStats::default();
1288        assert_eq!(stats.entry_count, 0);
1289        assert_eq!(stats.total_bytes, 0);
1290        assert!(!stats.dirty);
1291        assert_eq!(stats.backend, "");
1292    }
1293
1294    #[test]
1295    fn registry_stats_empty() {
1296        let registry = StateRegistry::in_memory();
1297        let stats = registry.stats();
1298        assert_eq!(stats.entry_count, 0);
1299        assert_eq!(stats.total_bytes, 0);
1300        assert!(!stats.dirty);
1301    }
1302
1303    // ── StoredEntry ─────────────────────────────────────────────────────
1304
1305    #[test]
1306    fn stored_entry_clone() {
1307        let entry = StoredEntry {
1308            key: "test".to_string(),
1309            version: 7,
1310            data: vec![1, 2, 3],
1311        };
1312        let cloned = entry.clone();
1313        assert_eq!(cloned.key, "test");
1314        assert_eq!(cloned.version, 7);
1315        assert_eq!(cloned.data, vec![1, 2, 3]);
1316    }
1317
1318    #[test]
1319    fn stored_entry_debug() {
1320        let entry = StoredEntry {
1321            key: "k".to_string(),
1322            version: 1,
1323            data: vec![],
1324        };
1325        let dbg = format!("{:?}", entry);
1326        assert!(dbg.contains("StoredEntry"));
1327    }
1328
1329    // ── Arc shared access ───────────────────────────────────────────────
1330
1331    #[test]
1332    fn registry_shared_concurrent_access() {
1333        let registry = StateRegistry::in_memory().shared();
1334        let r2 = Arc::clone(&registry);
1335
1336        registry.set("from_1", 1, vec![10]);
1337        r2.set("from_2", 1, vec![20]);
1338
1339        assert_eq!(registry.len(), 2);
1340        assert!(r2.get("from_1").is_some());
1341        assert!(registry.get("from_2").is_some());
1342    }
1343}
1344
1345#[cfg(all(test, feature = "state-persistence"))]
1346mod file_storage_tests {
1347    use super::*;
1348    use std::io::Write;
1349    use tempfile::TempDir;
1350
1351    #[test]
1352    fn file_storage_round_trip() {
1353        let tmp = TempDir::new().unwrap();
1354        let path = tmp.path().join("state.json");
1355        let storage = FileStorage::new(&path);
1356
1357        // Save
1358        let mut entries = HashMap::new();
1359        entries.insert(
1360            "widget::test".to_string(),
1361            StoredEntry {
1362                key: "widget::test".to_string(),
1363                version: 3,
1364                data: b"hello world".to_vec(),
1365            },
1366        );
1367        storage.save_all(&entries).unwrap();
1368
1369        // File should exist
1370        assert!(path.exists());
1371
1372        // Load back
1373        let loaded = storage.load_all().unwrap();
1374        assert_eq!(loaded.len(), 1);
1375        assert_eq!(loaded["widget::test"].version, 3);
1376        assert_eq!(loaded["widget::test"].data, b"hello world");
1377    }
1378
1379    #[test]
1380    fn file_storage_load_nonexistent() {
1381        let tmp = TempDir::new().unwrap();
1382        let path = tmp.path().join("does_not_exist.json");
1383        let storage = FileStorage::new(&path);
1384
1385        let entries = storage.load_all().unwrap();
1386        assert!(entries.is_empty());
1387    }
1388
1389    #[test]
1390    fn file_storage_clear() {
1391        let tmp = TempDir::new().unwrap();
1392        let path = tmp.path().join("state.json");
1393
1394        // Create file
1395        std::fs::write(&path, "{}").unwrap();
1396        assert!(path.exists());
1397
1398        let storage = FileStorage::new(&path);
1399        storage.clear().unwrap();
1400        assert!(!path.exists());
1401    }
1402
1403    #[test]
1404    fn file_storage_creates_parent_dirs() {
1405        let tmp = TempDir::new().unwrap();
1406        let path = tmp.path().join("nested").join("dirs").join("state.json");
1407        let storage = FileStorage::new(&path);
1408
1409        let mut entries = HashMap::new();
1410        entries.insert(
1411            "k".to_string(),
1412            StoredEntry {
1413                key: "k".to_string(),
1414                version: 1,
1415                data: vec![],
1416            },
1417        );
1418        storage.save_all(&entries).unwrap();
1419        assert!(path.exists());
1420    }
1421
1422    #[test]
1423    fn file_storage_handles_corrupt_entry() {
1424        let tmp = TempDir::new().unwrap();
1425        let path = tmp.path().join("state.json");
1426
1427        // Write valid JSON but with invalid base64
1428        let mut f = std::fs::File::create(&path).unwrap();
1429        writeln!(
1430            f,
1431            r#"{{"format_version":1,"entries":{{"bad":{{"version":1,"data_base64":"!!invalid!!"}},"good":{{"version":1,"data_base64":"aGVsbG8="}}}}}}"#
1432        )
1433        .unwrap();
1434
1435        let storage = FileStorage::new(&path);
1436        let loaded = storage.load_all().unwrap();
1437
1438        // Bad entry skipped, good entry loaded
1439        assert_eq!(loaded.len(), 1);
1440        assert!(loaded.contains_key("good"));
1441        assert_eq!(loaded["good"].data, b"hello");
1442    }
1443}