- Add gitignore and prettier configuration files for project scaffolding - Implement room access control service with project member verification - Create user access key management with CRUD operations and activity logging - Add accordion UI component for frontend expandable sections - Implement room AI configuration with list, upsert, and delete operations - Add AI event types for agent join/leave/status change tracking - Create streaming AI processing services for mode and react patterns - Build room AI service with model detection and idempotency handling - Integrate chat service orchestration for AI message processing - Add typing indicators and stream cancellation for AI interactions - Implement mention parsing and context extraction for AI agents
84 lines
3.4 KiB
Rust
84 lines
3.4 KiB
Rust
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<WsOutEvent> {
|
|
let room_ids: Vec<RoomId> = 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::<Arc<RoomMessageEvent>>(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<Arc<NotificationEvent>>,
|
|
) -> Option<WsOutEvent> {
|
|
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,
|
|
}
|
|
} |