use std::collections::HashMap; use std::hash::Hash; use std::sync::{Arc, Mutex}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum OutputSinkError { Closed, Full, Other(String), } pub trait OutputSink: Send + Sync + 'static { fn send(&self, item: T) -> Result<(), OutputSinkError>; } type SharedOutputSink = Arc>; type SinkRegistry = HashMap)>; pub struct OutputBridge where K: Copy + Eq + Hash, { sinks: Mutex>, } impl Default for OutputBridge where K: Copy + Eq + Hash, T: 'static, { fn default() -> Self { Self::new() } } impl OutputBridge where K: Copy + Eq + Hash, T: 'static, { #[must_use] pub fn new() -> Self { Self { sinks: Mutex::new(HashMap::new()), } } pub fn register(&self, key: K, sink: SharedOutputSink) -> u64 { if let Ok(mut map) = self.sinks.lock() { let gen = map.get(&key).map_or(0, |(g, _)| g.wrapping_add(1)); map.insert(key, (gen, sink)); gen } else { 0 } } pub fn unregister(&self, key: &K) { if let Ok(mut map) = self.sinks.lock() { map.remove(key); } } pub fn unregister_if(&self, key: &K, gen: u64) { if let Ok(mut map) = self.sinks.lock() { if matches!(map.get(key), Some((g, _)) if *g == gen) { map.remove(key); } } } pub fn try_send_output(&self, key: &K, item: T) -> Result<(), OutputSinkError> { let sink = { let Ok(map) = self.sinks.lock() else { return Err(OutputSinkError::Closed); }; map.get(key) .map(|(_, sink)| Arc::clone(sink)) .ok_or(OutputSinkError::Closed)? }; sink.send(item) } pub fn send_output(&self, key: &K, item: T) -> bool { self.try_send_output(key, item).is_ok() } #[must_use] pub fn active_sessions(&self) -> usize { self.sinks.lock().map(|m| m.len()).unwrap_or(0) } } struct ReplayEntry { generation: u64, sink: Option>, scrollback: Vec, } pub struct ReplayOutputBridge where K: Copy + Eq + Hash, { entries: Mutex>>, max_chunks: usize, max_bytes: usize, measure: fn(&T) -> usize, } impl ReplayOutputBridge where K: Copy + Eq + Hash, T: Clone + 'static, { #[must_use] pub fn new(max_chunks: usize, max_bytes: usize, measure: fn(&T) -> usize) -> Self { Self { entries: Mutex::new(HashMap::new()), max_chunks, max_bytes, measure, } } pub fn register(&self, key: K, sink: SharedOutputSink) -> u64 { if let Ok(mut map) = self.entries.lock() { match map.get_mut(&key) { Some(entry) => { entry.generation = entry.generation.wrapping_add(1); entry.sink = Some(sink); entry.generation } None => { map.insert( key, ReplayEntry { generation: 0, sink: Some(sink), scrollback: Vec::new(), }, ); 0 } } } else { 0 } } #[must_use] pub fn scrollback(&self, key: &K) -> Vec { self.entries .lock() .ok() .and_then(|m| m.get(key).map(|e| e.scrollback.clone())) .unwrap_or_default() } pub fn unregister(&self, key: &K) { if let Ok(mut map) = self.entries.lock() { map.remove(key); } } pub fn detach_if(&self, key: &K, gen: u64) { if let Ok(mut map) = self.entries.lock() { if let Some(entry) = map.get_mut(key) { if entry.generation == gen { entry.sink = None; } } } } pub fn try_send_output(&self, key: &K, item: T) -> Result<(), OutputSinkError> { let sink = { let Ok(mut map) = self.entries.lock() else { return Err(OutputSinkError::Closed); }; let Some(entry) = map.get_mut(key) else { return Err(OutputSinkError::Closed); }; entry.scrollback.push(item.clone()); trim_scrollback(entry, self.max_chunks, self.max_bytes, self.measure); entry.sink.as_ref().map(Arc::clone) }; match sink { Some(sink) => sink.send(item), None => Err(OutputSinkError::Closed), } } pub fn send_output(&self, key: &K, item: T) -> bool { self.try_send_output(key, item).is_ok() } #[must_use] pub fn active_sessions(&self) -> usize { self.entries.lock().map(|m| m.len()).unwrap_or(0) } } fn trim_scrollback( entry: &mut ReplayEntry, max_chunks: usize, max_bytes: usize, measure: fn(&T) -> usize, ) { while !entry.scrollback.is_empty() && (entry.scrollback.len() > max_chunks || entry.scrollback.iter().map(measure).sum::() > max_bytes) { entry.scrollback.remove(0); } }