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}