Skip to main content

oxihuman_core/
event_bus.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Simple synchronous event bus for plugin notifications.
5
6// ── Types ─────────────────────────────────────────────────────────────────────
7
8/// Kind of event dispatched on the bus.
9#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq)]
11pub enum EventKind {
12    TargetLoaded,
13    TargetUnloaded,
14    ParamChanged,
15    ExportStarted,
16    ExportFinished,
17    PluginRegistered,
18    Error,
19    Custom(String),
20}
21
22/// A single event with a JSON payload and wall-clock timestamp.
23#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct Event {
26    pub kind: EventKind,
27    /// JSON-encoded payload (may be `"null"` if empty).
28    pub payload: String,
29    /// Milliseconds since some epoch (caller-provided).
30    pub timestamp_ms: u64,
31}
32
33/// Type alias for a boxed event handler.
34pub type EventHandler = Box<dyn Fn(&Event) + Send + Sync>;
35
36/// Synchronous event bus: stores handlers keyed by `EventKind` and a history of all
37/// published events.
38pub struct EventBus {
39    handlers: Vec<(EventKind, EventHandler)>,
40    history: Vec<Event>,
41}
42
43// ── impl EventBus ─────────────────────────────────────────────────────────────
44
45impl EventBus {
46    /// Create a new, empty event bus.
47    #[allow(dead_code)]
48    pub fn new() -> Self {
49        Self {
50            handlers: Vec::new(),
51            history: Vec::new(),
52        }
53    }
54
55    /// Register a handler for a specific event kind.
56    #[allow(dead_code)]
57    pub fn subscribe(&mut self, kind: EventKind, handler: EventHandler) {
58        self.handlers.push((kind, handler));
59    }
60
61    /// Publish an event: invoke matching handlers then append to history.
62    #[allow(dead_code)]
63    pub fn publish(&mut self, event: Event) {
64        for (kind, handler) in &self.handlers {
65            if *kind == event.kind {
66                handler(&event);
67            }
68        }
69        self.history.push(event);
70    }
71
72    /// Access the full event history (oldest first).
73    #[allow(dead_code)]
74    pub fn history(&self) -> &[Event] {
75        &self.history
76    }
77
78    /// Clear the event history.
79    #[allow(dead_code)]
80    pub fn clear_history(&mut self) {
81        self.history.clear();
82    }
83
84    /// Total number of registered handlers.
85    #[allow(dead_code)]
86    pub fn handler_count(&self) -> usize {
87        self.handlers.len()
88    }
89
90    /// Total number of events published so far.
91    #[allow(dead_code)]
92    pub fn event_count(&self) -> usize {
93        self.history.len()
94    }
95}
96
97impl Default for EventBus {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103// ── Factory helpers ───────────────────────────────────────────────────────────
104
105/// Build a `ParamChanged` event with a JSON payload.
106#[allow(dead_code)]
107pub fn make_param_changed_event(name: &str, value: f32) -> Event {
108    Event {
109        kind: EventKind::ParamChanged,
110        payload: format!(r#"{{"name":"{name}","value":{value}}}"#),
111        timestamp_ms: 0,
112    }
113}
114
115/// Build an `ExportStarted` event.
116#[allow(dead_code)]
117pub fn make_export_event(path: &str, format: &str) -> Event {
118    Event {
119        kind: EventKind::ExportStarted,
120        payload: format!(r#"{{"path":"{path}","format":"{format}"}}"#),
121        timestamp_ms: 0,
122    }
123}
124
125/// Build an `Error` event.
126#[allow(dead_code)]
127pub fn make_error_event(msg: &str) -> Event {
128    Event {
129        kind: EventKind::Error,
130        payload: format!(r#"{{"message":"{msg}"}}"#),
131        timestamp_ms: 0,
132    }
133}
134
135// ── Tests ─────────────────────────────────────────────────────────────────────
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use std::sync::{Arc, Mutex};
141
142    fn ts() -> u64 {
143        0
144    }
145
146    #[test]
147    fn test_subscribe_and_publish_triggers_handler() {
148        let counter = Arc::new(Mutex::new(0u32));
149        let c = Arc::clone(&counter);
150        let mut bus = EventBus::new();
151        bus.subscribe(
152            EventKind::TargetLoaded,
153            Box::new(move |_| {
154                *c.lock().expect("should succeed") += 1;
155            }),
156        );
157        bus.publish(Event {
158            kind: EventKind::TargetLoaded,
159            payload: "null".to_string(),
160            timestamp_ms: ts(),
161        });
162        assert_eq!(*counter.lock().expect("should succeed"), 1);
163    }
164
165    #[test]
166    fn test_wrong_kind_does_not_trigger() {
167        let counter = Arc::new(Mutex::new(0u32));
168        let c = Arc::clone(&counter);
169        let mut bus = EventBus::new();
170        bus.subscribe(
171            EventKind::ExportFinished,
172            Box::new(move |_| {
173                *c.lock().expect("should succeed") += 1;
174            }),
175        );
176        bus.publish(Event {
177            kind: EventKind::TargetLoaded,
178            payload: "null".to_string(),
179            timestamp_ms: ts(),
180        });
181        assert_eq!(*counter.lock().expect("should succeed"), 0);
182    }
183
184    #[test]
185    fn test_history_grows() {
186        let mut bus = EventBus::new();
187        for i in 0..5 {
188            bus.publish(Event {
189                kind: EventKind::ParamChanged,
190                payload: format!("{i}"),
191                timestamp_ms: ts(),
192            });
193        }
194        assert_eq!(bus.history().len(), 5);
195    }
196
197    #[test]
198    fn test_clear_history_empties() {
199        let mut bus = EventBus::new();
200        bus.publish(Event {
201            kind: EventKind::Error,
202            payload: "null".to_string(),
203            timestamp_ms: ts(),
204        });
205        assert!(!bus.history().is_empty());
206        bus.clear_history();
207        assert!(bus.history().is_empty());
208    }
209
210    #[test]
211    fn test_handler_count() {
212        let mut bus = EventBus::new();
213        bus.subscribe(EventKind::TargetLoaded, Box::new(|_| {}));
214        bus.subscribe(EventKind::TargetUnloaded, Box::new(|_| {}));
215        assert_eq!(bus.handler_count(), 2);
216    }
217
218    #[test]
219    fn test_event_count_matches_published() {
220        let mut bus = EventBus::new();
221        bus.publish(Event {
222            kind: EventKind::ExportStarted,
223            payload: "null".to_string(),
224            timestamp_ms: ts(),
225        });
226        bus.publish(Event {
227            kind: EventKind::ExportFinished,
228            payload: "null".to_string(),
229            timestamp_ms: ts(),
230        });
231        assert_eq!(bus.event_count(), 2);
232    }
233
234    #[test]
235    fn test_multiple_handlers_same_kind() {
236        let counter = Arc::new(Mutex::new(0u32));
237        let c1 = Arc::clone(&counter);
238        let c2 = Arc::clone(&counter);
239        let mut bus = EventBus::new();
240        bus.subscribe(
241            EventKind::Error,
242            Box::new(move |_| *c1.lock().expect("should succeed") += 1),
243        );
244        bus.subscribe(
245            EventKind::Error,
246            Box::new(move |_| *c2.lock().expect("should succeed") += 1),
247        );
248        bus.publish(Event {
249            kind: EventKind::Error,
250            payload: "null".to_string(),
251            timestamp_ms: ts(),
252        });
253        assert_eq!(*counter.lock().expect("should succeed"), 2);
254    }
255
256    #[test]
257    fn test_custom_variant_matching() {
258        let counter = Arc::new(Mutex::new(0u32));
259        let c = Arc::clone(&counter);
260        let mut bus = EventBus::new();
261        bus.subscribe(
262            EventKind::Custom("my_event".to_string()),
263            Box::new(move |_| *c.lock().expect("should succeed") += 1),
264        );
265        bus.publish(Event {
266            kind: EventKind::Custom("my_event".to_string()),
267            payload: "null".to_string(),
268            timestamp_ms: ts(),
269        });
270        bus.publish(Event {
271            kind: EventKind::Custom("other".to_string()),
272            payload: "null".to_string(),
273            timestamp_ms: ts(),
274        });
275        assert_eq!(*counter.lock().expect("should succeed"), 1);
276    }
277
278    #[test]
279    fn test_make_param_changed_event_kind() {
280        let ev = make_param_changed_event("height", 1.75);
281        assert_eq!(ev.kind, EventKind::ParamChanged);
282        assert!(ev.payload.contains("height"));
283    }
284
285    #[test]
286    fn test_make_export_event_kind() {
287        let ev = make_export_event("/tmp/out.glb", "glb");
288        assert_eq!(ev.kind, EventKind::ExportStarted);
289        assert!(ev.payload.contains("glb"));
290    }
291
292    #[test]
293    fn test_make_error_event_kind() {
294        let ev = make_error_event("something went wrong");
295        assert_eq!(ev.kind, EventKind::Error);
296        assert!(ev.payload.contains("went wrong"));
297    }
298
299    #[test]
300    fn test_new_bus_empty() {
301        let bus = EventBus::new();
302        assert_eq!(bus.handler_count(), 0);
303        assert_eq!(bus.event_count(), 0);
304    }
305
306    #[test]
307    fn test_plugin_registered_event() {
308        let mut bus = EventBus::new();
309        let hit = Arc::new(Mutex::new(false));
310        let h = Arc::clone(&hit);
311        bus.subscribe(
312            EventKind::PluginRegistered,
313            Box::new(move |_| *h.lock().expect("should succeed") = true),
314        );
315        bus.publish(Event {
316            kind: EventKind::PluginRegistered,
317            payload: r#"{"name":"my-plugin"}"#.to_string(),
318            timestamp_ms: ts(),
319        });
320        assert!(*hit.lock().expect("should succeed"));
321    }
322
323    #[test]
324    fn test_history_payload_preserved() {
325        let mut bus = EventBus::new();
326        bus.publish(Event {
327            kind: EventKind::ParamChanged,
328            payload: r#"{"x":42}"#.to_string(),
329            timestamp_ms: 100,
330        });
331        assert_eq!(bus.history()[0].payload, r#"{"x":42}"#);
332        assert_eq!(bus.history()[0].timestamp_ms, 100);
333    }
334}