gitdataai/libs/transport/handler/session.rs

190 lines
6.5 KiB
Rust

use std::sync::Arc;
use dashmap::DashMap;
use tokio::sync::broadcast;
use models::RoomId;
use queue::{RoomMessageEvent, RoomMessageStreamChunkEvent, TypingEvent};
use service::AppService;
use crate::error::AppTransportError;
use crate::seq::SeqAllocator;
use crate::token::AppTransportTokenContext;
// ─── Constants ────────────────────────────────────────────────────────────────
pub const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
pub const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(60);
pub const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
pub const RATE_LIMIT_WINDOW: Duration = Duration::from_secs(1);
pub const MAX_MESSAGES_PER_SECOND: u32 = 1000;
pub const MAX_TEXT_MESSAGE_LEN: usize = 64 * 1024;
use std::time::Duration;
// ─── User Context ─────────────────────────────────────────────────────────────
#[derive(Clone)]
pub struct WsUserCtx {
pub user_id: uuid::Uuid,
pub device_id: String,
pub client_id: String,
/// Cached display name resolved at WS connect time.
pub display_name: String,
}
impl From<AppTransportTokenContext> for WsUserCtx {
fn from(ctx: AppTransportTokenContext) -> Self {
Self {
user_id: ctx.user_id,
device_id: ctx.device_id,
client_id: ctx.client_id,
display_name: ctx.user_id.to_string(),
}
}
}
// ─── Per-Room Subscription ────────────────────────────────────────────────────
use tokio::sync::Mutex;
pub struct RoomSubscription {
pub room_id: RoomId,
pub msg_rx: Mutex<broadcast::Receiver<Arc<RoomMessageEvent>>>,
pub stream_rx: Mutex<broadcast::Receiver<Arc<RoomMessageStreamChunkEvent>>>,
pub typing_rx: Mutex<broadcast::Receiver<Arc<TypingEvent>>>,
}
impl RoomSubscription {
pub fn new(
room_id: RoomId,
msg_rx: broadcast::Receiver<Arc<RoomMessageEvent>>,
stream_rx: broadcast::Receiver<Arc<RoomMessageStreamChunkEvent>>,
typing_rx: broadcast::Receiver<Arc<TypingEvent>>,
) -> Self {
Self {
room_id,
msg_rx: Mutex::new(msg_rx),
stream_rx: Mutex::new(stream_rx),
typing_rx: Mutex::new(typing_rx),
}
}
}
// ─── TransportSession ─────────────────────────────────────────────────────────
pub struct TransportSession {
pub user: WsUserCtx,
pub subscriptions: Arc<DashMap<RoomId, RoomSubscription>>,
pub seq: Arc<SeqAllocator>,
pub service: Arc<AppService>,
/// Cached project_id resolved from the first subscribed room.
/// Avoids repeated DB lookups on every PresenceUpdate.
pub project_id: Mutex<Option<uuid::Uuid>>,
}
impl TransportSession {
pub fn new(user: WsUserCtx, service: Arc<AppService>) -> Self {
Self {
user,
subscriptions: Arc::new(DashMap::new()),
seq: Arc::new(SeqAllocator::new(service.cache.clone(), service.db.clone())),
service,
project_id: Mutex::new(None),
}
}
pub async fn next_seq(&self, room: RoomId) -> Result<i64, AppTransportError> {
self.seq.seq(room).await
}
pub async fn subscribe_room(
&self,
room_id: RoomId,
) -> Result<RoomSubscription, AppTransportError> {
let manager = &self.service.room.room_manager;
let rx = manager
.subscribe(room_id, self.user.user_id)
.await
.map_err(|_| AppTransportError::Internal)?;
let stream_rx = manager.subscribe_room_stream(room_id).await;
let typing_rx = manager.subscribe_typing(room_id).await;
Ok(RoomSubscription::new(room_id, rx, stream_rx, typing_rx))
}
pub async fn unsubscribe_room(&self, room_id: RoomId) {
self.service
.room
.room_manager
.unsubscribe(room_id, self.user.user_id)
.await;
self.subscriptions.remove(&room_id);
}
pub async fn broadcast_typing(&self, room_id: RoomId, action: &str) {
let event = TypingEvent {
room_id,
user_id: self.user.user_id,
username: self.user.display_name.clone(),
avatar_url: None,
action: action.to_string(),
sender_type: Some("user".to_string()),
};
self.service
.room
.room_manager
.broadcast_typing(room_id, event)
.await;
}
/// Get the current project context from cache (populated on first subscription).
pub async fn get_current_project(&self) -> Option<uuid::Uuid> {
let cached = self.project_id.lock().await;
if cached.is_some() {
return *cached;
}
drop(cached);
// Lazy init: query first subscribed room, cache result.
let first_room = self.subscriptions.iter().next().map(|r| *r.key());
if let Some(room_id) = first_room {
use models::rooms::room;
use sea_orm::EntityTrait;
if let Ok(Some(rm)) = room::Entity::find_by_id(room_id)
.one(&self.service.db)
.await
{
let mut cached = self.project_id.lock().await;
*cached = Some(rm.project);
return Some(rm.project);
}
}
None
}
/// Refresh cached project_id — call after Subscribe/Unsubscribe if needed.
pub async fn refresh_project(&self) {
let first_room = self.subscriptions.iter().next().map(|r| *r.key());
let new_project = if let Some(room_id) = first_room {
use models::rooms::room;
use sea_orm::EntityTrait;
room::Entity::find_by_id(room_id)
.one(&self.service.db)
.await
.ok()
.flatten()
.map(|rm| rm.project)
} else {
None
};
let mut cached = self.project_id.lock().await;
*cached = new_project;
}
pub fn to_session(&self) -> session::Session {
let s = session::Session::no_op();
s.set_user(self.user.user_id);
s
}
}