gitdataai/libs/transport/bus.rs
ZhenYi 14f6e1e500 feat(core): initialize project with access control and AI integration
- Add gitignore and prettier configuration files for project scaffolding
- Implement room access control service with project member verification
- Create user access key management with CRUD operations and activity logging
- Add accordion UI component for frontend expandable sections
- Implement room AI configuration with list, upsert, and delete operations
- Add AI event types for agent join/leave/status change tracking
- Create streaming AI processing services for mode and react patterns
- Build room AI service with model detection and idempotency handling
- Integrate chat service orchestration for AI message processing
- Add typing indicators and stream cancellation for AI interactions
- Implement mention parsing and context extraction for AI agents
2026-05-03 06:04:31 +08:00

205 lines
7.2 KiB
Rust

use async_nats::jetstream;
use config::AppConfig;
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use crate::error::AppTransportError;
pub trait Transport: Send + Sync {
fn publish(
&self,
subject: &str,
payload: &[u8],
) -> impl std::future::Future<Output = Result<(), AppTransportError>> + Send;
fn subscribe(
&self,
subject: &str,
) -> impl std::future::Future<Output = mpsc::Receiver<Vec<u8>>> + Send;
}
pub struct NatsTransport {
jetstream: jetstream::Context,
stream_name: String,
}
impl NatsTransport {
pub async fn connect(config: &AppConfig) -> Result<Self, AppTransportError> {
let url = config
.nats_url()
.ok_or_else(|| AppTransportError::Internal)?;
let token = config
.nats_token()
.ok_or_else(|| AppTransportError::Internal)?;
let opts = async_nats::ConnectOptions::with_token(token)
.retry_on_initial_connect()
.connection_timeout(std::time::Duration::from_secs(10))
.reconnect_delay_callback(|attempts| {
let base = std::time::Duration::from_secs(1);
let delay = base.saturating_mul(2u32.saturating_pow(attempts as u32));
std::cmp::min(delay, std::time::Duration::from_secs(30))
})
.event_callback(|event| async move {
match event {
async_nats::Event::Connected => debug!("NATS connected"),
async_nats::Event::Disconnected => warn!("NATS disconnected, reconnecting"),
async_nats::Event::ServerError(e) => warn!(error = %e, "NATS server error"),
_ => {}
}
});
let client = opts
.connect(&url)
.await
.map_err(|_e| AppTransportError::Internal)?;
let jetstream = jetstream::new(client);
let stream_config = jetstream::stream::Config {
name: config.nats_stream_name(),
subjects: vec!["room.events.>".to_string()],
retention: jetstream::stream::RetentionPolicy::WorkQueue,
max_age: std::time::Duration::from_secs(config.nats_max_age_secs()),
storage: jetstream::stream::StorageType::Memory,
..Default::default()
};
jetstream
.get_or_create_stream(stream_config)
.await
.map_err(|e| {
warn!(error = %e, "Failed to create JetStream stream");
AppTransportError::Internal
})?;
info!(stream = %config.nats_stream_name(), "JetStream stream ready");
Ok(Self {
jetstream,
stream_name: config.nats_stream_name(),
})
}
}
impl Transport for NatsTransport {
async fn publish(&self, subject: &str, payload: &[u8]) -> Result<(), AppTransportError> {
let ack = self
.jetstream
.publish(subject.to_string(), payload.to_vec().into())
.await
.map_err(|e| {
warn!(error = %e, subject = %subject, "NATS publish failed");
AppTransportError::Internal
})?;
ack.await.map_err(|e| {
warn!(error = %e, "NATS publish ack failed");
AppTransportError::Internal
})?;
Ok(())
}
fn subscribe(
&self,
subject: &str,
) -> impl std::future::Future<Output = mpsc::Receiver<Vec<u8>>> + Send {
let subject = subject.to_string();
let jetstream = self.jetstream.clone();
let stream_name = self.stream_name.clone();
let buffer_size = 256;
async move {
let (tx, rx) = mpsc::channel(buffer_size);
let stream = match jetstream.get_stream(&stream_name).await {
Ok(s) => s,
Err(e) => {
warn!(error = %e, "Failed to get stream for subscription");
return rx;
}
};
let consumer_name = subject
.replace(['.', '>'], "-")
.trim_end_matches('-')
.to_string();
let durable = if consumer_name.is_empty() {
"room-events-default".to_string()
} else {
format!("room-events-sub-{}", consumer_name)
};
let config = async_nats::jetstream::consumer::pull::Config {
durable_name: Some(durable.clone()),
filter_subject: subject.clone(),
max_deliver: 3,
ack_wait: std::time::Duration::from_secs(10),
..Default::default()
};
let mut messages = match stream.get_or_create_consumer(&durable, config.clone()).await {
Ok(c) => match c.messages().await {
Ok(m) => m,
Err(e) => {
warn!(error = %e, "Failed to start consumer message stream");
return rx;
}
},
Err(e) => {
warn!(error = %e, "Failed to create subscriber consumer");
return rx;
}
};
tokio::spawn(async move {
use futures_util::StreamExt;
loop {
while let Some(result) = messages.next().await {
match result {
Ok(msg) => {
if tx.send(msg.payload.to_vec()).await.is_err() {
debug!("NATS subscriber channel closed");
return;
}
let _ = msg.ack().await;
}
Err(e) => warn!(error = %e, "NATS consumer message error"),
}
}
warn!(subject = %subject, "NATS consumer stream ended, reconnecting");
let mut delay = std::time::Duration::from_secs(1);
let max_delay = std::time::Duration::from_secs(30);
loop {
tokio::time::sleep(delay).await;
match stream
.get_or_create_consumer(&durable, config.clone())
.await
{
Ok(new_consumer) => {
match new_consumer.messages().await {
Ok(new_messages) => {
info!(subject = %subject, "NATS consumer reconnected");
messages = new_messages;
break;
}
Err(_) => {}
}
}
Err(_) => {}
}
warn!(error = ?delay, "Reconnect failed, retrying");
delay = std::cmp::min(delay.saturating_mul(2), max_delay);
}
}
});
rx
}
}
}