use bytes::Bytes;
use connectrpc::{ConnectRpcService, dispatcher::Dispatcher};
use http::{HeaderMap, HeaderName, HeaderValue, Request};
use http_body_util::BodyExt;
use tauri::ipc::{Channel, InvokeResponseBody};
use tower::ServiceExt;
use crate::{body::IpcRequestBody, codec, registry::CallRegistry, wire};
const IPC_ORIGIN: &str = "tauri://ipc";
pub(crate) async fn start<D: Dispatcher>(
service: ConnectRpcService<D>,
calls: std::sync::Arc<CallRegistry>,
start: wire::StartRequest,
channel: Option<Channel<InvokeResponseBody>>,
) -> Result<Vec<u8>, String> {
let call_id = start.call_id;
let (body, body_tx, first_chunk) = if start.streaming_request {
let (tx, body) = IpcRequestBody::channel();
let first = (!start.body.is_empty()).then(|| Bytes::from(start.body));
(body, Some(tx), first)
} else {
(
IpcRequestBody::complete(Bytes::from(start.body)),
None,
None,
)
};
let request = build_request(&start.url, &start.method, &start.headers, body)?;
let Some(channel) = channel else {
return buffered(service, request).await;
};
let (head_tx, head_rx) = tokio::sync::oneshot::channel();
let registration = calls.begin(call_id, body_tx.clone());
let pump = tokio::spawn({
let calls = std::sync::Arc::clone(&calls);
async move {
let response = match service.oneshot(request).await {
Ok(response) => response,
Err(e) => {
let _ = head_tx.send(Err(format!("connect service failed: {e}")));
return;
}
};
let (parts, mut response_body) = response.into_parts();
let head = wire::ResponseHead {
status: u32::from(parts.status.as_u16()),
headers: to_wire_headers(&parts.headers),
..Default::default()
};
if head_tx.send(Ok(codec::encode(&head))).is_err() {
return;
}
while let Some(frame) = response_body.frame().await {
tokio::task::coop::consume_budget().await;
let frame = match frame {
Ok(frame) => frame,
Err(e) => {
send_frame(&channel, wire::response_frame::Frame::Error(e.to_string()));
break;
}
};
let wire_frame = if let Some(data) = frame.data_ref() {
wire::response_frame::Frame::Message(data.to_vec())
} else if let Some(trailers) = frame.trailers_ref() {
wire::response_frame::Frame::Trailers(Box::new(wire::Trailers {
headers: to_wire_headers(trailers),
..Default::default()
}))
} else {
continue;
};
if !send_frame(&channel, wire_frame) {
break;
}
}
send_frame(&channel, wire::response_frame::Frame::End(Box::default()));
calls.forget(call_id);
}
});
registration.attach(pump.abort_handle());
if let (Some(tx), Some(chunk)) = (body_tx, first_chunk) {
tx.send(chunk)
.await
.map_err(|_| "request body closed".to_string())?;
}
head_rx
.await
.unwrap_or_else(|_| Err("call cancelled".to_string()))
}
fn send_frame(channel: &Channel<InvokeResponseBody>, frame: wire::response_frame::Frame) -> bool {
let encoded = codec::encode(&wire::ResponseFrame {
frame: Some(frame),
..Default::default()
});
channel.send(InvokeResponseBody::Raw(encoded)).is_ok()
}
async fn buffered<D: Dispatcher>(
service: ConnectRpcService<D>,
request: Request<IpcRequestBody>,
) -> Result<Vec<u8>, String> {
let response = service
.oneshot(request)
.await
.map_err(|e| format!("connect service failed: {e}"))?;
let (parts, body) = response.into_parts();
let collected = body
.collect()
.await
.map_err(|e| format!("response body failed: {e}"))?;
Ok(codec::encode(&wire::ResponseHead {
status: u32::from(parts.status.as_u16()),
headers: to_wire_headers(&parts.headers),
body: collected.to_bytes().to_vec(),
..Default::default()
}))
}
fn build_request(
url: &str,
method: &str,
headers: &[wire::Header],
body: IpcRequestBody,
) -> Result<Request<IpcRequestBody>, String> {
let mut builder = Request::builder()
.method(http::Method::from_bytes(method.as_bytes()).map_err(|_| "invalid HTTP method")?)
.uri(format!("{IPC_ORIGIN}{url}"));
for header in headers {
let name =
HeaderName::from_bytes(header.name.as_bytes()).map_err(|_| "invalid header name")?;
let value = HeaderValue::from_str(&header.value).map_err(|_| "invalid header value")?;
builder = builder.header(name, value);
}
builder.body(body).map_err(|e| e.to_string())
}
fn to_wire_headers(headers: &HeaderMap) -> Vec<wire::Header> {
headers
.iter()
.filter_map(|(name, value)| {
let value = value.to_str().ok()?;
Some(wire::Header {
name: name.as_str().to_string(),
value: value.to_string(),
..Default::default()
})
})
.collect()
}