use std::sync::Arc;
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use topcoat_core::{context::Cx, error::Result};
use topcoat_router::{
Body, HeaderMap, HeaderName, HeaderValue, Method, RemoteAddr, Router, Uri,
content::{
ViewResponseDelivery,
websocket::{Message, WebSocket, WebSocketUpgrade},
},
header,
request::{FromRequest, IDENTITY_HEADER, Request, extensions, headers, method, uri},
response::Response,
router,
};
use crate::{ConnectedRender, RUNTIME_PROTOCOL, SignalValues};
pub(super) fn requested(cx: &Cx) -> bool {
*method(cx) == Method::GET && requests_runtime_protocol(headers(cx))
}
fn requests_runtime_protocol(headers: &HeaderMap) -> bool {
headers
.get_all(header::SEC_WEBSOCKET_PROTOCOL)
.iter()
.filter_map(|value| value.to_str().ok())
.flat_map(|value| value.split(','))
.any(|protocol| protocol.trim() == RUNTIME_PROTOCOL)
}
pub(super) async fn accept(cx: &Cx, body: Body) -> Result<Response> {
let upgrade = WebSocketUpgrade::from_request(cx, body).await?;
let target = Arc::new(ConnectionTarget::from_handshake(cx));
upgrade
.protocols([RUNTIME_PROTOCOL])
.on_upgrade(move |socket| run(target, socket))
}
#[derive(Debug, Deserialize)]
struct RenderRequest {
#[serde(default)]
run: u64,
#[serde(default)]
shard: Option<String>,
#[serde(default)]
signals: SignalValues,
}
struct Render {
request: RenderRequest,
text: String,
}
#[derive(Serialize)]
#[serde(tag = "t", rename_all = "snake_case")]
enum ConnectionMessage<'a> {
Run { id: u64 },
Redirect { location: &'a str },
Error { status: u16 },
}
impl ConnectionMessage<'_> {
fn to_message(&self) -> Message {
Message::text(serde_json::to_string(self).expect("a connection message serializes"))
}
}
struct ConnectionTarget {
router: Router,
uri: Uri,
headers: HeaderMap,
remote: Option<RemoteAddr>,
}
impl ConnectionTarget {
fn from_handshake(cx: &Cx) -> Self {
let mut headers = headers(cx).clone();
for name in [
header::CONNECTION,
header::UPGRADE,
header::SEC_WEBSOCKET_KEY,
header::SEC_WEBSOCKET_VERSION,
header::SEC_WEBSOCKET_PROTOCOL,
header::SEC_WEBSOCKET_EXTENSIONS,
header::ACCEPT_ENCODING,
] {
headers.remove(name);
}
Self {
router: router(cx),
uri: uri(cx).clone(),
headers,
remote: extensions(cx).get::<RemoteAddr>().copied(),
}
}
fn request(&self, method: Method, headers: HeaderMap, body: Body) -> Request {
let mut request = Request::new(body);
*request.method_mut() = method;
*request.uri_mut() = self.uri.clone();
*request.headers_mut() = headers;
if let Some(remote) = self.remote {
request.extensions_mut().insert(remote);
}
request
}
async fn render(&self, render: Render, out: mpsc::Sender<Message>) {
let Render { request, text } = render;
let run = ConnectionMessage::Run { id: request.run }.to_message();
if out.send(run).await.is_err() {
return;
}
let response = match request.shard {
Some(identity) => {
let Ok(identity) = HeaderValue::try_from(identity) else {
let _ = out
.send(ConnectionMessage::Error { status: 400 }.to_message())
.await;
return;
};
let mut headers = self.headers.clone();
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
headers.insert(HeaderName::from_static(IDENTITY_HEADER), identity);
self.router
.handle_with(
self.request(Method::POST, headers, Body::from(text)),
(ConnectedRender, ViewResponseDelivery::Frames),
)
.await
}
None => {
self.router
.handle_with(
self.request(Method::GET, self.headers.clone(), Body::empty()),
(
ConnectedRender,
request.signals,
ViewResponseDelivery::Frames,
),
)
.await
}
};
let status = response.status();
if status.is_redirection()
&& let Some(location) = response.headers().get(header::LOCATION)
&& let Ok(location) = location.to_str()
{
let _ = out
.send(ConnectionMessage::Redirect { location }.to_message())
.await;
return;
}
let has_frames = response
.headers()
.get(header::CONTENT_TYPE)
.is_some_and(|value| value == "application/x-ndjson");
if !status.is_success() || !has_frames {
let _ = out
.send(
ConnectionMessage::Error {
status: status.as_u16(),
}
.to_message(),
)
.await;
return;
}
let mut frames = response.into_body().into_data_stream();
while let Some(frame) = frames.next().await {
let message = match frame {
Ok(bytes) => match String::from_utf8(bytes.to_vec()) {
Ok(text) => Message::text(text),
Err(_) => ConnectionMessage::Error { status: 500 }.to_message(),
},
Err(_) => ConnectionMessage::Error { status: 500 }.to_message(),
};
if out.send(message).await.is_err() {
return;
}
}
}
}
async fn run(target: Arc<ConnectionTarget>, socket: WebSocket) {
let (mut sink, mut stream) = socket.split();
let (out, mut queue) = mpsc::channel::<Message>(16);
let forward = async move {
while let Some(message) = queue.recv().await {
if sink.send(message).await.is_err() {
break;
}
}
};
let receive = async move {
let mut current: Option<tokio::task::JoinHandle<()>> = None;
while let Some(Ok(message)) = stream.next().await {
let Message::Text(text) = message else {
continue;
};
let Ok(request) = serde_json::from_str::<RenderRequest>(text.as_str()) else {
let error = ConnectionMessage::Error { status: 400 }.to_message();
if out.send(error).await.is_err() {
break;
}
continue;
};
if let Some(current) = current.take() {
current.abort();
let _ = current.await;
}
let render = Render {
request,
text: text.as_str().to_owned(),
};
let target = Arc::clone(&target);
let out = out.clone();
current = Some(tokio::spawn(async move {
target.render(render, out).await;
}));
}
if let Some(current) = current {
current.abort();
}
};
futures_util::future::join(forward, receive).await;
}
#[cfg(test)]
mod tests {
use topcoat_router::HeaderValue;
use super::*;
fn headers_with_protocols(value: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(
header::SEC_WEBSOCKET_PROTOCOL,
HeaderValue::from_static(value),
);
headers
}
#[test]
fn the_runtime_protocol_is_recognized_among_others() {
assert!(requests_runtime_protocol(&headers_with_protocols(
"topcoat-runtime"
)));
assert!(requests_runtime_protocol(&headers_with_protocols(
"chat.v1, topcoat-runtime"
)));
assert!(!requests_runtime_protocol(&headers_with_protocols(
"topcoat-runtime-v2"
)));
assert!(!requests_runtime_protocol(&HeaderMap::new()));
}
}