feat: Sse server auto ping (#74)
1. auto ping in sse stream every second to make cursor happy 2. configurable sse keep alive --------- Co-authored-by: = <=>
This commit is contained in:
parent
588a013090
commit
923e87af4d
2 changed files with 22 additions and 10 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
|
use std::{collections::HashMap, io, net::SocketAddr, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
|
|
@ -6,12 +6,11 @@ use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{
|
response::{
|
||||||
Response,
|
Response,
|
||||||
sse::{Event, Sse},
|
sse::{Event, KeepAlive, Sse},
|
||||||
},
|
},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use futures::{Sink, SinkExt, Stream, StreamExt};
|
use futures::{Sink, SinkExt, Stream};
|
||||||
use tokio::io;
|
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tokio_util::sync::{CancellationToken, PollSender};
|
use tokio_util::sync::{CancellationToken, PollSender};
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
|
@ -26,28 +25,33 @@ type TxStore =
|
||||||
Arc<tokio::sync::RwLock<HashMap<SessionId, tokio::sync::mpsc::Sender<ClientJsonRpcMessage>>>>;
|
Arc<tokio::sync::RwLock<HashMap<SessionId, tokio::sync::mpsc::Sender<ClientJsonRpcMessage>>>>;
|
||||||
pub type TransportReceiver = ReceiverStream<RxJsonRpcMessage<RoleServer>>;
|
pub type TransportReceiver = ReceiverStream<RxJsonRpcMessage<RoleServer>>;
|
||||||
|
|
||||||
|
const DEFAULT_AUTO_PING_INTERVAL: Duration = Duration::from_secs(15);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct App {
|
struct App {
|
||||||
txs: TxStore,
|
txs: TxStore,
|
||||||
transport_tx: tokio::sync::mpsc::UnboundedSender<SseServerTransport>,
|
transport_tx: tokio::sync::mpsc::UnboundedSender<SseServerTransport>,
|
||||||
post_path: Arc<str>,
|
post_path: Arc<str>,
|
||||||
|
sse_ping_interval: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
post_path: String,
|
post_path: String,
|
||||||
|
sse_ping_interval: Duration,
|
||||||
) -> (
|
) -> (
|
||||||
Self,
|
Self,
|
||||||
tokio::sync::mpsc::UnboundedReceiver<SseServerTransport>,
|
tokio::sync::mpsc::UnboundedReceiver<SseServerTransport>,
|
||||||
) {
|
) {
|
||||||
let (transport_tx, tranport_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (transport_tx, transport_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
(
|
(
|
||||||
Self {
|
Self {
|
||||||
txs: Default::default(),
|
txs: Default::default(),
|
||||||
transport_tx,
|
transport_tx,
|
||||||
post_path: post_path.into(),
|
post_path: post_path.into(),
|
||||||
|
sse_ping_interval,
|
||||||
},
|
},
|
||||||
tranport_rx,
|
transport_rx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +91,7 @@ async fn sse_handler(
|
||||||
) -> Result<Sse<impl Stream<Item = Result<Event, io::Error>>>, Response<String>> {
|
) -> Result<Sse<impl Stream<Item = Result<Event, io::Error>>>, Response<String>> {
|
||||||
let session = session_id();
|
let session = session_id();
|
||||||
tracing::info!(%session, "sse connection");
|
tracing::info!(%session, "sse connection");
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::{StreamExt, wrappers::ReceiverStream};
|
||||||
use tokio_util::sync::PollSender;
|
use tokio_util::sync::PollSender;
|
||||||
let (from_client_tx, from_client_rx) = tokio::sync::mpsc::channel(64);
|
let (from_client_tx, from_client_rx) = tokio::sync::mpsc::channel(64);
|
||||||
let (to_client_tx, to_client_rx) = tokio::sync::mpsc::channel(64);
|
let (to_client_tx, to_client_rx) = tokio::sync::mpsc::channel(64);
|
||||||
|
|
@ -108,11 +112,12 @@ async fn sse_handler(
|
||||||
if transport_send_result.is_err() {
|
if transport_send_result.is_err() {
|
||||||
tracing::warn!("send transport out error");
|
tracing::warn!("send transport out error");
|
||||||
let mut response =
|
let mut response =
|
||||||
Response::new("fail to send out trasnport, it seems server is closed".to_string());
|
Response::new("fail to send out transport, it seems server is closed".to_string());
|
||||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
return Err(response);
|
return Err(response);
|
||||||
}
|
}
|
||||||
let post_path = app.post_path.as_ref();
|
let post_path = app.post_path.as_ref();
|
||||||
|
let ping_interval = app.sse_ping_interval;
|
||||||
let stream = futures::stream::once(futures::future::ok(
|
let stream = futures::stream::once(futures::future::ok(
|
||||||
Event::default()
|
Event::default()
|
||||||
.event("endpoint")
|
.event("endpoint")
|
||||||
|
|
@ -124,7 +129,7 @@ async fn sse_handler(
|
||||||
Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)),
|
Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)),
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
Ok(Sse::new(stream))
|
Ok(Sse::new(stream).keep_alive(KeepAlive::new().interval(ping_interval)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SseServerTransport {
|
pub struct SseServerTransport {
|
||||||
|
|
@ -190,6 +195,7 @@ impl Stream for SseServerTransport {
|
||||||
mut self: std::pin::Pin<&mut Self>,
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
cx: &mut std::task::Context<'_>,
|
cx: &mut std::task::Context<'_>,
|
||||||
) -> std::task::Poll<Option<Self::Item>> {
|
) -> std::task::Poll<Option<Self::Item>> {
|
||||||
|
use futures::StreamExt;
|
||||||
self.stream.poll_next_unpin(cx)
|
self.stream.poll_next_unpin(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,6 +206,7 @@ pub struct SseServerConfig {
|
||||||
pub sse_path: String,
|
pub sse_path: String,
|
||||||
pub post_path: String,
|
pub post_path: String,
|
||||||
pub ct: CancellationToken,
|
pub ct: CancellationToken,
|
||||||
|
pub sse_keep_alive: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -215,6 +222,7 @@ impl SseServer {
|
||||||
sse_path: "/sse".to_string(),
|
sse_path: "/sse".to_string(),
|
||||||
post_path: "/message".to_string(),
|
post_path: "/message".to_string(),
|
||||||
ct: CancellationToken::new(),
|
ct: CancellationToken::new(),
|
||||||
|
sse_keep_alive: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +248,10 @@ impl SseServer {
|
||||||
/// Warning: This function creates a new SseServer instance with the provided configuration.
|
/// Warning: This function creates a new SseServer instance with the provided configuration.
|
||||||
/// `App.post_path` may be incorrect if using `Router` as an embedded router.
|
/// `App.post_path` may be incorrect if using `Router` as an embedded router.
|
||||||
pub fn new(config: SseServerConfig) -> (SseServer, Router) {
|
pub fn new(config: SseServerConfig) -> (SseServer, Router) {
|
||||||
let (app, transport_rx) = App::new(config.post_path.clone());
|
let (app, transport_rx) = App::new(
|
||||||
|
config.post_path.clone(),
|
||||||
|
config.sse_keep_alive.unwrap_or(DEFAULT_AUTO_PING_INTERVAL),
|
||||||
|
);
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
.route(&config.sse_path, get(sse_handler))
|
.route(&config.sse_path, get(sse_handler))
|
||||||
.route(&config.post_path, post(post_event_handler))
|
.route(&config.post_path, post(post_event_handler))
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
sse_path: "/sse".to_string(),
|
sse_path: "/sse".to_string(),
|
||||||
post_path: "/message".to_string(),
|
post_path: "/message".to_string(),
|
||||||
ct: tokio_util::sync::CancellationToken::new(),
|
ct: tokio_util::sync::CancellationToken::new(),
|
||||||
|
sse_keep_alive: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (sse_server, router) = SseServer::new(config);
|
let (sse_server, router) = SseServer::new(config);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue