gitdataai/libs/transport/handler/poll.rs
ZhenYi 14f6e1e500 feat(core): initialize project with access control and AI integration
- 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
2026-05-03 06:04:31 +08:00

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,
}
}