state_store/lib.rs
1//! Internal implementation detail of [`sonos-sdk`](https://crates.io/crates/sonos-sdk). Not intended for direct use.
2//!
3//! Generic State Management Library
4//!
5//! A type-safe, generic state management library with change detection
6//! and blocking iteration patterns.
7//!
8//! # Features
9//!
10//! - **Type-safe Storage**: Store and retrieve strongly-typed properties
11//! - **Change Detection**: Only emit events when values actually change
12//! - **Watch Pattern**: Register interest in specific properties
13//! - **Blocking Iteration**: Consume change events via blocking iterators
14//! - **Generic Entity IDs**: Use any hashable type as entity identifiers
15//!
16//! # Quick Start
17//!
18//! ```rust,ignore
19//! use state_store::{StateStore, Property};
20//!
21//! // Define a property type
22//! #[derive(Clone, PartialEq, Debug)]
23//! struct Temperature(f32);
24//!
25//! impl Property for Temperature {
26//! const KEY: &'static str = "temperature";
27//! }
28//!
29//! // Create a store with String entity IDs
30//! let store = StateStore::<String>::new();
31//!
32//! // Watch for temperature changes on sensor-1
33//! store.watch("sensor-1".to_string(), Temperature::KEY);
34//!
35//! // Set a value (will emit change event since watched)
36//! store.set(&"sensor-1".to_string(), Temperature(72.5));
37//!
38//! // Get current value
39//! let temp = store.get::<Temperature>(&"sensor-1".to_string());
40//! assert_eq!(temp, Some(Temperature(72.5)));
41//! ```
42//!
43//! # Iteration Patterns
44//!
45//! ```rust,ignore
46//! // Blocking iteration (waits for events)
47//! for event in store.iter() {
48//! println!("{} changed on {:?}", event.property_key, event.entity_id);
49//! }
50//!
51//! // Non-blocking (processes available events)
52//! for event in store.iter().try_iter() {
53//! println!("Event: {:?}", event);
54//! }
55//!
56//! // With timeout
57//! use std::time::Duration;
58//! if let Some(event) = store.iter().recv_timeout(Duration::from_secs(1)) {
59//! println!("Got event: {:?}", event);
60//! }
61//! ```
62//!
63//! # Architecture
64//!
65//! ```text
66//! StateStore<Id>
67//! │
68//! ├── entities: HashMap<Id, PropertyBag>
69//! │ │
70//! │ └── PropertyBag: HashMap<TypeId, Box<dyn Any>>
71//! │
72//! ├── watched: HashSet<(Id, property_key)>
73//! │
74//! └── event_channel: mpsc::channel<ChangeEvent<Id>>
75//! │
76//! └── ChangeIterator<Id>
77//! ```
78
79// Modules
80pub mod event;
81pub mod iter;
82pub mod property;
83pub mod store;
84
85// Re-exports - Public API
86pub use event::ChangeEvent;
87pub use iter::{ChangeIterator, TimeoutIter, TryIter};
88pub use property::Property;
89pub use store::{PropertyBag, StateStore};
90
91/// Prelude for convenient imports
92pub mod prelude {
93 pub use crate::event::ChangeEvent;
94 pub use crate::iter::ChangeIterator;
95 pub use crate::property::Property;
96 pub use crate::store::{PropertyBag, StateStore};
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[derive(Clone, PartialEq, Debug)]
104 struct Volume(u8);
105
106 impl Property for Volume {
107 const KEY: &'static str = "volume";
108 }
109
110 #[derive(Clone, PartialEq, Debug)]
111 struct Mute(bool);
112
113 impl Property for Mute {
114 const KEY: &'static str = "mute";
115 }
116
117 #[test]
118 fn test_full_workflow() {
119 // Create store
120 let store = StateStore::<String>::new();
121
122 // Add some properties
123 store.set(&"speaker-1".to_string(), Volume(50));
124 store.set(&"speaker-1".to_string(), Mute(false));
125
126 // Verify values
127 assert_eq!(
128 store.get::<Volume>(&"speaker-1".to_string()),
129 Some(Volume(50))
130 );
131 assert_eq!(
132 store.get::<Mute>(&"speaker-1".to_string()),
133 Some(Mute(false))
134 );
135
136 // Watch and verify events
137 store.watch("speaker-1".to_string(), Volume::KEY);
138
139 // Change value
140 store.set(&"speaker-1".to_string(), Volume(75));
141
142 // Get event
143 let event = store
144 .iter()
145 .recv_timeout(std::time::Duration::from_millis(100));
146 assert!(event.is_some());
147 assert_eq!(event.unwrap().property_key, Volume::KEY);
148 }
149
150 #[test]
151 fn test_multiple_entities() {
152 let store = StateStore::<String>::new();
153
154 store.set(&"speaker-1".to_string(), Volume(50));
155 store.set(&"speaker-2".to_string(), Volume(75));
156 store.set(&"speaker-3".to_string(), Volume(100));
157
158 assert_eq!(store.entity_count(), 3);
159 assert_eq!(
160 store.get::<Volume>(&"speaker-1".to_string()),
161 Some(Volume(50))
162 );
163 assert_eq!(
164 store.get::<Volume>(&"speaker-2".to_string()),
165 Some(Volume(75))
166 );
167 assert_eq!(
168 store.get::<Volume>(&"speaker-3".to_string()),
169 Some(Volume(100))
170 );
171 }
172
173 #[test]
174 fn test_store_clone_shares_state() {
175 let store1 = StateStore::<String>::new();
176 let store2 = store1.clone();
177
178 store1.set(&"speaker-1".to_string(), Volume(50));
179
180 // Both clones see the same data
181 assert_eq!(
182 store2.get::<Volume>(&"speaker-1".to_string()),
183 Some(Volume(50))
184 );
185 }
186}