use std::pin::Pin;
use std::time::Duration;
use futures::stream::{self, Stream};
use crate::axum::http::HeaderMap;
use crate::axum::response::sse::{Event, KeepAlive, Sse};
use crate::axum::response::{IntoResponse, Response};
use crate::realtime::channel::{Broadcast, ChannelPayload, Subscription};
use crate::realtime::error::{ChannelError, ProtocolHint, RealtimeError, admission_status};
use crate::realtime::origin::{OriginDecision, OriginPolicy};
use crate::realtime::registry::{ConnectionGuard, Registry};
use crate::realtime::shutdown::ShutdownConfig;
#[derive(Clone, Copy, Debug)]
pub struct SseLimits {
pub retry_ms: u64,
pub keep_alive_interval: Duration,
}
impl SseLimits {
#[must_use]
pub fn conservative() -> Self {
Self {
retry_ms: 3000,
keep_alive_interval: Duration::from_secs(15),
}
}
}
#[derive(Clone)]
pub struct SseEndpoint {
broadcast: Broadcast,
origin: OriginPolicy,
registry: Registry,
limits: SseLimits,
shutdown: ShutdownConfig,
}
impl SseEndpoint {
#[must_use]
pub fn new(
broadcast: Broadcast,
origin: OriginPolicy,
registry: Registry,
limits: SseLimits,
shutdown: ShutdownConfig,
) -> Self {
Self {
broadcast,
origin,
registry,
limits,
shutdown,
}
}
pub async fn handle(self, headers: HeaderMap, channel_id: String) -> Response {
let _ = channel_id;
if self.origin.authorize(headers.get("origin")) == OriginDecision::Denied {
tracing::debug!(
target: "arcature::realtime::sse::admit",
realtime_transport = "sse",
error_category = "origin",
"realtime sse rejected: origin policy"
);
return admission_status(&RealtimeError::Origin).into_response();
}
let guard = match self.registry.acquire(self.shutdown.max_connections()) {
Ok(g) => g,
Err(_) => {
tracing::debug!(
target: "arcature::realtime::sse::admit",
realtime_transport = "sse",
error_category = "limit",
"realtime sse rejected: connection limit"
);
return admission_status(&RealtimeError::ConnectionLimit).into_response();
}
};
let sub = self.broadcast.subscribe();
let shutdown = self.shutdown;
let retry_ms = self.limits.retry_ms;
let event_stream = sse_event_stream(sub, guard, shutdown, retry_ms);
let keep_alive = if self.limits.keep_alive_interval.is_zero() {
None
} else {
Some(
KeepAlive::new()
.interval(self.limits.keep_alive_interval)
.text("arcature-keepalive"),
)
};
let event_stream: Pin<
Box<dyn Stream<Item = Result<Event, std::convert::Infallible>> + Send>,
> = Box::pin(event_stream);
match keep_alive {
Some(ka) => Sse::new(event_stream).keep_alive(ka).into_response(),
None => Sse::new(event_stream).into_response(),
}
}
}
struct SseState {
sub: Subscription,
_guard: ConnectionGuard,
shutdown: ShutdownConfig,
retry_ms: u64,
yielded_retry: bool,
draining: bool,
channel_closed: bool,
}
fn sse_event_stream(
sub: Subscription,
guard: ConnectionGuard,
shutdown: ShutdownConfig,
retry_ms: u64,
) -> impl Stream<Item = Result<Event, std::convert::Infallible>> + Send {
let init = SseState {
sub,
_guard: guard,
shutdown,
retry_ms,
yielded_retry: false,
draining: false,
channel_closed: false,
};
stream::unfold(init, |mut state| async move {
if !state.yielded_retry {
state.yielded_retry = true;
return Some((
Ok(Event::default().retry(Duration::from_millis(state.retry_ms))),
state,
));
}
if state.draining {
return None;
}
if state.shutdown.is_draining() {
state.draining = true;
return Some((Ok(Event::default().comment("server-draining")), state));
}
if state.channel_closed {
return None;
}
tokio::select! {
_ = state.shutdown.drain_notified() => {
state.draining = true;
Some((Ok(Event::default().comment("server-draining")), state))
}
recv = state.sub.recv() => {
match recv {
Ok(payload) => match event_from_payload(payload) {
Ok(ev) => Some((Ok(ev), state)),
Err(hint) => {
Some((
Ok(Event::default().comment(format!("decode-error:{hint}"))),
state,
))
}
},
Err(ChannelError::Lagged) => {
Some((Ok(Event::default().comment("channel-lagged")), state))
}
Err(ChannelError::Closed) => {
state.channel_closed = true;
Some((Ok(Event::default().comment("channel-closed")), state))
}
Err(ChannelError::Full) => None,
}
}
}
})
}
fn event_from_payload(payload: ChannelPayload) -> Result<Event, ProtocolHint> {
let s = std::str::from_utf8(payload.as_bytes()).map_err(|_| ProtocolHint::Utf8)?;
Ok(Event::default().data(s))
}