1use std::fmt;
2use thiserror::Error;
3
4pub type StateResult<T> = Result<T, StateError>;
6
7#[derive(Error, Debug, Clone, PartialEq)]
9#[cfg_attr(
10 feature = "serialization",
11 derive(serde::Serialize, serde::Deserialize)
12)]
13pub enum StateError {
14 #[error("Store not found: {name}")]
15 StoreNotFound { name: String },
16
17 #[error("Invalid state transition from {from} to {to}")]
18 InvalidTransition { from: String, to: String },
19
20 #[error("Guard condition failed: {reason}")]
21 GuardFailed { reason: String },
22
23 #[error("Serialization error: {message}")]
24 SerializationError { message: String },
25
26 #[error("Validation error: {field} - {message}")]
27 ValidationError { field: String, message: String },
28
29 #[error("Machine not initialized")]
30 MachineNotInitialized,
31
32 #[error("Context error: {message}")]
33 ContextError { message: String },
34
35 #[error("Unknown error: {message}")]
36 Unknown { message: String },
37}
38
39impl StateError {
40 pub fn store_not_found(name: impl Into<String>) -> Self {
41 Self::StoreNotFound { name: name.into() }
42 }
43
44 pub fn invalid_transition(from: impl Into<String>, to: impl Into<String>) -> Self {
45 Self::InvalidTransition {
46 from: from.into(),
47 to: to.into(),
48 }
49 }
50
51 pub fn guard_failed(reason: impl Into<String>) -> Self {
52 Self::GuardFailed {
53 reason: reason.into(),
54 }
55 }
56
57 pub fn serialization_error(message: impl Into<String>) -> Self {
58 Self::SerializationError {
59 message: message.into(),
60 }
61 }
62
63 pub fn validation_error(field: impl Into<String>, message: impl Into<String>) -> Self {
64 Self::ValidationError {
65 field: field.into(),
66 message: message.into(),
67 }
68 }
69
70 pub fn context_error(message: impl Into<String>) -> Self {
71 Self::ContextError {
72 message: message.into(),
73 }
74 }
75
76 pub fn unknown(message: impl Into<String>) -> Self {
77 Self::Unknown {
78 message: message.into(),
79 }
80 }
81
82 pub fn new(message: impl Into<String>) -> Self {
83 Self::Unknown {
84 message: message.into(),
85 }
86 }
87
88 pub fn custom(message: impl Into<String>) -> Self {
89 Self::Unknown {
90 message: message.into(),
91 }
92 }
93}
94
95#[cfg(feature = "serde_json")]
97impl From<serde_json::Error> for StateError {
98 fn from(err: serde_json::Error) -> Self {
99 Self::SerializationError {
100 message: err.to_string(),
101 }
102 }
103}
104
105#[cfg(feature = "serde_yaml")]
106impl From<serde_yaml::Error> for StateError {
107 fn from(err: serde_yaml::Error) -> Self {
108 Self::SerializationError {
109 message: err.to_string(),
110 }
111 }
112}
113
114impl From<std::io::Error> for StateError {
115 fn from(err: std::io::Error) -> Self {
116 Self::Unknown {
117 message: err.to_string(),
118 }
119 }
120}
121
122impl From<Box<dyn std::error::Error + Send + Sync>> for StateError {
123 fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
124 Self::Unknown {
125 message: err.to_string(),
126 }
127 }
128}
129
130pub type StoreId = String;
132
133pub type MachineId = String;
135
136pub type StateId = String;
138
139pub type EventId = String;
141
142pub struct SubscriptionHandle {
144 id: String,
145 cleanup: Option<Box<dyn FnOnce() + std::marker::Send>>,
146}
147
148impl SubscriptionHandle {
149 pub fn new(cleanup: impl FnOnce() + 'static + std::marker::Send) -> Self {
150 Self {
151 id: format!("sub_{}", js_sys::Math::random()),
152 cleanup: Some(Box::new(cleanup)),
153 }
154 }
155
156 pub fn id(&self) -> &str {
157 &self.id
158 }
159
160 pub fn cancel(mut self) {
161 if let Some(cleanup) = self.cleanup.take() {
162 cleanup();
163 }
164 }
165}
166
167impl Drop for SubscriptionHandle {
168 fn drop(&mut self) {
169 if let Some(cleanup) = self.cleanup.take() {
170 cleanup();
171 }
172 }
173}
174
175#[derive(Debug, Clone)]
177pub struct Config {
178 pub enable_devtools: bool,
179 pub enable_persistence: bool,
180 pub enable_time_travel: bool,
181 pub enable_logging: bool,
182 pub log_level: LogLevel,
183 pub persistence_key: Option<String>,
184}
185
186impl Default for Config {
187 fn default() -> Self {
188 Self {
189 enable_devtools: cfg!(debug_assertions),
190 enable_persistence: false,
191 enable_time_travel: false,
192 enable_logging: cfg!(debug_assertions),
193 log_level: LogLevel::Info,
194 persistence_key: None,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum LogLevel {
202 Trace,
203 Debug,
204 Info,
205 Warn,
206 Error,
207}
208
209impl fmt::Display for LogLevel {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 LogLevel::Trace => write!(f, "TRACE"),
213 LogLevel::Debug => write!(f, "DEBUG"),
214 LogLevel::Info => write!(f, "INFO"),
215 LogLevel::Warn => write!(f, "WARN"),
216 LogLevel::Error => write!(f, "ERROR"),
217 }
218 }
219}
220
221pub trait WithId {
223 fn id(&self) -> String;
224}
225
226pub trait Validate {
228 type Error;
229 fn validate(&self) -> Result<(), Self::Error>;
230}
231
232pub trait Serialize {
234 fn serialize(&self) -> StateResult<String>;
235}
236
237pub trait Deserialize<T> {
239 fn deserialize(data: &str) -> StateResult<T>;
240}
241
242pub mod time {
244 use std::time::{Duration, Instant};
245
246 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
247 pub struct Timeout {
248 duration: Duration,
249 start: Instant,
250 }
251
252 impl Timeout {
253 pub fn new(duration: Duration) -> Self {
254 Self {
255 duration,
256 start: Instant::now(),
257 }
258 }
259
260 pub fn is_expired(&self) -> bool {
261 self.start.elapsed() >= self.duration
262 }
263
264 pub fn remaining(&self) -> Duration {
265 self.duration.saturating_sub(self.start.elapsed())
266 }
267
268 pub fn reset(&mut self) {
269 self.start = Instant::now();
270 }
271 }
272}
273
274pub mod collections {
276 use super::StoreId;
277 use std::collections::HashMap;
278
279 #[derive(Debug, Clone)]
281 pub struct StoreRegistry<T> {
282 stores: HashMap<StoreId, T>,
283 }
284
285 impl<T> StoreRegistry<T> {
286 pub fn new() -> Self {
287 Self {
288 stores: HashMap::new(),
289 }
290 }
291
292 pub fn register(&mut self, id: StoreId, store: T) {
293 self.stores.insert(id, store);
294 }
295
296 pub fn get(&self, id: &StoreId) -> Option<&T> {
297 self.stores.get(id)
298 }
299
300 pub fn get_mut(&mut self, id: &StoreId) -> Option<&mut T> {
301 self.stores.get_mut(id)
302 }
303
304 pub fn remove(&mut self, id: &StoreId) -> Option<T> {
305 self.stores.remove(id)
306 }
307
308 pub fn list(&self) -> impl Iterator<Item = &StoreId> {
309 self.stores.keys()
310 }
311
312 pub fn len(&self) -> usize {
313 self.stores.len()
314 }
315
316 pub fn is_empty(&self) -> bool {
317 self.stores.is_empty()
318 }
319 }
320
321 impl<T> Default for StoreRegistry<T> {
322 fn default() -> Self {
323 Self::new()
324 }
325 }
326
327 pub type MachineRegistry<T> = StoreRegistry<T>;
329}
330
331#[cfg(feature = "serialization")]
332mod serde_support {
333 use super::StateError;
334 use serde::{Deserialize, Serialize};
335
336 impl<T> super::Serialize for T
337 where
338 T: Serialize,
339 {
340 fn serialize(&self) -> super::StateResult<String> {
341 serde_json::to_string(self).map_err(|e| StateError::serialization_error(e.to_string()))
342 }
343 }
344
345 impl<T> super::Deserialize<T> for T
346 where
347 T: for<'de> Deserialize<'de>,
348 {
349 fn deserialize(data: &str) -> super::StateResult<T> {
350 serde_json::from_str(data).map_err(|e| StateError::serialization_error(e.to_string()))
351 }
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use std::time::Duration;
359
360 #[test]
361 fn state_error_creation() {
362 let error = StateError::store_not_found("test_store");
363 assert!(matches!(error, StateError::StoreNotFound { .. }));
364
365 let error = StateError::invalid_transition("idle", "running");
366 assert!(matches!(error, StateError::InvalidTransition { .. }));
367 }
368
369 #[test]
370 fn config_default() {
371 let config = Config::default();
372 assert_eq!(config.enable_devtools, cfg!(debug_assertions));
373 assert!(!config.enable_persistence);
374 }
375
376 #[test]
377 fn subscription_handle_cleanup() {
378 println!("Skipping subscription handle cleanup test - requires WASM environment");
381 }
382
383 #[test]
384 fn timeout_functionality() {
385 let mut timeout = time::Timeout::new(Duration::from_millis(100));
386 assert!(!timeout.is_expired());
387
388 std::thread::sleep(Duration::from_millis(150));
389 assert!(timeout.is_expired());
390
391 timeout.reset();
392 assert!(!timeout.is_expired());
393 }
394
395 #[test]
396 fn store_registry() {
397 let mut registry = collections::StoreRegistry::new();
398 assert!(registry.is_empty());
399
400 registry.register("store1".to_string(), "value1");
401 assert_eq!(registry.len(), 1);
402
403 assert_eq!(registry.get(&"store1".to_string()), Some(&"value1"));
404 assert_eq!(registry.get(&"store2".to_string()), None);
405
406 let removed = registry.remove(&"store1".to_string());
407 assert_eq!(removed, Some("value1"));
408 assert!(registry.is_empty());
409 }
410}