use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum EventType {
QueryStarted,
QueryCompleted,
QueryError,
DataLoaded,
DataUpdated,
ConnectionEstablished,
ConnectionLost,
Custom(String),
}
impl EventType {
pub fn label(&self) -> String {
match self {
EventType::QueryStarted => "QueryStarted".to_string(),
EventType::QueryCompleted => "QueryCompleted".to_string(),
EventType::QueryError => "QueryError".to_string(),
EventType::DataLoaded => "DataLoaded".to_string(),
EventType::DataUpdated => "DataUpdated".to_string(),
EventType::ConnectionEstablished => "ConnectionEstablished".to_string(),
EventType::ConnectionLost => "ConnectionLost".to_string(),
EventType::Custom(name) => format!("Custom({name})"),
}
}
}
#[derive(Debug, Clone)]
pub struct WasmEvent {
pub event_type: EventType,
pub payload: HashMap<String, String>,
pub timestamp_ms: u64,
pub source: String,
}
pub type SubscriptionId = u64;
#[derive(Debug, Clone, Default)]
pub struct HandlerStats {
pub calls: u64,
pub last_called_ms: Option<u64>,
}
#[derive(Debug, Clone)]
struct Subscription {
event_type: EventType,
handler_name: String,
stats: HandlerStats,
}
pub struct EventDispatcher {
subscriptions: HashMap<SubscriptionId, Subscription>,
next_id: SubscriptionId,
}
impl EventDispatcher {
pub fn new() -> Self {
Self {
subscriptions: HashMap::new(),
next_id: 1,
}
}
pub fn subscribe(&mut self, event_type: EventType, handler_name: &str) -> SubscriptionId {
let id = self.next_id;
self.next_id += 1;
self.subscriptions.insert(
id,
Subscription {
event_type,
handler_name: handler_name.to_string(),
stats: HandlerStats::default(),
},
);
id
}
pub fn unsubscribe(&mut self, id: SubscriptionId) -> bool {
self.subscriptions.remove(&id).is_some()
}
pub fn dispatch(&mut self, event: WasmEvent) -> usize {
let mut count = 0usize;
for sub in self.subscriptions.values_mut() {
if sub.event_type == event.event_type {
sub.stats.calls += 1;
sub.stats.last_called_ms = Some(event.timestamp_ms);
count += 1;
}
}
count
}
pub fn dispatch_to_all(&mut self, event: WasmEvent) -> usize {
let count = self.subscriptions.len();
for sub in self.subscriptions.values_mut() {
sub.stats.calls += 1;
sub.stats.last_called_ms = Some(event.timestamp_ms);
}
count
}
pub fn subscription_count(&self) -> usize {
self.subscriptions.len()
}
pub fn handler_stats(&self, id: SubscriptionId) -> Option<&HandlerStats> {
self.subscriptions.get(&id).map(|s| &s.stats)
}
pub fn subscriptions_for_type(&self, event_type: &EventType) -> usize {
self.subscriptions
.values()
.filter(|s| &s.event_type == event_type)
.count()
}
pub fn handler_name(&self, id: SubscriptionId) -> Option<&str> {
self.subscriptions.get(&id).map(|s| s.handler_name.as_str())
}
pub fn clear_all(&mut self) {
self.subscriptions.clear();
}
}
impl Default for EventDispatcher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_event(event_type: EventType, ts: u64) -> WasmEvent {
WasmEvent {
event_type,
payload: HashMap::new(),
timestamp_ms: ts,
source: "test".to_string(),
}
}
fn make_event_with_payload(event_type: EventType, ts: u64, key: &str, val: &str) -> WasmEvent {
let mut payload = HashMap::new();
payload.insert(key.to_string(), val.to_string());
WasmEvent {
event_type,
payload,
timestamp_ms: ts,
source: "test".to_string(),
}
}
#[test]
fn test_subscribe_returns_unique_ids() {
let mut d = EventDispatcher::new();
let id1 = d.subscribe(EventType::QueryStarted, "h1");
let id2 = d.subscribe(EventType::QueryStarted, "h2");
assert_ne!(id1, id2);
}
#[test]
fn test_subscription_count_increments() {
let mut d = EventDispatcher::new();
assert_eq!(d.subscription_count(), 0);
d.subscribe(EventType::DataLoaded, "h1");
assert_eq!(d.subscription_count(), 1);
d.subscribe(EventType::DataUpdated, "h2");
assert_eq!(d.subscription_count(), 2);
}
#[test]
fn test_subscribe_stores_handler_name() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryCompleted, "my_handler");
assert_eq!(d.handler_name(id), Some("my_handler"));
}
#[test]
fn test_unsubscribe_known_id_returns_true() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryError, "h");
assert!(d.unsubscribe(id));
}
#[test]
fn test_unsubscribe_unknown_id_returns_false() {
let mut d = EventDispatcher::new();
assert!(!d.unsubscribe(9999));
}
#[test]
fn test_unsubscribe_decrements_count() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::ConnectionEstablished, "h");
assert_eq!(d.subscription_count(), 1);
d.unsubscribe(id);
assert_eq!(d.subscription_count(), 0);
}
#[test]
fn test_unsubscribe_removes_handler_from_type_count() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryCompleted, "h");
assert_eq!(d.subscriptions_for_type(&EventType::QueryCompleted), 1);
d.unsubscribe(id);
assert_eq!(d.subscriptions_for_type(&EventType::QueryCompleted), 0);
}
#[test]
fn test_unsubscribe_then_no_dispatch() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::DataLoaded, "h");
d.unsubscribe(id);
let called = d.dispatch(make_event(EventType::DataLoaded, 100));
assert_eq!(called, 0);
}
#[test]
fn test_dispatch_calls_matching_handler() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryCompleted, "on_done");
let called = d.dispatch(make_event(EventType::QueryCompleted, 500));
assert_eq!(called, 1);
let stats = d.handler_stats(id).expect("stats");
assert_eq!(stats.calls, 1);
}
#[test]
fn test_dispatch_does_not_call_mismatched_handler() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryStarted, "on_start");
let called = d.dispatch(make_event(EventType::QueryCompleted, 100));
assert_eq!(called, 0);
let stats = d.handler_stats(id).expect("stats");
assert_eq!(stats.calls, 0);
}
#[test]
fn test_dispatch_returns_zero_with_no_subscribers() {
let mut d = EventDispatcher::new();
let called = d.dispatch(make_event(EventType::DataLoaded, 100));
assert_eq!(called, 0);
}
#[test]
fn test_dispatch_multiple_handlers_same_type() {
let mut d = EventDispatcher::new();
let id1 = d.subscribe(EventType::DataUpdated, "h1");
let id2 = d.subscribe(EventType::DataUpdated, "h2");
let id3 = d.subscribe(EventType::DataUpdated, "h3");
let called = d.dispatch(make_event(EventType::DataUpdated, 1000));
assert_eq!(called, 3);
for id in [id1, id2, id3] {
assert_eq!(d.handler_stats(id).expect("stats").calls, 1);
}
}
#[test]
fn test_dispatch_updates_last_called_ms() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryStarted, "h");
d.dispatch(make_event(EventType::QueryStarted, 7777));
let stats = d.handler_stats(id).expect("stats");
assert_eq!(stats.last_called_ms, Some(7777));
}
#[test]
fn test_dispatch_accumulates_call_count() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryCompleted, "h");
for ts in [100, 200, 300] {
d.dispatch(make_event(EventType::QueryCompleted, ts));
}
assert_eq!(d.handler_stats(id).expect("stats").calls, 3);
}
#[test]
fn test_dispatch_last_called_updated_on_each_call() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryCompleted, "h");
d.dispatch(make_event(EventType::QueryCompleted, 100));
d.dispatch(make_event(EventType::QueryCompleted, 200));
assert_eq!(
d.handler_stats(id).expect("stats").last_called_ms,
Some(200)
);
}
#[test]
fn test_dispatch_mixed_types_only_calls_matching() {
let mut d = EventDispatcher::new();
let id_a = d.subscribe(EventType::DataLoaded, "ha");
let id_b = d.subscribe(EventType::DataUpdated, "hb");
d.dispatch(make_event(EventType::DataLoaded, 50));
assert_eq!(d.handler_stats(id_a).expect("a").calls, 1);
assert_eq!(d.handler_stats(id_b).expect("b").calls, 0);
}
#[test]
fn test_dispatch_to_all_calls_every_handler() {
let mut d = EventDispatcher::new();
let id1 = d.subscribe(EventType::QueryStarted, "h1");
let id2 = d.subscribe(EventType::DataLoaded, "h2");
let id3 = d.subscribe(EventType::ConnectionLost, "h3");
let called = d.dispatch_to_all(make_event(EventType::QueryCompleted, 999));
assert_eq!(called, 3);
for id in [id1, id2, id3] {
assert_eq!(d.handler_stats(id).expect("stats").calls, 1);
}
}
#[test]
fn test_dispatch_to_all_updates_timestamp_for_all() {
let mut d = EventDispatcher::new();
let id1 = d.subscribe(EventType::QueryStarted, "h1");
let id2 = d.subscribe(EventType::DataUpdated, "h2");
d.dispatch_to_all(make_event(EventType::QueryError, 12345));
assert_eq!(
d.handler_stats(id1).expect("s1").last_called_ms,
Some(12345)
);
assert_eq!(
d.handler_stats(id2).expect("s2").last_called_ms,
Some(12345)
);
}
#[test]
fn test_dispatch_to_all_returns_zero_when_empty() {
let mut d = EventDispatcher::new();
let called = d.dispatch_to_all(make_event(EventType::DataLoaded, 1));
assert_eq!(called, 0);
}
#[test]
fn test_initial_stats_are_zero() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryError, "h");
let stats = d.handler_stats(id).expect("stats");
assert_eq!(stats.calls, 0);
assert!(stats.last_called_ms.is_none());
}
#[test]
fn test_handler_stats_returns_none_for_unknown_id() {
let d = EventDispatcher::new();
assert!(d.handler_stats(42).is_none());
}
#[test]
fn test_handler_stats_after_unsubscribe_returns_none() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::DataLoaded, "h");
d.unsubscribe(id);
assert!(d.handler_stats(id).is_none());
}
#[test]
fn test_subscriptions_for_type_zero_initially() {
let d = EventDispatcher::new();
assert_eq!(d.subscriptions_for_type(&EventType::QueryCompleted), 0);
}
#[test]
fn test_subscriptions_for_type_counts_correctly() {
let mut d = EventDispatcher::new();
d.subscribe(EventType::DataLoaded, "h1");
d.subscribe(EventType::DataLoaded, "h2");
d.subscribe(EventType::QueryCompleted, "h3");
assert_eq!(d.subscriptions_for_type(&EventType::DataLoaded), 2);
assert_eq!(d.subscriptions_for_type(&EventType::QueryCompleted), 1);
assert_eq!(d.subscriptions_for_type(&EventType::QueryStarted), 0);
}
#[test]
fn test_clear_all_removes_all_subscriptions() {
let mut d = EventDispatcher::new();
d.subscribe(EventType::DataLoaded, "h1");
d.subscribe(EventType::DataUpdated, "h2");
d.subscribe(EventType::QueryError, "h3");
assert_eq!(d.subscription_count(), 3);
d.clear_all();
assert_eq!(d.subscription_count(), 0);
}
#[test]
fn test_clear_all_then_dispatch_returns_zero() {
let mut d = EventDispatcher::new();
d.subscribe(EventType::QueryCompleted, "h");
d.clear_all();
let called = d.dispatch(make_event(EventType::QueryCompleted, 1));
assert_eq!(called, 0);
}
#[test]
fn test_clear_all_then_resubscribe_works() {
let mut d = EventDispatcher::new();
d.subscribe(EventType::QueryCompleted, "h");
d.clear_all();
let id = d.subscribe(EventType::QueryCompleted, "h2");
let called = d.dispatch(make_event(EventType::QueryCompleted, 1));
assert_eq!(called, 1);
assert_eq!(d.handler_stats(id).expect("s").calls, 1);
}
#[test]
fn test_custom_event_subscribe_and_dispatch() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::Custom("my_event".to_string()), "custom_handler");
let called = d.dispatch(make_event(EventType::Custom("my_event".to_string()), 1));
assert_eq!(called, 1);
assert_eq!(d.handler_stats(id).expect("s").calls, 1);
}
#[test]
fn test_custom_event_different_names_do_not_match() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::Custom("event_a".to_string()), "ha");
let called = d.dispatch(make_event(EventType::Custom("event_b".to_string()), 1));
assert_eq!(called, 0);
assert_eq!(d.handler_stats(id).expect("s").calls, 0);
}
#[test]
fn test_custom_event_label() {
let t = EventType::Custom("special_event".to_string());
assert_eq!(t.label(), "Custom(special_event)");
}
#[test]
fn test_standard_event_labels() {
assert_eq!(EventType::QueryStarted.label(), "QueryStarted");
assert_eq!(EventType::QueryCompleted.label(), "QueryCompleted");
assert_eq!(EventType::QueryError.label(), "QueryError");
assert_eq!(EventType::DataLoaded.label(), "DataLoaded");
assert_eq!(EventType::DataUpdated.label(), "DataUpdated");
assert_eq!(
EventType::ConnectionEstablished.label(),
"ConnectionEstablished"
);
assert_eq!(EventType::ConnectionLost.label(), "ConnectionLost");
}
#[test]
fn test_dispatch_with_payload() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::DataLoaded, "h");
let event = make_event_with_payload(EventType::DataLoaded, 42, "graph", "default");
let called = d.dispatch(event);
assert_eq!(called, 1);
assert_eq!(d.handler_stats(id).expect("s").calls, 1);
}
#[test]
fn test_event_source_field() {
let event = WasmEvent {
event_type: EventType::QueryCompleted,
payload: HashMap::new(),
timestamp_ms: 100,
source: "sparql_engine".to_string(),
};
assert_eq!(event.source, "sparql_engine");
}
#[test]
fn test_dispatcher_default_is_empty() {
let d = EventDispatcher::default();
assert_eq!(d.subscription_count(), 0);
}
#[test]
fn test_handler_stats_default() {
let s = HandlerStats::default();
assert_eq!(s.calls, 0);
assert!(s.last_called_ms.is_none());
}
#[test]
fn test_multiple_dispatch_cycles_accumulate() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::ConnectionEstablished, "conn_handler");
for i in 1u64..=10 {
d.dispatch(make_event(EventType::ConnectionEstablished, i * 100));
}
let stats = d.handler_stats(id).expect("s");
assert_eq!(stats.calls, 10);
assert_eq!(stats.last_called_ms, Some(1000));
}
#[test]
fn test_connection_lost_event() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::ConnectionLost, "on_disconnect");
let called = d.dispatch(make_event(EventType::ConnectionLost, 9999));
assert_eq!(called, 1);
assert_eq!(d.handler_stats(id).expect("s").calls, 1);
}
#[test]
fn test_dispatch_to_all_accumulates_with_normal_dispatch() {
let mut d = EventDispatcher::new();
let id = d.subscribe(EventType::QueryStarted, "h");
d.dispatch(make_event(EventType::QueryStarted, 1));
d.dispatch_to_all(make_event(EventType::DataLoaded, 2));
let stats = d.handler_stats(id).expect("s");
assert_eq!(stats.calls, 2);
}
}