use std::fmt::Debug;
use std::sync::Arc;
use actix_web::{web, HttpRequest, HttpResponse};
use actix_ws::AggregatedMessage;
use async_trait::async_trait;
use futures_util::StreamExt;
use bytestring::ByteString;
use plaza::agent::{Agent, AgentId};
use plaza::error::PlazaError;
use plaza::session::{ConnectionId, MessageTarget, PresenceEvent, Session, SessionMessage, SessionReceiver};
use serde::de::DeserializeOwned;
use serde::Serialize;
use fibre::mpsc;
use tracing::{debug, error, warn};
use crate::codec::{JsonCodec, WireCodec};
use crate::manager::{ConnectionManager, OutboundFrame, TransportSession, DEFAULT_BROADCAST_CAPACITY, DEFAULT_CLIENT_QUEUE_CAPACITY};
const RTT_FAST_PINGS: u32 = 8;
const RTT_FAST_INTERVAL: std::time::Duration = std::time::Duration::from_millis(125);
const RTT_IDLE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
const TRANSPORT: &str = "actix_ws";
const MAX_CONTINUATION_SIZE: usize = 1024 * 1024;
pub struct ActixWsPlazaSession<Op: Send + 'static, ID: AgentId, C: WireCodec = JsonCodec> {
inner: Arc<TransportSession<Op, ID, C>>,
}
impl<Op, ID> ActixWsPlazaSession<Op, ID, JsonCodec>
where
Op: Serialize + DeserializeOwned + Clone + Debug + Send + Sync + 'static,
ID: AgentId,
{
pub fn new() -> Arc<Self> {
Self::with_codec(JsonCodec)
}
}
impl<Op, ID, C> ActixWsPlazaSession<Op, ID, C>
where
Op: Serialize + DeserializeOwned + Clone + Debug + Send + Sync + 'static,
ID: AgentId,
C: WireCodec,
{
pub fn connection_rtt(&self, conn_id: plaza::session::ConnectionId) -> Option<(std::time::Duration, std::time::Duration, u64)> {
let manager = self.inner.manager();
let smoothed = manager.rtt(conn_id)?;
let min = manager.min_rtt(conn_id)?;
Some((smoothed, min, manager.rtt_samples(conn_id)))
}
pub fn agent_rtt(&self, id: &ID) -> Option<(std::time::Duration, u64)> {
self.inner.manager().agent_rtt(id)
}
pub fn stats(&self) -> std::sync::Arc<crate::stats::TransportStats> {
self.inner.manager().stats()
}
pub fn with_codec(codec: C) -> Arc<Self> {
Arc::new(Self {
inner: TransportSession::new(TRANSPORT, codec, DEFAULT_BROADCAST_CAPACITY),
})
}
pub fn handle_connection(
&self,
req: &HttpRequest,
stream: web::Payload,
agent: Agent<ID>,
) -> Result<HttpResponse, actix_web::Error> {
let (response, ws_session, msg_stream) = actix_ws::handle(req, stream)?;
let manager = self.inner.manager().clone();
let codec = self.inner.codec().clone();
actix_web::rt::spawn(async move {
connection_task(ws_session, msg_stream, agent, manager, codec).await;
});
Ok(response)
}
}
async fn connection_task<ID: AgentId, C: WireCodec>(
mut ws_session: actix_ws::Session,
msg_stream: actix_ws::MessageStream,
agent: Agent<ID>,
manager: Arc<ConnectionManager<ID>>,
codec: C,
) {
let mut msg_stream = msg_stream
.aggregate_continuations()
.max_continuation_size(MAX_CONTINUATION_SIZE);
let (to_client_tx, to_client_rx) = mpsc::bounded_async::<OutboundFrame>(DEFAULT_CLIENT_QUEUE_CAPACITY);
let conn_id = manager.register(agent.clone(), to_client_tx);
let send_as_text = codec.is_text();
let mut ping_sent_at: Option<std::time::Instant> = None;
let mut pings_sent: u32 = 0;
let mut next_ping = tokio::time::Instant::now() + RTT_FAST_INTERVAL;
let close_reason = loop {
tokio::select! {
Ok(frame) = to_client_rx.recv() => {
let sent = if send_as_text {
match ByteString::try_from(frame) {
Ok(text) => ws_session.text(text).await,
Err(e) => {
warn!(transport = TRANSPORT, conn_id, error = %e, "Codec claims text but produced non-UTF-8; dropping the frame.");
continue;
}
}
} else {
ws_session.binary(frame).await
};
if sent.is_err() {
debug!(transport = TRANSPORT, conn_id, "Client went away during send.");
break None;
}
}
_ = tokio::time::sleep_until(next_ping) => {
pings_sent += 1;
next_ping = tokio::time::Instant::now() + if pings_sent < RTT_FAST_PINGS { RTT_FAST_INTERVAL } else { RTT_IDLE_INTERVAL };
if ws_session.ping(b"").await.is_err() {
break None;
}
ping_sent_at = Some(std::time::Instant::now());
}
incoming = msg_stream.next() => {
match incoming {
Some(Ok(AggregatedMessage::Binary(bytes))) => {
manager.forward_incoming(agent.clone(), bytes);
}
Some(Ok(AggregatedMessage::Text(text))) => {
manager.forward_incoming(agent.clone(), text.into_bytes());
}
Some(Ok(AggregatedMessage::Ping(payload))) => {
if ws_session.pong(&payload).await.is_err() {
break None;
}
}
Some(Ok(AggregatedMessage::Pong(_))) => {
if let Some(sent) = ping_sent_at.take() {
manager.record_rtt(conn_id, sent.elapsed());
}
}
Some(Ok(AggregatedMessage::Close(reason))) => {
debug!(transport = TRANSPORT, conn_id, ?reason, "Client closed the connection.");
break reason;
}
Some(Err(e)) => {
error!(transport = TRANSPORT, conn_id, error = %e, "WebSocket protocol error; closing.");
break None;
}
None => break None,
}
}
else => break None,
}
};
manager.deregister(conn_id);
let _ = ws_session.close(close_reason).await;
}
#[async_trait]
impl<Op, ID, C> Session<Op, ID> for ActixWsPlazaSession<Op, ID, C>
where
Op: Serialize + DeserializeOwned + Clone + Debug + Send + Sync + 'static,
ID: AgentId,
C: WireCodec,
{
async fn agent_join(&self, agent_info: Agent<ID>) -> Result<ConnectionId, PlazaError<ID>> {
self.inner.agent_join(agent_info).await
}
async fn agent_leave(&self, agent_id: &ID, conn_id: ConnectionId) -> Result<(), PlazaError<ID>> {
self.inner.agent_leave(agent_id, conn_id).await
}
async fn send_message(
&self,
target: MessageTarget<ID>,
msg: SessionMessage<Op, ID>,
) -> Result<(), PlazaError<ID>> {
self.inner.send_message(target, msg).await
}
fn subscribe_to_incoming_messages(&self) -> SessionReceiver<SessionMessage<Op, ID>> {
self.inner.subscribe_to_incoming_messages()
}
fn on_presence_change(&self) -> SessionReceiver<PresenceEvent<ID>> {
self.inner.on_presence_change()
}
}