orbit/state/
reactive.rs

1//! New scope-based reactive system for Orbit UI
2//!
3//! This module provides a fine-grained reactive system based on reactive scopes
4//! rather than global registries, eliminating circular dependency issues.
5
6use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
7
8/// Errors that can occur in the reactive system
9#[derive(Debug, Clone)]
10pub enum SignalError {
11    /// Signal has been dropped or is no longer accessible
12    SignalDropped,
13    /// Circular dependency detected
14    CircularDependency,
15    /// Invalid state transition
16    InvalidState,
17}
18
19impl std::fmt::Display for SignalError {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            SignalError::SignalDropped => write!(f, "Signal has been dropped"),
23            SignalError::CircularDependency => write!(f, "Circular dependency detected"),
24            SignalError::InvalidState => write!(f, "Invalid state transition"),
25        }
26    }
27}
28
29impl std::error::Error for SignalError {}
30
31/// Reactive scope that manages signals, effects, and computed values
32///
33/// This is a simplified implementation focused on basic functionality.
34/// Advanced dependency tracking will be implemented in future versions.
35#[derive(Debug)]
36pub struct ReactiveScope {
37    // Reserved for future dependency tracking functionality
38}
39
40impl ReactiveScope {
41    /// Create a new reactive scope
42    pub fn new() -> Self {
43        Self {
44            // Future: Add dependency tracking infrastructure here
45        }
46    }
47}
48
49impl Default for ReactiveScope {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55/// A reactive signal that holds a value
56pub struct Signal<T> {
57    pub value: Arc<RwLock<T>>,
58    dirty: Arc<RwLock<bool>>,
59}
60
61// Explicit Send + Sync implementations
62unsafe impl<T: Send + Sync> Send for Signal<T> {}
63unsafe impl<T: Send + Sync> Sync for Signal<T> {}
64
65impl<T> Signal<T>
66where
67    T: Send + Sync + 'static,
68{
69    /// Get the current value of the signal
70    pub fn get(&self) -> RwLockReadGuard<T> {
71        // TODO: Track this read for reactive dependencies
72        self.value.read().unwrap()
73    }
74
75    /// Get a mutable reference to the signal's value
76    pub fn get_mut(&self) -> RwLockWriteGuard<T> {
77        self.value.write().unwrap()
78    }
79
80    /// Set the signal's value and trigger updates
81    pub fn set(&self, value: T) -> Result<(), SignalError> {
82        {
83            let mut val = self.value.write().unwrap();
84            *val = value;
85        }
86
87        // Mark as dirty and trigger updates
88        *self.dirty.write().unwrap() = true;
89
90        // TODO: In a full implementation, this would trigger dependent updates
91        Ok(())
92    }
93
94    /// Update the signal's value with a function
95    pub fn update<F>(&self, f: F) -> Result<(), SignalError>
96    where
97        F: FnOnce(&mut T),
98    {
99        {
100            let mut value = self.value.write().unwrap();
101            f(&mut *value);
102        }
103        self.set_dirty()
104    }
105
106    fn set_dirty(&self) -> Result<(), SignalError> {
107        *self.dirty.write().unwrap() = true;
108        Ok(())
109    }
110}
111
112/// A reactive effect that runs when its dependencies change
113pub struct Effect<F> {
114    callback: Mutex<Option<F>>,
115    dirty: Arc<RwLock<bool>>,
116}
117
118// Explicit Send + Sync implementations
119unsafe impl<F: Send + Sync> Send for Effect<F> {}
120unsafe impl<F: Send + Sync> Sync for Effect<F> {}
121
122impl Effect<Box<dyn FnMut() + Send + Sync + 'static>> {
123    /// Execute the effect
124    pub fn run(&self) -> Result<(), SignalError> {
125        // Check if we should run
126        let should_run = {
127            let callback_ref = self.callback.lock().unwrap();
128            callback_ref.is_some()
129        };
130
131        if should_run {
132            let mut callback = self.callback.lock().unwrap().take().unwrap();
133            callback();
134            *self.callback.lock().unwrap() = Some(callback);
135            *self.dirty.write().unwrap() = false;
136        }
137        Ok(())
138    }
139}
140
141/// A computed value that derives from other reactive values
142pub struct ReactiveComputed<T, F> {
143    value: Arc<RwLock<Option<T>>>,
144    compute_fn: Mutex<Option<F>>,
145    dirty: Arc<RwLock<bool>>,
146}
147
148// Explicit Send + Sync implementations
149unsafe impl<T: Send + Sync, F: Send + Sync> Send for ReactiveComputed<T, F> {}
150unsafe impl<T: Send + Sync, F: Send + Sync> Sync for ReactiveComputed<T, F> {}
151
152impl<T> ReactiveComputed<T, Box<dyn FnMut() -> T + Send + Sync + 'static>>
153where
154    T: Send + Sync + Clone + 'static,
155{
156    /// Get the computed value, recalculating if necessary
157    pub fn get(&self) -> Result<T, SignalError> {
158        if *self.dirty.read().unwrap() || self.value.read().unwrap().is_none() {
159            self.recompute()?;
160        }
161
162        self.value
163            .read()
164            .unwrap()
165            .clone()
166            .ok_or(SignalError::InvalidState)
167    }
168
169    fn recompute(&self) -> Result<(), SignalError> {
170        // Check if we should compute
171        let should_compute = {
172            let compute_ref = self.compute_fn.lock().unwrap();
173            compute_ref.is_some()
174        };
175
176        if should_compute {
177            let mut compute_fn = self.compute_fn.lock().unwrap().take().unwrap();
178            let new_value = compute_fn();
179            *self.value.write().unwrap() = Some(new_value);
180            *self.compute_fn.lock().unwrap() = Some(compute_fn);
181            *self.dirty.write().unwrap() = false;
182        }
183        Ok(())
184    }
185}
186
187/// Create a new signal with an initial value
188pub fn create_signal<T>(_scope: &ReactiveScope, initial_value: T) -> Signal<T>
189where
190    T: Send + Sync + 'static,
191{
192    Signal {
193        value: Arc::new(RwLock::new(initial_value)),
194        dirty: Arc::new(RwLock::new(false)),
195    }
196}
197
198/// Create a new effect that runs when dependencies change
199pub fn create_effect<F>(
200    _scope: &ReactiveScope,
201    callback: F,
202) -> Effect<Box<dyn FnMut() + Send + Sync + 'static>>
203where
204    F: FnMut() + Send + Sync + 'static,
205{
206    let effect = Effect {
207        callback: Mutex::new(Some(
208            Box::new(callback) as Box<dyn FnMut() + Send + Sync + 'static>
209        )),
210        dirty: Arc::new(RwLock::new(true)), // Start dirty to run on creation
211    };
212
213    // Run initially
214    let _ = effect.run();
215    effect
216}
217
218/// Create a new computed value
219pub fn create_computed<T, F>(
220    _scope: &ReactiveScope,
221    compute_fn: F,
222) -> ReactiveComputed<T, Box<dyn FnMut() -> T + Send + Sync + 'static>>
223where
224    F: FnMut() -> T + Send + Sync + 'static,
225    T: Send + Sync + Clone + 'static,
226{
227    ReactiveComputed {
228        value: Arc::new(RwLock::new(None)),
229        compute_fn: Mutex::new(Some(
230            Box::new(compute_fn) as Box<dyn FnMut() -> T + Send + Sync + 'static>
231        )),
232        dirty: Arc::new(RwLock::new(true)), // Start dirty to compute on first access
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_signal_creation_and_access() {
242        let scope = ReactiveScope::new();
243        let signal = create_signal(&scope, 42);
244
245        assert_eq!(*signal.get(), 42);
246    }
247
248    #[test]
249    fn test_signal_update() {
250        let scope = ReactiveScope::new();
251        let signal = create_signal(&scope, 10);
252
253        signal.update(|v| *v += 5).unwrap();
254        assert_eq!(*signal.get(), 15);
255    }
256
257    #[test]
258    fn test_effect_creation() {
259        let scope = ReactiveScope::new();
260        let counter = Arc::new(RwLock::new(0));
261        let counter_clone = counter.clone();
262
263        let _effect = create_effect(&scope, move || {
264            *counter_clone.write().unwrap() += 1;
265        });
266
267        // Effect should run once on creation
268        assert_eq!(*counter.read().unwrap(), 1);
269    }
270
271    #[test]
272    fn test_computed_value() {
273        let scope = ReactiveScope::new();
274        let signal = create_signal(&scope, 5);
275        let signal_clone = signal.value.clone();
276
277        let computed = create_computed(&scope, move || *signal_clone.read().unwrap() * 2);
278
279        assert_eq!(computed.get().unwrap(), 10);
280    }
281}