arminius_lib_nats/
responder.rs1use std::pin::Pin;
9
10use arminius_lib_message_bus::{Command, Delivery, Error, ReceivedCommand, Responder};
11use async_nats::jetstream;
12use futures::{Stream, StreamExt as _};
13
14use crate::{
15 client::NatsClient,
16 requester::{__private, NatsReceivedCommand},
17 resubscribe::resubscribing_stream,
18};
19
20impl Responder for NatsClient {
21 #[expect(
28 clippy::manual_async_fn,
29 reason = "by default async fn returns a Future that is not Send"
30 )]
31 fn respond<C: Command + Send + 'static>(
32 self,
33 ) -> impl Future<
34 Output = Result<
35 impl Stream<Item = Result<impl ReceivedCommand<C>, Error>> + Send + 'static,
36 Error,
37 >,
38 > + Send
39 + 'static {
40 async move { self.respond_inner::<C>().await }
41 }
42}
43
44impl NatsClient {
45 async fn respond_inner<C: Command + Send + 'static>(
50 &self,
51 ) -> Result<
52 impl Stream<Item = Result<NatsReceivedCommand<C>, Error>> + Send + 'static + use<C>,
53 Error,
54 > {
55 let initial = self.make_command_stream::<C>().await?;
58 tracing::debug!(subject = C::SUBJECT, "Subscribed to command stream");
59
60 let client = self.clone();
61 Ok(resubscribing_stream(
62 self.signals.clone(),
63 self.stream_config.consumer.command_prefetch.get(),
64 C::SUBJECT,
65 initial,
66 move || {
67 let client = client.clone();
68 async move { client.make_command_stream::<C>().await }
69 },
70 ))
71 }
72
73 async fn make_command_stream<C: Command + 'static>(
78 &self,
79 ) -> Result<Pin<Box<dyn Stream<Item = Result<NatsReceivedCommand<C>, Error>> + Send>>, Error> {
80 let setup_err = |error: String| Error::SubscriptionFailed {
81 subject: C::SUBJECT_FILTER.to_owned(),
82 error,
83 };
84
85 let stream = self
89 .ensure_command_stream(C::SUBJECT_FILTER)
90 .await
91 .map_err(|error| setup_err(error.to_string()))?;
92
93 let command_slug = C::SUBJECT.replace(['.', '*', '>'], "_");
95 let consumer_name = format!("{}-{}", self.service_name, command_slug);
96
97 let mut consumer_config =
98 self.stream_config.consumer.pull_config(consumer_name, vec![C::SUBJECT.to_owned()]);
99
100 if C::DELIVERY == Delivery::AtMostOnce {
103 consumer_config.max_deliver = 1;
104 }
105
106 let consumer = stream
107 .create_consumer(consumer_config)
108 .await
109 .map_err(|error| setup_err(error.to_string()))?;
110
111 let message_stream = consumer
112 .stream()
113 .max_messages_per_batch(self.stream_config.consumer.command_prefetch.get())
114 .messages()
115 .await
116 .map_err(|error| setup_err(error.to_string()))?;
117 let client = self.client.clone();
118
119 Ok(Box::pin(
120 message_stream.filter_map(move |result| received_command::<C>(result, client.clone())),
121 ))
122 }
123}
124
125async fn received_command<C: Command + 'static>(
133 result: Result<jetstream::Message, impl std::fmt::Display>,
134 client: async_nats::Client,
135) -> Option<Result<NatsReceivedCommand<C>, Error>> {
136 let recv_err = |error: String| Error::ReceptionFailed { subject: C::SUBJECT.to_owned(), error };
137
138 let msg = match result {
139 Ok(msg) => msg,
140 Err(error) => return Some(Err(recv_err(error.to_string()))),
141 };
142 let context = __private::extract_otel_context(&msg);
143
144 let Some(return_subject) = crate::headers::reply_subject(msg.headers.as_ref()) else {
148 tracing::warn!(
149 subject = %msg.subject,
150 "Command message missing reply-subject header; skipping"
151 );
152 if let Err(error) = msg.ack().await {
153 tracing::warn!(%error, "Failed to ack skipped command message");
154 }
155 return None;
156 };
157
158 let cmd = match C::from_payload(msg.subject.as_str(), &msg.payload) {
159 Ok(cmd) => cmd,
160 Err(error) => return Some(Err(recv_err(error.to_string()))),
161 };
162 Some(Ok(NatsReceivedCommand::new(cmd, context, return_subject, msg, client)))
163}