use std::sync::Arc;
use bytes::Bytes;
use tokio::sync::{Notify, mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use wasmtime::component::{Accessor, AccessorTask, HasSelf, Linker, Resource, ResourceTable};
use camel_api::{Body, CamelError, Exchange, ExchangePattern, Message, StreamBody, Value};
use camel_component_api::consumer::ConsumerContext;
use crate::return_stream::{
DEFAULT_DRAIN_CHANNEL_BOUND, DrainCoord, DrainEvent, StreamReturnable, TerminalSlot,
drain_guest_stream, receiver_to_body_stream,
};
use crate::source_bindings::camel::plugin::source_host::{HttpRequest, SubmitOutcome};
use crate::source_bindings::camel::plugin::types::{
WasmBody, WasmExchange, WasmMessage, WasmPattern,
};
pub struct HttpListenerHandle;
pub struct HttpMeta {
pub method: String,
pub path: String,
pub headers: Vec<(String, String)>,
}
type BodyChunkRx = mpsc::Receiver<Result<Bytes, CamelError>>;
type RequestChannelItem = (HttpMeta, BodyChunkRx);
pub const REQUEST_CHANNEL_CAPACITY: usize = 1;
pub const EXCHANGE_CHANNEL_CAPACITY: usize = 1;
pub struct SourceChannels {
pub request_tx: mpsc::Sender<RequestChannelItem>,
pub request_rx: Arc<tokio::sync::Mutex<mpsc::Receiver<RequestChannelItem>>>,
pub exchange_tx: mpsc::Sender<(Exchange, oneshot::Sender<SubmitOutcome>)>,
pub exchange_rx: mpsc::Receiver<(Exchange, oneshot::Sender<SubmitOutcome>)>,
}
impl SourceChannels {
pub fn new() -> Self {
let (request_tx, request_rx) = mpsc::channel(REQUEST_CHANNEL_CAPACITY);
let (exchange_tx, exchange_rx) = mpsc::channel(EXCHANGE_CHANNEL_CAPACITY);
Self {
request_tx,
request_rx: Arc::new(tokio::sync::Mutex::new(request_rx)),
exchange_tx,
exchange_rx,
}
}
}
impl Default for SourceChannels {
fn default() -> Self {
Self::new()
}
}
pub const DEFAULT_MAX_REQUEST_BODY_BYTES: u64 = 10 * 1024 * 1024;
pub struct SourceHostState {
pub table: ResourceTable,
pub wasi: wasmtime_wasi::WasiCtx,
pub request_rx: Arc<tokio::sync::Mutex<mpsc::Receiver<RequestChannelItem>>>,
pub exchange_tx: mpsc::Sender<(Exchange, oneshot::Sender<SubmitOutcome>)>,
pub cancel_token: CancellationToken,
pub max_request_body_bytes: u64,
}
impl wasmtime_wasi::WasiView for SourceHostState {
fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> {
wasmtime_wasi::WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.table,
}
}
}
type SourceWasmError = crate::source_bindings::camel::plugin::types::WasmError;
impl crate::source_bindings::camel::plugin::source_host::HostHttpListener for SourceHostState {
fn drop(
&mut self,
_resource: wasmtime::component::Resource<HttpListenerHandle>,
) -> wasmtime::Result<()> {
Ok(())
}
}
impl crate::source_bindings::camel::plugin::source_host::Host for SourceHostState {
fn is_cancelled(&mut self) -> bool {
self.cancel_token.is_cancelled()
}
}
impl crate::source_bindings::camel::plugin::source_host::HostWithStore<SourceHostState>
for wasmtime::component::HasSelf<SourceHostState>
{
async fn accept_http(
accessor: &Accessor<SourceHostState, wasmtime::component::HasSelf<SourceHostState>>,
_listener: Resource<HttpListenerHandle>,
) -> Result<Option<HttpRequest>, SourceWasmError> {
let (request_rx, cancel_token, max_body) = accessor.with(|mut view| {
let state = view.get();
(
state.request_rx.clone(),
state.cancel_token.clone(),
state.max_request_body_bytes,
)
});
let mut guard = request_rx.lock().await;
let meta_and_body = tokio::select! {
r = guard.recv() => r,
_ = cancel_token.cancelled() => {
return Ok(None);
}
};
drop(guard);
let Some((meta, mut body_rx)) = meta_and_body else {
return Ok(None);
};
let box_stream: futures::stream::BoxStream<'static, Result<Bytes, CamelError>> =
Box::pin(futures::stream::poll_fn(move |cx| body_rx.poll_recv(cx)));
let cancel = cancel_token.clone();
let handle = crate::stream_bridge::assemble_stream_body_source(
accessor, box_stream, cancel, max_body,
);
let request = match handle {
Ok(body) => Some(HttpRequest {
method: meta.method,
path: meta.path,
headers: meta.headers,
body,
}),
Err(e) => {
tracing::error!("failed to assemble stream body for accept-http: {e}");
None
}
};
Ok(request)
}
async fn submit_exchange(
accessor: &Accessor<SourceHostState, wasmtime::component::HasSelf<SourceHostState>>,
mut exchange: WasmExchange,
) -> Result<SubmitOutcome, SourceWasmError> {
let (exchange_tx, cancel_token, stream_parts) = accessor.with(|mut view| {
let state = view.get();
let exchange_tx = state.exchange_tx.clone();
let cancel_token = state.cancel_token.clone();
let stream_parts = exchange.take_stream();
(exchange_tx, cancel_token, stream_parts)
});
let mut native = source_exchange_to_native(exchange);
let (reply_tx, reply_rx) = oneshot::channel();
if let Some((stream_reader, terminal, stream_metadata)) = stream_parts {
let (chunk_tx, chunk_rx) = mpsc::channel::<DrainEvent>(DEFAULT_DRAIN_CHANNEL_BOUND);
let source_terminal: TerminalSlot = Arc::new(std::sync::Mutex::new(None));
let coord = DrainCoord {
cancel: cancel_token.clone(),
progress: Arc::new(Notify::new()),
receiver_gone: Arc::new(Notify::new()),
terminal_slot: source_terminal.clone(),
};
accessor.spawn(SubmitExchangeDrain {
stream_reader,
terminal,
chunk_tx,
coord,
});
native.input.body = Body::Stream(StreamBody {
stream: Arc::new(tokio::sync::Mutex::new(Some(Box::pin(
receiver_to_body_stream(crate::return_stream::DrainReceiver {
rx: chunk_rx,
terminal: source_terminal,
}),
)))),
metadata: stream_metadata,
});
}
let outcome = tokio::select! {
res = async {
if exchange_tx.send((native, reply_tx)).await.is_err() {
return SubmitOutcome::Stopped;
}
reply_rx.await.unwrap_or(SubmitOutcome::Stopped)
} => res,
_ = cancel_token.cancelled() => SubmitOutcome::Stopped,
};
Ok(outcome)
}
}
struct SubmitExchangeDrain {
stream_reader: wasmtime::component::StreamReader<u8>,
terminal: wasmtime::component::FutureReader<Result<(), SourceWasmError>>,
chunk_tx: mpsc::Sender<DrainEvent>,
coord: DrainCoord,
}
impl AccessorTask<SourceHostState, HasSelf<SourceHostState>> for SubmitExchangeDrain {
async fn run(
self,
accessor: &Accessor<SourceHostState, HasSelf<SourceHostState>>,
) -> wasmtime::Result<()> {
drain_guest_stream::<SourceWasmError, SourceHostState>(
accessor,
self.stream_reader,
self.terminal,
self.chunk_tx,
self.coord,
)
.await;
Ok(())
}
}
fn source_exchange_to_native(wasm: WasmExchange) -> Exchange {
let input = source_message_to_native(wasm.input);
let mut exchange = Exchange::new(input);
if !wasm.correlation_id.is_empty() {
exchange.correlation_id = wasm.correlation_id;
}
if let Some(out) = wasm.output {
exchange.output = Some(source_message_to_native(out));
}
for (key, raw) in wasm.properties {
let value = serde_json::from_str::<Value>(&raw).unwrap_or(Value::String(raw));
exchange.properties.insert(key, value);
}
exchange.pattern = match wasm.pattern {
WasmPattern::InOnly => ExchangePattern::InOnly,
WasmPattern::InOut => ExchangePattern::InOut,
};
exchange
}
fn source_message_to_native(msg: WasmMessage) -> Message {
let headers = msg
.headers
.into_iter()
.map(|(k, raw)| {
let value = serde_json::from_str::<Value>(&raw).unwrap_or(Value::String(raw));
(k, value)
})
.collect();
Message {
headers,
body: source_body_to_native(msg.body),
}
}
fn source_body_to_native(body: WasmBody) -> Body {
match body {
WasmBody::Empty => Body::Empty,
WasmBody::Text(s) => Body::Text(s),
WasmBody::Bytes(v) => Body::Bytes(Bytes::from(v)),
WasmBody::Json(s) => serde_json::from_str::<Value>(&s)
.map(Body::Json)
.unwrap_or(Body::Text(s)),
WasmBody::Xml(s) => Body::Xml(s),
WasmBody::Stream(_) => {
tracing::warn!("source: undrained stream body reached conversion — dropped");
Body::Empty
}
}
}
pub fn add_to_linker(linker: &mut Linker<SourceHostState>) -> Result<(), wasmtime::Error> {
wasmtime_wasi::p2::add_to_linker_async(linker)?;
crate::source_bindings::camel::plugin::source_host::add_to_linker::<_, HasSelf<_>>(
linker,
|state| state,
)?;
Ok(())
}
pub async fn run_http_listener(
listener: tokio::net::TcpListener,
path_filter: Option<String>,
request_tx: mpsc::Sender<RequestChannelItem>,
cancel: CancellationToken,
) -> Result<(), CamelError> {
use axum::Router;
use axum::extract::State;
use axum::http::Request;
use axum::response::Response;
use axum::routing::any;
use http_body_util::BodyExt;
struct ListenerState {
tx: mpsc::Sender<RequestChannelItem>,
cancel: CancellationToken,
}
async fn handler(
State(state): State<Arc<ListenerState>>,
req: Request<axum::body::Body>,
) -> Response {
let (parts, body) = req.into_parts();
let (body_tx, body_rx) = mpsc::channel::<Result<Bytes, CamelError>>(
crate::return_stream::DEFAULT_DRAIN_CHANNEL_BOUND,
);
let http_meta = HttpMeta {
method: parts.method.to_string(),
path: parts.uri.path().to_string(),
headers: parts
.headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect(),
};
if state.tx.send((http_meta, body_rx)).await.is_err() {
return Response::builder()
.status(503)
.body(axum::body::Body::from("service unavailable"))
.unwrap(); }
let drain_cancel = state.cancel.clone();
tokio::spawn(async move {
let mut body = body;
loop {
tokio::select! {
frame = body.frame() => {
match frame {
Some(Ok(frame)) => {
if let Ok(data) = frame.into_data()
&& body_tx.send(Ok(data)).await.is_err()
{
break;
}
}
Some(Err(e)) => {
let _ = body_tx
.send(Err(CamelError::Io(format!("body read: {e}"))))
.await;
break;
}
None => break,
}
}
_ = drain_cancel.cancelled() => break,
}
}
});
Response::builder()
.status(202)
.body(axum::body::Body::from("accepted"))
.unwrap() }
let state = Arc::new(ListenerState {
tx: request_tx,
cancel: cancel.clone(),
});
let route_path = path_filter
.filter(|path| !path.is_empty())
.map(|path| {
if path.starts_with('/') {
path
} else {
format!("/{path}")
}
})
.unwrap_or_else(|| "/{*path}".to_string());
let app = Router::new()
.route(&route_path, any(handler))
.with_state(state);
let local = listener.local_addr().ok();
if let Some(addr) = &local {
tracing::info!(%addr, "source HTTP listener started");
}
axum::serve(listener, app)
.with_graceful_shutdown(async move { cancel.cancelled().await })
.await
.map_err(|e| CamelError::Io(format!("HTTP listener error: {e}")))?;
if let Some(addr) = &local {
tracing::info!(%addr, "source HTTP listener stopped");
}
Ok(())
}
pub async fn run_pipeline_bridge(
mut exchange_rx: mpsc::Receiver<(Exchange, oneshot::Sender<SubmitOutcome>)>,
ctx: ConsumerContext,
) -> Result<(), CamelError> {
while let Some((exchange, reply_tx)) = exchange_rx.recv().await {
let outcome = match ctx.send(exchange).await {
Ok(()) => SubmitOutcome::Accepted,
Err(_) => SubmitOutcome::Stopped,
};
let _ = reply_tx.send(outcome);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_channels_new() {
let channels = SourceChannels::new();
assert!(!channels.request_tx.is_closed());
assert!(!channels.exchange_tx.is_closed());
}
#[test]
fn test_source_channels_default() {
let channels = SourceChannels::default();
assert!(!channels.request_tx.is_closed());
}
#[test]
fn test_request_channel_close_returns_none() {
let (tx, mut rx) = mpsc::channel::<RequestChannelItem>(1);
drop(tx);
let result = std::thread::spawn(move || rx.blocking_recv())
.join()
.unwrap();
assert!(result.is_none());
}
#[test]
fn test_exchange_channel_close_detected() {
let (tx, rx) = mpsc::channel::<(Exchange, oneshot::Sender<SubmitOutcome>)>(1);
drop(rx);
assert!(tx.is_closed());
}
#[test]
fn test_cancel_token_is_cancelled() {
let token = CancellationToken::new();
assert!(!token.is_cancelled());
token.cancel();
assert!(token.is_cancelled());
}
#[test]
fn test_submit_outcome_variants() {
let accepted = SubmitOutcome::Accepted;
let stopped = SubmitOutcome::Stopped;
assert!(matches!(accepted, SubmitOutcome::Accepted));
assert!(matches!(stopped, SubmitOutcome::Stopped));
}
#[test]
fn source_exchange_to_native_maps_fields() {
use crate::source_bindings::camel::plugin::types as src;
let wasm = src::WasmExchange {
input: src::WasmMessage {
headers: vec![("key".to_string(), "val".to_string())],
body: src::WasmBody::Text("hello".to_string()),
},
output: None,
properties: vec![("p".to_string(), "v".to_string())],
pattern: src::WasmPattern::InOnly,
correlation_id: "corr-1".to_string(),
route_id: Some("route-1".to_string()),
message_id: Some("msg-1".to_string()),
};
let native = source_exchange_to_native(wasm);
assert_eq!(native.input.headers.len(), 1);
assert_eq!(
native.input.headers.get("key"),
Some(&camel_api::Value::String("val".to_string()))
);
assert!(matches!(native.input.body, Body::Text(_)));
assert_eq!(
native.properties.get("p"),
Some(&camel_api::Value::String("v".to_string()))
);
assert!(matches!(native.pattern, ExchangePattern::InOnly));
assert_eq!(native.correlation_id, "corr-1");
}
}