use std::collections::{HashMap, HashSet};
use std::sync::{Arc, OnceLock, RwLock};
use super::sinks::flush_all_sinks;
use super::{AgentEvent, AgentEventSink, AgentEventSinkError};
#[cfg(test)]
#[derive(Clone)]
pub(super) struct RegisteredSink {
pub(super) owner: std::thread::ThreadId,
pub(super) sink: Arc<dyn AgentEventSink>,
}
#[cfg(not(test))]
pub(super) type RegisteredSink = Arc<dyn AgentEventSink>;
struct ParentRoute {
parent: String,
owner: std::thread::ThreadId,
}
#[derive(Default)]
struct SessionSinkRegistry {
sinks: HashMap<String, Vec<RegisteredSink>>,
parents: HashMap<String, ParentRoute>,
}
type ExternalSinkRegistry = RwLock<SessionSinkRegistry>;
fn sink_arc(entry: &RegisteredSink) -> &Arc<dyn AgentEventSink> {
#[cfg(test)]
{
&entry.sink
}
#[cfg(not(test))]
{
entry
}
}
fn registered_snapshot(reg: &SessionSinkRegistry, session_id: &str) -> Vec<RegisteredSink> {
let mut result = Vec::new();
let mut visited = HashSet::new();
let mut current = Some(session_id);
while let Some(id) = current {
if !visited.insert(id) {
break;
}
if let Some(entries) = reg.sinks.get(id) {
for entry in entries {
if !result
.iter()
.any(|existing| Arc::ptr_eq(sink_arc(existing), sink_arc(entry)))
{
result.push(entry.clone());
}
}
}
current = reg.parents.get(id).map(|route| route.parent.as_str());
}
result
}
pub(crate) fn link_session_sinks(parent: &str, child: &str) {
external_sinks()
.write()
.expect("sink registry poisoned")
.parents
.insert(
child.to_string(),
ParentRoute {
parent: parent.to_string(),
owner: std::thread::current().id(),
},
);
}
pub(crate) fn clear_session_parent_routes(id: &str) {
let mut reg = external_sinks().write().expect("sink registry poisoned");
reg.parents
.retain(|child, route| child != id || route.owner != std::thread::current().id());
}
pub(crate) fn retire_session_observer_lineage(id: &str) {
external_sinks()
.write()
.expect("sink registry poisoned")
.parents
.retain(|child, route| child != id && route.parent != id);
}
fn external_sinks() -> &'static ExternalSinkRegistry {
static REGISTRY: OnceLock<ExternalSinkRegistry> = OnceLock::new();
REGISTRY.get_or_init(|| RwLock::new(SessionSinkRegistry::default()))
}
pub fn register_sink(session_id: impl Into<String>, sink: Arc<dyn AgentEventSink>) {
let session_id = session_id.into();
let mut reg = external_sinks().write().expect("sink registry poisoned");
#[cfg(test)]
let sink = RegisteredSink {
owner: std::thread::current().id(),
sink,
};
reg.sinks.entry(session_id).or_default().push(sink);
}
pub fn clear_session_sinks(session_id: &str) {
external_sinks()
.write()
.expect("sink registry poisoned")
.sinks
.remove(session_id);
}
pub fn reset_all_sinks() {
#[cfg(test)]
{
let owner = std::thread::current().id();
let mut reg = external_sinks().write().expect("sink registry poisoned");
reg.sinks.retain(|_, sinks| {
sinks.retain(|sink| sink.owner != owner);
!sinks.is_empty()
});
reg.parents.retain(|_, route| route.owner != owner);
drop(reg);
crate::agent_sessions::reset_session_store();
reset_wildcard_sinks();
}
#[cfg(not(test))]
{
let mut reg = external_sinks().write().expect("sink registry poisoned");
reg.sinks.clear();
reg.parents.clear();
drop(reg);
crate::agent_sessions::reset_session_store();
wildcard_sinks()
.write()
.expect("wildcard registry poisoned")
.clear();
}
}
pub fn mirror_session_sinks(source_session_id: &str, target_session_id: &str) {
if source_session_id.is_empty() || target_session_id.is_empty() {
return;
}
if source_session_id == target_session_id {
return;
}
let mut reg = external_sinks().write().expect("sink registry poisoned");
let source_sinks = reg
.sinks
.get(source_session_id)
.cloned()
.unwrap_or_default();
let target = reg.sinks.entry(target_session_id.to_string()).or_default();
for source in source_sinks {
if !target
.iter()
.any(|existing| Arc::ptr_eq(sink_arc(existing), sink_arc(&source)))
{
target.push(source);
}
}
}
pub fn emit_event(event: &AgentEvent) {
for sink in session_sink_snapshot(event.session_id()) {
sink.handle_event(event);
}
super::transport::emit(event);
for sink in wildcard_sink_snapshot() {
sink.handle_event(event);
}
}
pub async fn flush_session_sinks(session_id: &str) -> Result<(), AgentEventSinkError> {
let mut sinks = session_sink_snapshot(session_id);
sinks.extend(wildcard_sink_snapshot());
flush_all_sinks(sinks).await
}
pub async fn flush_and_clear_session_sinks(session_id: &str) -> Result<(), AgentEventSinkError> {
let result = flush_session_sinks(session_id).await;
clear_session_sinks(session_id);
result
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct WildcardSinkHandle(pub(crate) u64);
#[cfg(test)]
#[derive(Clone)]
struct WildcardSinkEntry {
handle: WildcardSinkHandle,
owner: std::thread::ThreadId,
sink: Arc<dyn AgentEventSink>,
}
#[cfg(not(test))]
#[derive(Clone)]
struct WildcardSinkEntry {
handle: WildcardSinkHandle,
sink: Arc<dyn AgentEventSink>,
}
type WildcardSinkRegistry = RwLock<Vec<WildcardSinkEntry>>;
fn wildcard_sinks() -> &'static WildcardSinkRegistry {
static REGISTRY: OnceLock<WildcardSinkRegistry> = OnceLock::new();
REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
}
fn session_sink_snapshot(session_id: &str) -> Vec<Arc<dyn AgentEventSink>> {
let reg = external_sinks().read().expect("sink registry poisoned");
registered_snapshot(®, session_id)
.iter()
.map(|entry| sink_arc(entry).clone())
.collect()
}
fn wildcard_sink_snapshot() -> Vec<Arc<dyn AgentEventSink>> {
let reg = wildcard_sinks().read().expect("wildcard registry poisoned");
#[cfg(test)]
{
let owner = std::thread::current().id();
reg.iter()
.filter(|entry| entry.owner == owner)
.map(|entry| entry.sink.clone())
.collect()
}
#[cfg(not(test))]
{
reg.iter().map(|entry| entry.sink.clone()).collect()
}
}
fn next_wildcard_handle() -> WildcardSinkHandle {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
WildcardSinkHandle(COUNTER.fetch_add(1, Ordering::SeqCst))
}
pub fn register_wildcard_sink(sink: Arc<dyn AgentEventSink>) -> WildcardSinkHandle {
let handle = next_wildcard_handle();
let mut reg = wildcard_sinks()
.write()
.expect("wildcard registry poisoned");
#[cfg(test)]
let entry = WildcardSinkEntry {
handle,
owner: std::thread::current().id(),
sink,
};
#[cfg(not(test))]
let entry = WildcardSinkEntry { handle, sink };
reg.push(entry);
handle
}
pub fn unregister_wildcard_sink(handle: WildcardSinkHandle) {
let mut reg = wildcard_sinks()
.write()
.expect("wildcard registry poisoned");
reg.retain(|entry| entry.handle != handle);
}
#[cfg(test)]
pub fn reset_wildcard_sinks() {
let owner = std::thread::current().id();
let mut reg = wildcard_sinks()
.write()
.expect("wildcard registry poisoned");
reg.retain(|entry| entry.owner != owner);
}
pub fn session_external_sink_count(session_id: &str) -> usize {
session_sink_snapshot(session_id).len()
}
pub fn session_has_external_sink(session_id: &str, sink: &Arc<dyn AgentEventSink>) -> bool {
session_sink_snapshot(session_id)
.iter()
.any(|entry| Arc::ptr_eq(entry, sink))
}
pub fn session_closure_subscriber_count(session_id: &str) -> usize {
crate::agent_sessions::subscriber_count(session_id)
}