cratestack_axum/rpc/
sse_subscription.rs1use axum::body::{Body, Bytes};
18use axum::response::Response;
19use cratestack_core::SubscriptionStream;
20use cratestack_core::rpc::RpcErrorBody;
21use futures_util::future::{Either, select};
22use futures_util::stream::{self, StreamExt};
23
24use super::sse::{format_error_event, format_message_event, sse_response};
25
26pub(crate) const KEEPALIVE: std::time::Duration = std::time::Duration::from_secs(15);
28
29pub fn encode_subscription_sse_response(items: SubscriptionStream<serde_json::Value>) -> Response {
31 let ready = stream::once(async {
32 Ok::<_, std::convert::Infallible>(Bytes::from_static(
33 b"event: ready\ndata: {\"id\":1,\"ready\":{}}\n\n",
34 ))
35 });
36 let items = stream::unfold((items, 1u64, false), |(mut items, id, ended)| async move {
37 if ended {
38 return None;
39 }
40 let next = match select(items.next(), Box::pin(tokio::time::sleep(KEEPALIVE))).await {
41 Either::Left((next, _)) => next?,
42 Either::Right(((), _)) => {
43 let keepalive = Bytes::from_static(b": keepalive\n\n");
44 return Some((Ok(keepalive), (items, id, false)));
45 }
46 };
47 let id = id + 1;
48 let (bytes, ended): (Bytes, bool) = match next {
49 Ok(item) => (format_message_event(id, &item), false),
50 Err(error) => (
51 format_error_event(id, &RpcErrorBody::from_cratestack(&error)),
52 true,
53 ),
54 };
55 Some((Ok::<_, std::convert::Infallible>(bytes), (items, id, ended)))
56 });
57 sse_response(Body::from_stream(ready.chain(items)))
58}
59
60#[cfg(test)]
61mod tests;