glitcher_api/
hooks.rs

1//! Action System Hook Interface
2//!
3//! Provides extensible hook points for action execution events.
4//!
5//! ## Use Cases
6//!
7//! - **Recording/Replay**: Capture action events for session recording
8//! - **MIDI Learning**: Map hardware controls to actions
9//! - **Analytics**: Track action usage patterns
10//! - **Debug Logging**: Trace action execution for debugging
11//! - **Network Sync**: Synchronize actions across multiple instances (OSC/Artnet)
12//! - **UI State**: Update undo/redo buffers, highlight active controls
13//!
14//! ## Usage
15//!
16//! ```rust,ignore
17//! use glitcher_api::hooks::{ActionHook, BeforeActionEvent, AfterActionEvent};
18//!
19//! struct MyRecordingHook {
20//!     events: Vec<RecordedEvent>,
21//! }
22//!
23//! impl ActionHook for MyRecordingHook {
24//!     fn before_action(&mut self, event: &BeforeActionEvent) {
25//!         println!("About to execute: {}", event.action_name);
26//!     }
27//!
28//!     fn after_action(&mut self, event: &AfterActionEvent) {
29//!         if event.result.is_ok() {
30//!             self.events.push(RecordedEvent {
31//!                 timestamp_ms: event.timestamp_ms,
32//!                 node_id: event.node_id,
33//!                 action_name: event.action_name.clone(),
34//!                 random_seed: event.random_seed,
35//!             });
36//!         }
37//!     }
38//! }
39//! ```
40
41use slotmap::KeyData;
42
43// ============================================================================
44// Recording Types (for RecordingHook implementation)
45// ============================================================================
46
47/// Recorded event for session recording/replay
48///
49/// This type is designed for action-based recording, where actions are
50/// re-executed on replay with the same random seed for deterministic results.
51///
52/// ## Usage
53///
54/// ```rust,ignore
55/// use glitcher_api::hooks::{RecordedEvent, ActionHook, AfterActionEvent};
56///
57/// struct RecordingHook {
58///     events: Vec<RecordedEvent>,
59/// }
60///
61/// impl ActionHook for RecordingHook {
62///     fn after_action(&mut self, event: &AfterActionEvent) {
63///         if event.result.is_ok() {
64///             self.events.push(RecordedEvent::from_after_event(event));
65///         }
66///     }
67/// }
68/// ```
69#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
70pub struct RecordedEvent {
71    /// Timestamp in milliseconds since session start
72    pub timestamp_ms: u64,
73
74    /// Target node ID (stable KeyData for serialization)
75    pub node_id: KeyData,
76
77    /// Action name (e.g., "reset", "randomize")
78    pub action_name: String,
79
80    /// Random seed used (for deterministic replay)
81    pub random_seed: u64,
82}
83
84impl RecordedEvent {
85    /// Create a RecordedEvent from an AfterActionEvent
86    pub fn from_after_event(event: &AfterActionEvent) -> Self {
87        Self {
88            timestamp_ms: event.timestamp_ms,
89            node_id: event.node_id,
90            action_name: event.action_name.clone(),
91            random_seed: event.random_seed,
92        }
93    }
94}
95
96// ============================================================================
97// Hook Event Types
98// ============================================================================
99
100/// Event fired before action execution
101#[derive(Debug, Clone)]
102pub struct BeforeActionEvent {
103    /// Timestamp in milliseconds since session start
104    pub timestamp_ms: u64,
105
106    /// Target node ID (stable KeyData for serialization)
107    pub node_id: KeyData,
108
109    /// Action name (e.g., "reset", "randomize")
110    pub action_name: String,
111
112    /// Random seed for deterministic execution
113    pub random_seed: u64,
114
115    /// High-frequency execution hint
116    pub is_high_frequency: bool,
117}
118
119/// Event fired after action execution
120#[derive(Debug, Clone)]
121pub struct AfterActionEvent {
122    /// Timestamp in milliseconds since session start
123    pub timestamp_ms: u64,
124
125    /// Target node ID
126    pub node_id: KeyData,
127
128    /// Action name
129    pub action_name: String,
130
131    /// Random seed used
132    pub random_seed: u64,
133
134    /// Number of parameters updated
135    pub update_count: usize,
136
137    /// Execution result (Ok = success, Err = error message)
138    pub result: Result<(), String>,
139}
140
141/// Hook interface for action execution events
142///
143/// Implement this trait to receive notifications about action execution.
144/// Hooks are called in registration order.
145///
146/// ## Thread Safety
147///
148/// Hooks must be `Send` to support multi-threaded execution contexts.
149/// If your hook needs to share state, use `Arc<Mutex<T>>` or channels.
150pub trait ActionHook: Send {
151    /// Called before action execution
152    ///
153    /// This hook is called before the action logic runs.
154    /// Use this to:
155    /// - Log action invocations
156    /// - Prepare state for recording
157    /// - Update UI state (mark button as active)
158    ///
159    /// Note: This hook cannot cancel action execution.
160    fn before_action(&mut self, event: &BeforeActionEvent);
161
162    /// Called after action execution
163    ///
164    /// This hook is called after the action completes (success or failure).
165    /// Use this to:
166    /// - Record action events
167    /// - Update analytics
168    /// - Send network sync messages
169    /// - Update undo/redo buffers
170    ///
171    /// The `result` field indicates whether execution succeeded.
172    fn after_action(&mut self, event: &AfterActionEvent);
173
174    /// Optional: Hook name for debugging
175    fn name(&self) -> &str {
176        "AnonymousHook"
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    struct TestHook {
185        before_count: usize,
186        after_count: usize,
187    }
188
189    impl ActionHook for TestHook {
190        fn before_action(&mut self, _event: &BeforeActionEvent) {
191            self.before_count += 1;
192        }
193
194        fn after_action(&mut self, _event: &AfterActionEvent) {
195            self.after_count += 1;
196        }
197
198        fn name(&self) -> &str {
199            "TestHook"
200        }
201    }
202
203    #[test]
204    fn test_hook_trait_object() {
205        let mut hook: Box<dyn ActionHook> = Box::new(TestHook {
206            before_count: 0,
207            after_count: 0,
208        });
209
210        let before_event = BeforeActionEvent {
211            timestamp_ms: 1000,
212            node_id: KeyData::from_ffi(0),
213            action_name: "test".to_string(),
214            random_seed: 42,
215            is_high_frequency: false,
216        };
217
218        let after_event = AfterActionEvent {
219            timestamp_ms: 1001,
220            node_id: KeyData::from_ffi(0),
221            action_name: "test".to_string(),
222            random_seed: 42,
223            update_count: 3,
224            result: Ok(()),
225        };
226
227        hook.before_action(&before_event);
228        hook.after_action(&after_event);
229
230        // Type erasure works correctly
231        assert_eq!(hook.name(), "TestHook");
232    }
233
234    #[test]
235    fn test_multiple_hooks() {
236        let mut hooks: Vec<Box<dyn ActionHook>> = vec![
237            Box::new(TestHook {
238                before_count: 0,
239                after_count: 0,
240            }),
241            Box::new(TestHook {
242                before_count: 0,
243                after_count: 0,
244            }),
245        ];
246
247        let before_event = BeforeActionEvent {
248            timestamp_ms: 1000,
249            node_id: KeyData::from_ffi(0),
250            action_name: "test".to_string(),
251            random_seed: 42,
252            is_high_frequency: false,
253        };
254
255        // Call all hooks
256        for hook in &mut hooks {
257            hook.before_action(&before_event);
258        }
259
260        assert_eq!(hooks.len(), 2);
261    }
262
263    #[test]
264    fn test_event_cloning() {
265        let before = BeforeActionEvent {
266            timestamp_ms: 1000,
267            node_id: KeyData::from_ffi(0),
268            action_name: "test".to_string(),
269            random_seed: 42,
270            is_high_frequency: false,
271        };
272
273        let cloned = before.clone();
274        assert_eq!(cloned.timestamp_ms, before.timestamp_ms);
275        assert_eq!(cloned.action_name, before.action_name);
276    }
277
278    #[test]
279    fn test_after_event_result() {
280        let success = AfterActionEvent {
281            timestamp_ms: 1000,
282            node_id: KeyData::from_ffi(0),
283            action_name: "test".to_string(),
284            random_seed: 42,
285            update_count: 5,
286            result: Ok(()),
287        };
288
289        assert!(success.result.is_ok());
290
291        let failure = AfterActionEvent {
292            timestamp_ms: 1000,
293            node_id: KeyData::from_ffi(0),
294            action_name: "test".to_string(),
295            random_seed: 42,
296            update_count: 0,
297            result: Err("test error".to_string()),
298        };
299
300        assert!(failure.result.is_err());
301    }
302}