use crate::inbox::{InboxError, InboxSender};
#[cfg(target_arch = "wasm32")]
use crate::tokio;
use crate::types::InboxItem;
use meerkat_core::PlainEventSource;
use meerkat_core::event_injector::{EventInjector, EventInjectorError};
pub type SubscriberRegistry = std::sync::Arc<
parking_lot::Mutex<
std::collections::HashMap<uuid::Uuid, tokio::sync::mpsc::Sender<meerkat_core::AgentEvent>>,
>,
>;
pub fn new_subscriber_registry() -> SubscriberRegistry {
std::sync::Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new()))
}
pub struct CommsEventInjector {
sender: InboxSender,
subscriber_registry: SubscriberRegistry,
}
impl CommsEventInjector {
pub fn new(sender: InboxSender, registry: SubscriberRegistry) -> Self {
Self {
sender,
subscriber_registry: registry,
}
}
}
impl EventInjector for CommsEventInjector {
fn inject(&self, body: String, source: PlainEventSource) -> Result<(), EventInjectorError> {
self.sender
.send(InboxItem::PlainEvent {
body,
source,
interaction_id: None,
})
.map_err(|e| match e {
InboxError::Full => EventInjectorError::Full,
InboxError::Closed => EventInjectorError::Closed,
})
}
}
impl meerkat_core::SubscribableInjector for CommsEventInjector {
fn inject_with_subscription(
&self,
body: String,
source: PlainEventSource,
) -> Result<meerkat_core::InteractionSubscription, EventInjectorError> {
let id = uuid::Uuid::new_v4();
let (tx, rx) = tokio::sync::mpsc::channel(4096);
self.subscriber_registry.lock().insert(id, tx);
if let Err(e) = self.sender.send(InboxItem::PlainEvent {
body,
source,
interaction_id: Some(id),
}) {
self.subscriber_registry.lock().remove(&id);
return Err(match e {
InboxError::Full => EventInjectorError::Full,
InboxError::Closed => EventInjectorError::Closed,
});
}
Ok(meerkat_core::InteractionSubscription {
id: meerkat_core::InteractionId(id),
events: rx,
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::inbox::Inbox;
#[test]
fn test_comms_event_injector_sends_to_inbox() {
let (mut inbox, sender) = Inbox::new();
let injector = CommsEventInjector::new(sender, new_subscriber_registry());
injector
.inject("hello".to_string(), PlainEventSource::Tcp)
.unwrap();
let items = inbox.try_drain();
assert_eq!(items.len(), 1);
match &items[0] {
InboxItem::PlainEvent { body, source, .. } => {
assert_eq!(body, "hello");
assert_eq!(*source, PlainEventSource::Tcp);
}
other => panic!("Expected PlainEvent, got {other:?}"),
}
}
#[test]
fn test_comms_event_injector_reports_full() {
let (_inbox, sender) = Inbox::new_with_capacity(1);
let injector = CommsEventInjector::new(sender, new_subscriber_registry());
injector
.inject("first".to_string(), PlainEventSource::Tcp)
.unwrap();
let result = injector.inject("second".to_string(), PlainEventSource::Tcp);
assert!(
matches!(result, Err(EventInjectorError::Full)),
"Expected Full error"
);
}
#[test]
fn test_comms_event_injector_as_dyn() {
use std::sync::Arc;
let (mut inbox, sender) = Inbox::new();
let injector: Arc<dyn EventInjector> =
Arc::new(CommsEventInjector::new(sender, new_subscriber_registry()));
injector
.inject("via dyn".to_string(), PlainEventSource::Webhook)
.unwrap();
let items = inbox.try_drain();
assert_eq!(items.len(), 1);
}
#[test]
fn test_inject_with_subscription_stores_subscriber_and_sends_item() {
use meerkat_core::SubscribableInjector;
let registry = new_subscriber_registry();
let (mut inbox, sender) = Inbox::new();
let injector = CommsEventInjector::new(sender, registry.clone());
let sub = injector
.inject_with_subscription("tracked".to_string(), PlainEventSource::Rpc)
.unwrap();
let items = inbox.try_drain();
assert_eq!(items.len(), 1);
match &items[0] {
InboxItem::PlainEvent {
body,
interaction_id,
..
} => {
assert_eq!(body, "tracked");
assert_eq!(*interaction_id, Some(sub.id.0));
}
other => panic!("Expected PlainEvent, got {other:?}"),
}
assert!(registry.lock().contains_key(&sub.id.0));
}
#[test]
fn test_inject_with_subscription_cleans_up_on_full() {
use meerkat_core::SubscribableInjector;
let registry = new_subscriber_registry();
let (_inbox, sender) = Inbox::new_with_capacity(1);
let injector = CommsEventInjector::new(sender, registry.clone());
injector
.inject("first".to_string(), PlainEventSource::Tcp)
.unwrap();
let result = injector.inject_with_subscription("second".to_string(), PlainEventSource::Tcp);
assert!(matches!(result, Err(EventInjectorError::Full)));
assert!(registry.lock().is_empty());
}
#[test]
fn test_inject_with_subscription_cleans_up_on_closed_inbox() {
use meerkat_core::SubscribableInjector;
let registry = new_subscriber_registry();
let (inbox, sender) = Inbox::new();
drop(inbox);
let injector = CommsEventInjector::new(sender, registry.clone());
let result = injector.inject_with_subscription("closed".to_string(), PlainEventSource::Tcp);
assert!(matches!(result, Err(EventInjectorError::Closed)));
assert!(registry.lock().is_empty(), "registry should be cleaned up");
}
}