Skip to main content

ruststream_nats/
jetstream.rs

1//! `JetStream` publishing: the [`JetStreamPublish`] policy and its live [`JetStreamPublisher`].
2//!
3//! `JetStream` publishing is a different contract from Core NATS, not a mode of it: the server
4//! answers every publish with an acknowledgement, and the publisher may state what it expects the
5//! stream to look like. Both live on this pair, so the Core publisher keeps the fire-and-forget
6//! shape the transport actually has.
7
8use std::fmt::{Debug, Formatter};
9use std::sync::Arc;
10
11use async_nats::jetstream::Context;
12use async_nats::jetstream::message::PublishMessage;
13use bytes::Bytes;
14use ruststream::{OutgoingMessage, PairError, PublishPolicy, Publisher};
15
16use crate::broker::{ConnectedNatsBroker, NatsConnection};
17use crate::publisher::NatsPublishPolicy;
18use crate::{convert::headers_to_nats, error::NatsError};
19
20/// The acknowledgement a `JetStream` stream returns for an accepted publish.
21pub use async_nats::jetstream::publish::PublishAck;
22
23/// The `JetStream` publish policy: pure declaration, constructible anywhere.
24///
25/// Beyond naming the transport, the policy carries the stream expectations the server checks
26/// before accepting a message. They are per-publisher declarations: a publisher that states
27/// `expect_stream("ORDERS")` fails loudly if its subject is routed to another stream, instead of
28/// writing somewhere unintended. The sequence and message-id expectations serve a single writer
29/// enforcing an optimistic-concurrency chain.
30///
31/// # Examples
32///
33/// ```
34/// use ruststream_nats::JetStreamPublish;
35///
36/// let policy = JetStreamPublish::default().expect_stream("ORDERS");
37/// # let _ = policy;
38/// ```
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
40#[must_use]
41pub struct JetStreamPublish {
42    /// Every field is an expectation the server checks before accepting the publish.
43    stream: Option<String>,
44    last_sequence: Option<u64>,
45    last_subject_sequence: Option<u64>,
46    last_message_id: Option<String>,
47}
48
49impl JetStreamPublish {
50    /// Requires the subject to be served by the named stream. The server rejects the publish
51    /// otherwise, so a misrouted subject surfaces as an error rather than a silent write.
52    pub fn expect_stream(mut self, stream: impl Into<String>) -> Self {
53        self.stream = Some(stream.into());
54        self
55    }
56
57    /// Requires the stream's last sequence to be `sequence` at the moment of the publish.
58    pub const fn expect_last_sequence(mut self, sequence: u64) -> Self {
59        self.last_sequence = Some(sequence);
60        self
61    }
62
63    /// Requires the last sequence *on the published subject* to be `sequence`.
64    pub const fn expect_last_subject_sequence(mut self, sequence: u64) -> Self {
65        self.last_subject_sequence = Some(sequence);
66        self
67    }
68
69    /// Requires the stream's last message id (the `Nats-Msg-Id` of the previous publish) to be
70    /// `id`.
71    pub fn expect_last_message_id(mut self, id: impl Into<String>) -> Self {
72        self.last_message_id = Some(id.into());
73        self
74    }
75
76    /// Applies the declared expectations to one outgoing `JetStream` message.
77    fn apply(&self, mut message: PublishMessage) -> PublishMessage {
78        if let Some(stream) = &self.stream {
79            message = message.expected_stream(stream);
80        }
81        if let Some(sequence) = self.last_sequence {
82            message = message.expected_last_sequence(sequence);
83        }
84        if let Some(sequence) = self.last_subject_sequence {
85            message = message.expected_last_subject_sequence(sequence);
86        }
87        if let Some(id) = &self.last_message_id {
88            message = message.expected_last_message_id(id);
89        }
90        message
91    }
92}
93
94impl PublishPolicy<ConnectedNatsBroker> for JetStreamPublish {
95    type Live = JetStreamPublisher;
96
97    async fn pair(self, connected: &ConnectedNatsBroker) -> Result<Self::Live, PairError> {
98        Ok(self.bind(connected))
99    }
100}
101
102impl NatsPublishPolicy for JetStreamPublish {
103    fn bind(self, connected: &ConnectedNatsBroker) -> Self::Live {
104        JetStreamPublisher {
105            connection: Arc::clone(connected.connection()),
106            context: connected.jetstream(),
107            policy: self,
108        }
109    }
110}
111
112/// The live `JetStream` publisher. Cheap to clone.
113///
114/// Every publish waits for the stream's acknowledgement, so a rejected message (unknown stream,
115/// violated expectation, storage failure) is an error rather than a silent drop. Like the Core
116/// publisher it aliases the connection and may outlive it: after the broker shuts down every
117/// publish reports [`NatsError::Closed`].
118#[derive(Clone)]
119pub struct JetStreamPublisher {
120    connection: Arc<NatsConnection>,
121    context: Context,
122    policy: JetStreamPublish,
123}
124
125impl Debug for JetStreamPublisher {
126    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("JetStreamPublisher")
128            .field("policy", &self.policy)
129            .finish_non_exhaustive()
130    }
131}
132
133impl JetStreamPublisher {
134    /// Publishes into the stream and returns the acknowledgement: the stream the message landed
135    /// in, its sequence there, and whether the deduplication window recognised it as a duplicate.
136    ///
137    /// [`Publisher::publish`] is this call with the acknowledgement discarded.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`NatsError::Closed`] when the broker has shut down, [`NatsError::Publish`] when
142    /// the message cannot be sent, and [`NatsError::JetStream`] when the stream rejects it (no
143    /// such stream, or an expectation from the policy did not hold).
144    ///
145    /// # Cancel safety
146    ///
147    /// Not cancel-safe: dropping the future after the message is on the wire abandons the
148    /// acknowledgement, leaving the publish in an indeterminate state.
149    pub async fn publish_ack(&self, msg: OutgoingMessage<'_>) -> Result<PublishAck, NatsError> {
150        // Checked before the send: the context caches a client clone that would happily queue a
151        // publish into a drained connection.
152        self.connection.live_client(msg.name())?;
153
154        let mut message = PublishMessage::build().payload(Bytes::copy_from_slice(msg.payload()));
155        if let Some(headers) = headers_to_nats(msg.headers()) {
156            message = message.headers(headers);
157        }
158
159        self.context
160            .send_publish(msg.name().to_owned(), self.policy.apply(message))
161            .await
162            .map_err(|err| NatsError::Publish(Box::new(err)))?
163            .await
164            .map_err(|err| NatsError::JetStream(Box::new(err)))
165    }
166}
167
168impl Publisher for JetStreamPublisher {
169    type Error = NatsError;
170
171    /// # Cancel safety
172    ///
173    /// Not cancel-safe; see [`publish_ack`](Self::publish_ack).
174    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
175        self.publish_ack(msg).await.map(|_ack| ())
176    }
177}