use async_trait::async_trait;
#[cfg(feature = "kafka")]
use super::types::DeliveredEvent;
use super::{SubscriptionError, transport::TransportAdapter, types::SubscriptionEvent};
#[derive(Debug, Clone)]
pub struct KafkaConfig {
pub endpoint: String,
pub default_topic: String,
pub client_id: String,
pub acks: String,
pub timeout_ms: u64,
pub compression: Option<String>,
}
impl KafkaConfig {
#[must_use]
pub fn new(endpoint: impl Into<String>, default_topic: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
default_topic: default_topic.into(),
client_id: "fraiseql".to_string(),
acks: "all".to_string(),
timeout_ms: 5_000,
compression: None,
}
}
#[must_use]
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = client_id.into();
self
}
#[must_use]
pub fn with_acks(mut self, acks: impl Into<String>) -> Self {
self.acks = acks.into();
self
}
#[must_use]
pub const fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.timeout_ms = timeout_ms;
self
}
#[must_use]
pub fn with_compression(mut self, compression: impl Into<String>) -> Self {
self.compression = Some(compression.into());
self
}
}
#[cfg(feature = "kafka")]
pub struct KafkaAdapter {
config: KafkaConfig,
egress: fraiseql_kafka::KafkaEgress,
}
#[cfg(feature = "kafka")]
impl std::fmt::Debug for KafkaAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KafkaAdapter")
.field("endpoint", &self.config.endpoint)
.field("default_topic", &self.config.default_topic)
.field("client_id", &self.config.client_id)
.finish_non_exhaustive()
}
}
#[cfg(feature = "kafka")]
impl KafkaAdapter {
pub fn new(config: KafkaConfig) -> Result<Self, SubscriptionError> {
use std::time::Duration;
let egress_config = fraiseql_kafka::KafkaEgressConfig::new(config.client_id.clone())
.with_timeouts(
Duration::from_millis(config.timeout_ms),
Duration::from_millis(config.timeout_ms.min(1_000)),
)
.with_compression(config.compression.clone());
let egress = fraiseql_kafka::KafkaEgress::connect(&config.endpoint, &egress_config)
.map_err(|error| {
SubscriptionError::Internal(format!("Failed to create Kafka producer: {error}"))
})?;
tracing::info!(
endpoint = %config.endpoint,
topic = %config.default_topic,
client_id = %config.client_id,
"KafkaAdapter connected through the shared Kafka egress"
);
Ok(Self { config, egress })
}
fn get_topic(&self, _subscription_name: &str) -> &str {
&self.config.default_topic
}
}
#[cfg(feature = "kafka")]
#[async_trait]
impl TransportAdapter for KafkaAdapter {
async fn deliver(
&self,
event: &SubscriptionEvent,
subscription_name: &str,
) -> Result<(), SubscriptionError> {
use fraiseql_kafka::{EgressOutcome, EgressRecord};
let message = DeliveredEvent::from_event(event, subscription_name);
let topic = self.get_topic(subscription_name);
let payload = serde_json::to_string(&message).map_err(|e| {
SubscriptionError::Internal(format!("Failed to serialize message: {e}"))
})?;
let outcome = self
.egress
.send(EgressRecord {
topic,
key: message.entity_id,
payload: payload.as_bytes(),
headers: &[("fraiseql-event-id", message.event_id)],
})
.await;
match outcome {
EgressOutcome::Delivered => {
tracing::debug!(
topic = topic,
key = message.entity_id,
event_id = %event.event_id,
"Kafka message delivered successfully"
);
Ok(())
},
EgressOutcome::Transient(reason) => {
tracing::error!(
topic = topic,
key = message.entity_id,
event_id = %event.event_id,
%reason,
"Kafka delivery failed; at-most-once, so this event is not retried"
);
Err(SubscriptionError::DeliveryFailed {
transport: "kafka".to_string(),
reason,
})
},
EgressOutcome::Permanent(reason) => {
tracing::error!(
topic = topic,
key = message.entity_id,
event_id = %event.event_id,
%reason,
"Kafka refused this message permanently; redelivery could not succeed"
);
Err(SubscriptionError::DeliveryFailed {
transport: "kafka".to_string(),
reason,
})
},
}
}
fn name(&self) -> &'static str {
"kafka"
}
async fn health_check(&self) -> bool {
self.egress.is_reachable(std::time::Duration::from_secs(5))
}
}
#[cfg(not(feature = "kafka"))]
#[derive(Debug)]
pub struct KafkaAdapter {
config: KafkaConfig,
}
#[cfg(not(feature = "kafka"))]
impl KafkaAdapter {
pub fn new(config: KafkaConfig) -> Result<Self, SubscriptionError> {
tracing::warn!(
endpoint = %config.endpoint,
topic = %config.default_topic,
"KafkaAdapter created (STUB - enable 'kafka' feature for real Kafka support)"
);
Ok(Self { config })
}
fn get_topic(&self, _subscription_name: &str) -> &str {
&self.config.default_topic
}
}
#[cfg(not(feature = "kafka"))]
#[async_trait]
impl TransportAdapter for KafkaAdapter {
async fn deliver(
&self,
event: &SubscriptionEvent,
subscription_name: &str,
) -> Result<(), SubscriptionError> {
let topic = self.get_topic(subscription_name);
Err(SubscriptionError::Internal(format!(
"Kafka transport is not compiled into this binary (event {event_id} for topic \
'{topic}' not delivered). Rebuild with `--features kafka`.",
event_id = event.event_id,
)))
}
fn name(&self) -> &'static str {
"kafka"
}
async fn health_check(&self) -> bool {
tracing::debug!("Kafka health check (STUB): kafka feature not compiled in — unhealthy");
false
}
}