use std::sync::Arc; use tokio::sync::broadcast; use tokio_stream::StreamExt; use tokio_stream::wrappers::BroadcastStream; use models::RoomId; use queue::RoomMessageEvent; use room::types::NotificationEvent; use super::dispatch::EventDispatcher; use super::session::TransportSession; use super::types::WsOutEvent; /// Poll all active room subscriptions and yield the next available push event. /// AI stream chunks are handled specially: /// - seq=0 (first chunk) → MessageStreamStart (WS only sends message_id) /// - done=true (final chunk) → MessageStreamDone (WS notifies SSE ended) /// - Other chunks → skipped (delivered via SSE endpoint) pub async fn poll_subscriptions(session: &TransportSession) -> Option { let room_ids: Vec = session.subscriptions.iter().map(|r| *r.key()).collect(); if room_ids.is_empty() { tokio::time::sleep(std::time::Duration::from_millis(50)).await; return None; } for room_id in room_ids { let manager = &session.service.room.room_manager; let mut msg_stream = BroadcastStream::new(manager.subscribe(room_id, session.user.user_id).await.unwrap_or_else(|_| { let (_tx, rx) = tokio::sync::broadcast::channel::>(1); rx })); let mut stream_stream = BroadcastStream::new(manager.subscribe_room_stream(room_id).await); let mut typing_stream = BroadcastStream::new(manager.subscribe_typing(room_id).await); let msg = tokio::time::timeout(std::time::Duration::from_millis(10), msg_stream.next()).await; if let Ok(Some(Ok(event))) = msg { if let Some(reactions) = event.reactions.clone() { return Some(EventDispatcher::dispatch_reactions( room_id, event.message_id.unwrap_or(event.id), &reactions, )); } return Some(EventDispatcher::dispatch_message(&event)); } let chunk = tokio::time::timeout(std::time::Duration::from_millis(10), stream_stream.next()).await; if let Ok(Some(Ok(chunk))) = chunk { // WS pipeline: only push message_id for AI streams // Full content delivered via SSE endpoint /ws/ai-stream/{room_id}/{message_id} if chunk.seq == 0 && !chunk.done { return Some(EventDispatcher::dispatch_stream_start(&chunk)); } if chunk.done { return Some(EventDispatcher::dispatch_stream_done(&chunk)); } // seq > 0 && !done: skip — SSE handles intermediate chunks } let typing = tokio::time::timeout(std::time::Duration::from_millis(10), typing_stream.next()).await; if let Ok(Some(Ok(event))) = typing { return Some(EventDispatcher::dispatch_typing(&event)); } } None } /// Poll user-level notification stream. pub async fn poll_notifications( notif_rx: &mut broadcast::Receiver>, ) -> Option { match notif_rx.try_recv() { Ok(event) => Some(EventDispatcher::dispatch_notification(&event)), Err(broadcast::error::TryRecvError::Empty) => None, Err(broadcast::error::TryRecvError::Lagged(n)) => { tracing::warn!(skipped = n, "notification channel lagged"); None } Err(broadcast::error::TryRecvError::Closed) => None, } }