use std::sync::Arc;
use anyhow::{Context, Result};
use async_nats::Client;
use bytes::Bytes;
use flume::{Receiver, Sender};
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{FutureExt, StreamExt};
use tokio::sync::oneshot;
use tracing::error;
use super::{Message, Publisher, Subscriber, Subscription};
#[derive(Debug, Clone)]
pub struct NatsConfig {
pub server_url: String,
pub subject_prefix: Option<String>,
}
impl NatsConfig {
pub fn new(server_url: impl Into<String>) -> Self {
Self {
server_url: server_url.into(),
subject_prefix: None,
}
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.subject_prefix = Some(prefix.into());
self
}
pub async fn connect(&self) -> Result<Client> {
async_nats::connect(&self.server_url)
.await
.context("failed to connect to NATS server")
}
fn format_subject(&self, subject: &str) -> String {
match &self.subject_prefix {
Some(prefix) => format!("{}.{}", prefix, subject),
None => subject.to_string(),
}
}
}
enum PublishCommand {
Publish { subject: String, payload: Bytes },
Flush { done: oneshot::Sender<Result<()>> },
}
pub struct NatsPublisher {
tx: Sender<PublishCommand>,
config: Arc<NatsConfig>,
}
impl NatsPublisher {
pub fn new(client: Client, config: NatsConfig) -> Self {
let (tx, rx) = flume::unbounded();
let config = Arc::new(config);
tokio::spawn(Self::run_publish_loop(client, rx));
Self { tx, config }
}
pub async fn connect(config: NatsConfig) -> Result<Self> {
let client = config.connect().await?;
Ok(Self::new(client, config))
}
async fn run_publish_loop(client: Client, rx: Receiver<PublishCommand>) {
while let Ok(cmd) = rx.recv_async().await {
match cmd {
PublishCommand::Publish { subject, payload } => {
if let Err(e) = client.publish(subject, payload).await {
error!("failed to publish message: {e}");
}
}
PublishCommand::Flush { done } => {
let result = client.flush().await.context("failed to flush");
let _ = done.send(result);
}
}
}
}
}
impl Publisher for NatsPublisher {
fn publish(&self, subject: &str, payload: Bytes) -> Result<()> {
let subject = self.config.format_subject(subject);
self.tx
.send(PublishCommand::Publish { subject, payload })
.map_err(|_| anyhow::anyhow!("publisher task has terminated"))
}
fn flush(&self) -> BoxFuture<'static, Result<()>> {
let (done_tx, done_rx) = oneshot::channel();
let tx = self.tx.clone();
async move {
tx.send(PublishCommand::Flush { done: done_tx })
.map_err(|_| anyhow::anyhow!("publisher task has terminated"))?;
done_rx
.await
.map_err(|_| anyhow::anyhow!("publisher task has terminated"))?
}
.boxed()
}
}
pub struct NatsSubscriber {
client: Client,
config: NatsConfig,
}
impl NatsSubscriber {
pub fn new(client: Client, config: NatsConfig) -> Self {
Self { client, config }
}
pub async fn connect(config: NatsConfig) -> Result<Self> {
let client = config.connect().await?;
Ok(Self::new(client, config))
}
}
impl Subscriber for NatsSubscriber {
fn subscribe(&self, subject: &str) -> BoxFuture<'static, Result<Subscription>> {
let subject = self.config.format_subject(subject);
let client = self.client.clone();
async move {
let subscriber = client
.subscribe(subject)
.await
.context("failed to subscribe")?;
let stream: BoxStream<'static, Message> = subscriber
.map(|msg| Message {
subject: msg.subject.to_string(),
payload: msg.payload,
})
.boxed();
Ok(stream)
}
.boxed()
}
}