use std::{
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use async_nats::{
RequestError, StatusCode,
client::traits::{Publisher, Subscriber, TimeoutProvider},
subject::ToSubject,
};
use bytes::Bytes;
use futures::{FutureExt, Stream, StreamExt};
pub trait RequestManyExt: TimeoutProvider + Publisher + Subscriber + Clone {
fn request_many(&self) -> RequestMany<Self>;
}
impl<T> RequestManyExt for T
where
T: TimeoutProvider + Publisher + Subscriber + Clone,
{
fn request_many(&self) -> RequestMany<T> {
RequestMany::new(self.clone(), self.timeout())
}
}
type SentinelPredicate = Option<Box<dyn Fn(&async_nats::Message) -> bool + 'static>>;
pub struct RequestMany<T>
where
T: TimeoutProvider + Publisher + Subscriber,
{
client: T,
sentinel: SentinelPredicate,
max_wait: Option<Duration>,
stall_wait: Option<Duration>,
max_messags: Option<usize>,
}
impl<T> RequestMany<T>
where
T: TimeoutProvider + Publisher + Subscriber + Clone,
{
pub fn new(client: T, max_wait: Option<Duration>) -> Self {
RequestMany {
client,
sentinel: None,
max_wait,
stall_wait: None,
max_messags: None,
}
}
pub fn sentinel(mut self, sentinel: impl Fn(&async_nats::Message) -> bool + 'static) -> Self {
self.sentinel = Some(Box::new(sentinel));
self
}
pub fn stall_wait(mut self, stall_wait: Duration) -> Self {
self.stall_wait = Some(stall_wait);
self
}
pub fn max_messages(mut self, max_messages: usize) -> Self {
self.max_messags = Some(max_messages);
self
}
pub fn max_wait(mut self, max_wait: Option<Duration>) -> Self {
self.max_wait = max_wait;
self
}
pub async fn send<S: ToSubject>(
self,
subject: S,
payload: Bytes,
) -> Result<Responses<async_nats::Subscriber>, RequestError> {
let response_subject = nuid::next().to_string();
let responses = self.client.subscribe(response_subject.clone()).await?;
self.client
.publish_with_reply(subject, response_subject, payload)
.await?;
let timer = self
.max_wait
.map(|max_wait| Box::pin(tokio::time::sleep(max_wait)));
Ok(Responses {
timer,
stall: None,
responses,
messages_received: 0,
sentinel: self.sentinel,
max_messages: self.max_messags,
stall_wait: self.stall_wait,
reason: None,
})
}
}
pub struct Responses<S>
where
S: Stream<Item = async_nats::Message> + Unpin,
{
responses: S,
messages_received: usize,
timer: Option<Pin<Box<tokio::time::Sleep>>>,
stall: Option<Pin<Box<tokio::time::Sleep>>>,
sentinel: SentinelPredicate,
max_messages: Option<usize>,
stall_wait: Option<Duration>,
reason: Option<TerminationReason>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TerminationReason {
MaxMessages,
MaxWait,
StallWait,
Sentinel,
NoResponders,
SubscriptionClosed,
}
impl<S> Responses<S>
where
S: Stream<Item = async_nats::Message> + Unpin,
{
pub fn termination_reason(&self) -> Option<TerminationReason> {
self.reason.clone()
}
}
impl<S> Stream for Responses<S>
where
S: Stream<Item = async_nats::Message> + Unpin,
{
type Item = async_nats::Message;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Some(timer) = self.timer.as_mut() {
match timer.poll_unpin(cx) {
Poll::Ready(_) => {
self.reason = Some(TerminationReason::MaxWait);
return Poll::Ready(None);
}
Poll::Pending => {}
}
}
if let Some(max_messages) = self.max_messages
&& self.messages_received >= max_messages
{
self.reason = Some(TerminationReason::MaxMessages);
return Poll::Ready(None);
}
if let Some(stall) = self.stall_wait {
let stall = self
.stall
.get_or_insert_with(|| Box::pin(tokio::time::sleep(stall)));
match stall.as_mut().poll_unpin(cx) {
Poll::Ready(_) => {
self.reason = Some(TerminationReason::StallWait);
return Poll::Ready(None);
}
Poll::Pending => {}
}
}
match self.responses.poll_next_unpin(cx) {
Poll::Ready(message) => match message {
Some(message) => {
if message.status == Some(StatusCode::NO_RESPONDERS) {
self.reason = Some(TerminationReason::NoResponders);
return Poll::Ready(None);
}
self.messages_received += 1;
self.stall = None;
match self.sentinel {
Some(ref sentinel) => {
if sentinel(&message) {
self.reason = Some(TerminationReason::Sentinel);
Poll::Ready(None)
} else {
Poll::Ready(Some(message))
}
}
None => Poll::Ready(Some(message)),
}
}
None => {
self.reason = Some(TerminationReason::SubscriptionClosed);
Poll::Ready(None)
}
},
Poll::Pending => Poll::Pending,
}
}
}