use futures::stream;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{RwLock, broadcast};
use tokio_stream::Stream;
use tracing::{debug, error};
#[derive(Debug, Clone)]
pub enum SseEvent {
Connected,
Data(Value),
Error(String),
KeepAlive,
}
impl SseEvent {
pub fn format(&self) -> String {
match self {
SseEvent::Connected => {
"event: message\ndata: {\"type\":\"connected\",\"message\":\"SSE connection established\"}\n\n".to_string()
}
SseEvent::Data(data) => {
format!(
"event: message\ndata: {}\n\n",
serde_json::to_string(data).unwrap_or_else(|_| "{}".to_string())
)
}
SseEvent::Error(msg) => {
format!(
"event: message\ndata: {{\"error\":\"{}\"}}\n\n",
msg.replace('"', "\\\"")
)
}
SseEvent::KeepAlive => {
": keepalive\n\n".to_string()
}
}
}
}
pub struct SseManager {
sender: broadcast::Sender<SseEvent>,
connections: Arc<RwLock<HashMap<String, SseConnection>>>,
}
#[derive(Debug)]
pub struct SseConnection {
pub id: String,
pub receiver: broadcast::Receiver<SseEvent>,
}
impl SseManager {
pub fn new() -> Self {
let (sender, _) = broadcast::channel(1000);
Self {
sender,
connections: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn create_connection(&self, connection_id: String) -> SseConnection {
let receiver = self.sender.subscribe();
let connection = SseConnection {
id: connection_id.clone(),
receiver,
};
{
let mut connections = self.connections.write().await;
connections.insert(
connection_id,
SseConnection {
id: connection.id.clone(),
receiver: self.sender.subscribe(),
},
);
}
debug!("SSE connection created: {}", connection.id);
let _ = self.sender.send(SseEvent::Connected);
connection
}
pub async fn remove_connection(&self, connection_id: &str) {
let mut connections = self.connections.write().await;
connections.remove(connection_id);
debug!("SSE connection removed: {}", connection_id);
}
pub async fn broadcast(&self, event: SseEvent) {
if let Err(err) = self.sender.send(event) {
error!("Failed to broadcast SSE event: {}", err);
}
}
pub async fn send_data(&self, data: Value) {
self.broadcast(SseEvent::Data(data)).await;
}
pub async fn send_error(&self, message: String) {
self.broadcast(SseEvent::Error(message)).await;
}
pub async fn send_keep_alive(&self) {
self.broadcast(SseEvent::KeepAlive).await;
}
pub async fn connection_count(&self) -> usize {
let connections = self.connections.read().await;
connections.len()
}
}
impl Default for SseManager {
fn default() -> Self {
Self::new()
}
}
impl SseConnection {
pub fn into_stream(self) -> impl Stream<Item = Result<String, broadcast::error::RecvError>> {
stream::unfold(self, |mut connection| async move {
match connection.receiver.recv().await {
Ok(event) => {
let formatted = event.format();
Some((Ok(formatted), connection))
}
Err(err) => Some((Err(err), connection)),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_sse_event_format() {
let connected = SseEvent::Connected;
assert!(connected.format().contains("event: message"));
let data = SseEvent::Data(json!({"message": "test"}));
assert!(data.format().contains("event: message"));
let error = SseEvent::Error("test error".to_string());
assert!(error.format().contains("event: message"));
let ping = SseEvent::KeepAlive;
assert!(!ping.format().contains("event:"));
assert!(ping.format().starts_with(":"));
}
#[tokio::test]
async fn test_sse_manager() {
let manager = SseManager::new();
assert_eq!(manager.connection_count().await, 0);
let _conn = manager.create_connection("test-123".to_string()).await;
assert_eq!(manager.connection_count().await, 1);
manager.remove_connection("test-123").await;
assert_eq!(manager.connection_count().await, 0);
}
#[tokio::test]
async fn test_broadcast() {
let manager = SseManager::new();
let mut conn = manager.create_connection("test-456".to_string()).await;
if let Ok(event) = conn.receiver.recv().await {
assert!(matches!(event, SseEvent::Connected));
}
manager.send_data(json!({"test": "message"})).await;
if let Ok(event) = conn.receiver.recv().await {
match event {
SseEvent::Data(data) => {
assert_eq!(data["test"], "message");
}
_ => panic!("Expected data event"),
}
}
}
}