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}