ruststream_nats/jetstream.rs
1//! `JetStream` publishing: the [`JetStreamPublish`] policy, its [`JetStreamOptions`] per-message
2//! settings, and the live [`JetStreamPublisher`].
3//!
4//! `JetStream` publishing is a different contract from Core NATS, not a mode of it: the server
5//! answers every publish with an acknowledgement, and the message may state what it expects the
6//! stream to look like. Both live on this pair, so the Core publisher keeps the fire-and-forget
7//! shape the transport actually has.
8
9// Without the `testing` feature a publisher's link has one variant; see `broker.rs`.
10#![cfg_attr(
11 not(feature = "testing"),
12 allow(clippy::infallible_destructuring_match)
13)]
14
15use std::fmt::{Debug, Formatter};
16use std::future::{Future, ready};
17use std::sync::Arc;
18
19use async_nats::jetstream::Context;
20use async_nats::jetstream::message::PublishMessage;
21use bytes::BytesMut;
22#[cfg(feature = "testing")]
23use ruststream::HeaderMap;
24#[cfg(feature = "asyncapi")]
25use ruststream::asyncapi::{Binding, Bindings};
26use ruststream::runtime::{PublishBuilder, PublishSink};
27use ruststream::{OutgoingMessage, PairError, PublishPolicy, Publisher, Take};
28#[cfg(feature = "asyncapi")]
29use serde::Serialize;
30
31use crate::broker::{ConnectedNatsBroker, Link, NatsConnection};
32#[cfg(feature = "testing")]
33use crate::in_process::{Bus, Origin, PublishMode};
34use crate::publisher::NatsPublishPolicy;
35#[cfg(feature = "asyncapi")]
36use crate::subject::JETSTREAM_EXTENSION;
37use crate::{convert::nats_parts, error::NatsError};
38
39/// The acknowledgement a `JetStream` stream returns for an accepted publish.
40pub use async_nats::jetstream::publish::PublishAck;
41
42/// The `JetStream` protocol header names, as `async-nats` writes them on the wire.
43#[cfg(feature = "testing")]
44pub(crate) const MESSAGE_ID: &str = "Nats-Msg-Id";
45#[cfg(feature = "testing")]
46pub(crate) const EXPECTED_LAST_SEQUENCE: &str = "Nats-Expected-Last-Sequence";
47#[cfg(feature = "testing")]
48pub(crate) const EXPECTED_LAST_SUBJECT_SEQUENCE: &str = "Nats-Expected-Last-Subject-Sequence";
49#[cfg(feature = "testing")]
50pub(crate) const EXPECTED_LAST_MESSAGE_ID: &str = "Nats-Expected-Last-Msg-Id";
51#[cfg(feature = "testing")]
52pub(crate) const EXPECTED_STREAM: &str = "Nats-Expected-Stream";
53
54/// What one `JetStream` publish states about itself, over and above its subject and payload.
55///
56/// Every field is optional, and a call carries only what it set. These are protocol fields the
57/// server reads, not headers of the application: a deduplication id, and the three expectations
58/// that hold an optimistic-concurrency chain together.
59///
60/// A publish that violates an expectation is refused by the stream, and the publish returns
61/// [`NatsError::JetStream`]. That is the point of stating one: the writer learns that the stream
62/// moved under it instead of appending over someone else's message.
63///
64/// The fields are public because a test names the value the publish carried, as
65/// `tb.out::<Marker>().assert_called_once().with_options(&JetStreamOptions { .. })`.
66///
67/// # Examples
68///
69/// ```
70/// # #[cfg(all(feature = "testing"))]
71/// # mod demo {
72/// use std::error::Error;
73///
74/// use ruststream::testing::TestApp;
75/// use ruststream_nats::prelude::*;
76/// use serde::{Deserialize, Serialize};
77///
78/// #[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
79/// struct Order {
80/// id: u64,
81/// }
82///
83/// #[derive(Serialize, Outgoing)]
84/// #[outgoing(name = "archive.orders")]
85/// struct Archived {
86/// id: u64,
87/// }
88///
89/// #[derive(OutSlot)]
90/// #[publishes(Archived)]
91/// struct Archive;
92///
93/// #[subscriber("orders.created")]
94/// async fn archive(
95/// order: &Order,
96/// Out(out): Out<impl Publisher<Options = JetStreamOptions>, Archive>,
97/// ) -> HandlerOutcome {
98/// let sent = out
99/// .message(&Archived { id: order.id })
100/// .message_id(format!("order-{}", order.id))
101/// .publish()
102/// .await;
103/// if sent.is_err() {
104/// return HandlerOutcome::retry();
105/// }
106/// HandlerOutcome::ack()
107/// }
108///
109/// pub fn app() -> impl App {
110/// RustStream::new(AppInfo::new("archive", "0.1.0"))
111/// .with_broker(NatsBroker::new("nats://localhost:4222"), |b| {
112/// b.include(archive)
113/// .out(Archive, JetStreamPublish::default().expect_stream("ARCHIVE"))
114/// .build();
115/// })
116/// }
117///
118/// pub async fn archives_once_per_order() -> Result<(), Box<dyn Error>> {
119/// let tb = TestApp::start(app()).await?;
120///
121/// tb.broker::<NatsBroker>()
122/// .message(&Order { id: 7 })
123/// .to("orders.created")
124/// .publish()
125/// .await?;
126///
127/// tb.out::<Archive>()
128/// .assert_called_once()
129/// .with_options(&JetStreamOptions {
130/// message_id: Some("order-7".into()),
131/// ..JetStreamOptions::default()
132/// });
133/// tb.shutdown().await?;
134/// Ok(())
135/// }
136/// # }
137/// # #[cfg(all(feature = "testing"))]
138/// # #[tokio::main]
139/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
140/// # demo::archives_once_per_order().await
141/// # }
142/// # #[cfg(not(all(feature = "testing")))]
143/// # fn main() {}
144/// ```
145#[derive(Debug, Clone, Default, PartialEq, Eq)]
146pub struct JetStreamOptions {
147 /// The `Nats-Msg-Id` the stream's deduplication window recognises a repeat by.
148 pub message_id: Option<String>,
149 /// Requires the stream's last sequence to be this at the moment of the publish.
150 pub expect_last_sequence: Option<u64>,
151 /// Requires the last sequence *on the published subject* to be this.
152 pub expect_last_subject_sequence: Option<u64>,
153 /// Requires the stream's last `Nats-Msg-Id` to be this.
154 pub expect_last_message_id: Option<String>,
155}
156
157impl JetStreamOptions {
158 /// Writes the fields this value set as the `JetStream` protocol headers a server reads.
159 ///
160 /// The real publisher lets `async-nats` write them; the in-process transport has no such
161 /// builder, so it writes the same headers itself, and its stream reads them as a server
162 /// does.
163 #[cfg(feature = "testing")]
164 pub(crate) fn write_headers(&self, headers: &mut HeaderMap) {
165 if let Some(id) = &self.message_id {
166 headers.insert(MESSAGE_ID, id.clone());
167 }
168 if let Some(sequence) = self.expect_last_sequence {
169 headers.insert(EXPECTED_LAST_SEQUENCE, sequence.to_string());
170 }
171 if let Some(sequence) = self.expect_last_subject_sequence {
172 headers.insert(EXPECTED_LAST_SUBJECT_SEQUENCE, sequence.to_string());
173 }
174 if let Some(id) = &self.expect_last_message_id {
175 headers.insert(EXPECTED_LAST_MESSAGE_ID, id.clone());
176 }
177 }
178
179 /// Writes the fields this value set onto one outgoing `JetStream` message.
180 pub(crate) fn apply(&self, mut message: PublishMessage) -> PublishMessage {
181 if let Some(id) = &self.message_id {
182 message = message.message_id(id);
183 }
184 if let Some(sequence) = self.expect_last_sequence {
185 message = message.expected_last_sequence(sequence);
186 }
187 if let Some(sequence) = self.expect_last_subject_sequence {
188 message = message.expected_last_subject_sequence(sequence);
189 }
190 if let Some(id) = &self.expect_last_message_id {
191 message = message.expected_last_message_id(id);
192 }
193 message
194 }
195}
196
197/// The per-message `JetStream` settings, as steps on the publish builder.
198///
199/// A handler body that adjusts one of these names this crate's prelude and bounds its slot
200/// `Out<impl Publisher<Options = JetStreamOptions>, Marker>`. That body is tied to `JetStream`,
201/// and its signature says so; a body that publishes plainly stays on the framework prelude alone.
202///
203/// The steps sit on the builder rather than on a publisher of their own, so the publish they
204/// finish still travels through the mount site's entry, with the codec and the transforms that
205/// entry named.
206///
207/// # Examples
208///
209/// ```no_run
210/// use ruststream_nats::prelude::*;
211/// # use serde::Serialize;
212/// #[derive(Serialize, Outgoing)]
213/// struct Archived {
214/// id: u64,
215/// }
216///
217/// # async fn record(
218/// # archived: &Archived,
219/// # ledger: impl Publisher<Options = JetStreamOptions>,
220/// # ) -> Result<(), Box<dyn std::error::Error>> {
221/// ledger
222/// .message(archived)
223/// .to("orders.archived")
224/// .message_id(format!("order-{}", archived.id))
225/// .expect_last_subject_sequence(41)
226/// .publish()
227/// .await?;
228/// # Ok(())
229/// # }
230/// ```
231pub trait JetStreamPublishSteps {
232 /// Tags this message with a deduplication id, so a repeat inside the stream's deduplication
233 /// window is stored once.
234 #[must_use]
235 fn message_id(self, id: impl Into<String>) -> Self;
236
237 /// Requires the stream's last sequence to be `sequence`, or the stream refuses this message.
238 #[must_use]
239 fn expect_last_sequence(self, sequence: u64) -> Self;
240
241 /// Requires the last sequence on this message's own subject to be `sequence`.
242 #[must_use]
243 fn expect_last_subject_sequence(self, sequence: u64) -> Self;
244
245 /// Requires the stream's last `Nats-Msg-Id` to be `id`.
246 #[must_use]
247 fn expect_last_message_id(self, id: impl Into<String>) -> Self;
248}
249
250/// The bound on the sink's options type is what keeps these steps off a builder over any other
251/// publisher, this crate's Core NATS one included.
252impl<Sink, Body, Enc, Hdrs, Dest> JetStreamPublishSteps
253 for PublishBuilder<Sink, Body, Enc, Hdrs, Dest>
254where
255 Sink: PublishSink<Options = JetStreamOptions>,
256{
257 fn message_id(mut self, id: impl Into<String>) -> Self {
258 self.options_mut()
259 .get_or_insert_with(JetStreamOptions::default)
260 .message_id = Some(id.into());
261 self
262 }
263
264 fn expect_last_sequence(mut self, sequence: u64) -> Self {
265 self.options_mut()
266 .get_or_insert_with(JetStreamOptions::default)
267 .expect_last_sequence = Some(sequence);
268 self
269 }
270
271 fn expect_last_subject_sequence(mut self, sequence: u64) -> Self {
272 self.options_mut()
273 .get_or_insert_with(JetStreamOptions::default)
274 .expect_last_subject_sequence = Some(sequence);
275 self
276 }
277
278 fn expect_last_message_id(mut self, id: impl Into<String>) -> Self {
279 self.options_mut()
280 .get_or_insert_with(JetStreamOptions::default)
281 .expect_last_message_id = Some(id.into());
282 self
283 }
284}
285
286/// The `JetStream` publish policy: pure declaration, constructible anywhere.
287///
288/// It carries what belongs to the publisher rather than to a message, which on `JetStream` is one
289/// thing: the stream the subject must be served by. A publisher that states
290/// `expect_stream("ORDERS")` fails loudly if its subject is routed elsewhere, instead of writing
291/// somewhere unintended.
292///
293/// The sequence and message-id expectations are not here. They belong to one message - an expected
294/// last sequence fixed for the life of a publisher can hold for at most one publish - so they are
295/// [`JetStreamOptions`] fields, set by the [`JetStreamPublishSteps`] steps at the call site.
296///
297/// # Examples
298///
299/// ```
300/// # mod demo {
301/// use ruststream_nats::prelude::*;
302/// use serde::{Deserialize, Serialize};
303///
304/// #[derive(Deserialize)]
305/// struct Payment {
306/// order: u64,
307/// }
308///
309/// #[derive(Serialize, Outgoing)]
310/// #[outgoing(name = "orders.paid")]
311/// struct Paid {
312/// order: u64,
313/// }
314///
315/// #[subscriber("payments.settled", reply)]
316/// async fn record(payment: &Payment) -> Paid {
317/// Paid { order: payment.order }
318/// }
319///
320/// #[ruststream::app]
321/// fn app() -> impl App {
322/// RustStream::new(AppInfo::new("payments", "0.1.0")).with_broker(
323/// NatsBroker::new("nats://localhost:4222"),
324/// |b| {
325/// // The reply is stored, and a subject the ORDERS stream does not serve is an error.
326/// b.include(record)
327/// .out_reply(JetStreamPublish::default().expect_stream("ORDERS"));
328/// },
329/// )
330/// }
331/// # }
332/// # fn main() {}
333/// ```
334#[derive(Debug, Clone, Default, PartialEq, Eq)]
335#[must_use]
336pub struct JetStreamPublish {
337 stream: Option<String>,
338}
339
340impl JetStreamPublish {
341 /// Requires the subject to be served by the named stream. The server rejects the publish
342 /// otherwise, so a misrouted subject surfaces as an error rather than a silent write.
343 pub fn expect_stream(mut self, stream: impl Into<String>) -> Self {
344 self.stream = Some(stream.into());
345 self
346 }
347
348 /// Applies the publisher's own declaration to one outgoing `JetStream` message.
349 fn apply(&self, message: PublishMessage) -> PublishMessage {
350 match &self.stream {
351 Some(stream) => message.expected_stream(stream),
352 None => message,
353 }
354 }
355
356 /// The same declaration as the protocol header a server reads. See
357 /// [`JetStreamOptions::write_headers`].
358 #[cfg(feature = "testing")]
359 pub(crate) fn write_headers(&self, headers: &mut HeaderMap) {
360 if let Some(stream) = &self.stream {
361 headers.insert(EXPECTED_STREAM, stream.clone());
362 }
363 }
364}
365
366impl PublishPolicy<ConnectedNatsBroker> for JetStreamPublish {
367 type Live = JetStreamPublisher;
368
369 fn pair(
370 self,
371 connected: &ConnectedNatsBroker,
372 ) -> impl Future<Output = Result<Self::Live, PairError>> {
373 ready(Ok(self.bind(connected)))
374 }
375
376 /// The stream this publisher requires, and the subject it requires that stream to serve.
377 ///
378 /// The `nats` binding has no field for either, so both travel under this crate's extension
379 /// key. `channel` is the destination the mount site resolved, and naming it next to the
380 /// stream is what makes the requirement readable: a reader sees which subject the stream has
381 /// to carry, including where a naming transform decides the address per delivery and the
382 /// channel reports none. A publisher that requires no stream has no requirement to state and
383 /// contributes nothing.
384 ///
385 /// There is no reply address here: on a `JetStream` delivery the protocol's reply field is the
386 /// acknowledgement inbox, not a request's, so a request is answered over Core NATS and
387 /// [`NatsPublish`](crate::NatsPublish) is what reports it.
388 #[cfg(feature = "asyncapi")]
389 fn channel_bindings(&self, channel: &str) -> Bindings {
390 self.stream
391 .as_deref()
392 .map(|stream| JetStreamPublishChannel {
393 expected_stream: stream,
394 subject: channel,
395 })
396 .and_then(|body| Binding::extension(JETSTREAM_EXTENSION, &body).ok())
397 .map(|binding| Bindings::new().with(binding))
398 .unwrap_or_default()
399 }
400}
401
402/// The stream a publisher requires and the subject it must serve, under this crate's extension
403/// key.
404#[cfg(feature = "asyncapi")]
405#[derive(Serialize)]
406#[serde(rename_all = "camelCase")]
407struct JetStreamPublishChannel<'a> {
408 expected_stream: &'a str,
409 subject: &'a str,
410}
411
412impl NatsPublishPolicy for JetStreamPublish {
413 fn bind(self, connected: &ConnectedNatsBroker) -> Self::Live {
414 let link = match connected.link() {
415 Link::Nats(connection) => JetStreamLink::Nats {
416 connection: Arc::clone(connection),
417 context: connection.jetstream(),
418 },
419 #[cfg(feature = "testing")]
420 Link::InProcess(bus) => JetStreamLink::InProcess(Arc::clone(bus)),
421 };
422 JetStreamPublisher { link, policy: self }
423 }
424}
425
426/// What a `JetStream` publisher speaks over: the connection and its `JetStream` context, or, under
427/// the `testing` feature, the in-process transport.
428// The size difference exists only with the `testing` feature; boxing the production variant to
429// even it out would cost every production publisher an allocation.
430#[cfg_attr(feature = "testing", allow(clippy::large_enum_variant))]
431#[derive(Clone)]
432enum JetStreamLink {
433 Nats {
434 connection: Arc<NatsConnection>,
435 context: Context,
436 },
437 #[cfg(feature = "testing")]
438 InProcess(Arc<Bus>),
439}
440
441// A production build holds the connection and the context and nothing else.
442#[cfg(not(feature = "testing"))]
443const _: () = assert!(size_of::<JetStreamLink>() == size_of::<(Arc<NatsConnection>, Context)>());
444
445/// The live `JetStream` publisher. Cheap to clone.
446///
447/// Every publish waits for the stream's acknowledgement, so a rejected message (unknown stream,
448/// violated expectation, storage failure) is an error rather than a silent drop. Like the Core
449/// publisher it aliases the connection and may outlive it: after the broker shuts down every
450/// publish reports [`NatsError::Closed`].
451#[derive(Clone)]
452pub struct JetStreamPublisher {
453 link: JetStreamLink,
454 policy: JetStreamPublish,
455}
456
457impl Debug for JetStreamPublisher {
458 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
459 f.debug_struct("JetStreamPublisher")
460 .field("policy", &self.policy)
461 .finish_non_exhaustive()
462 }
463}
464
465impl JetStreamPublisher {
466 /// Publishes into the stream and returns the acknowledgement: the stream the message landed
467 /// in, its sequence there, and whether the deduplication window recognised it as a duplicate.
468 ///
469 /// [`Publisher::publish`] is this call with the acknowledgement discarded. `options` is what
470 /// this one message states about itself; pass `None` to publish on the policy alone.
471 ///
472 /// # Errors
473 ///
474 /// Returns [`NatsError::Closed`] when the broker has shut down, [`NatsError::Publish`] when
475 /// the message cannot be sent, and [`NatsError::JetStream`] when the stream rejects it (no
476 /// such stream, or an expectation did not hold).
477 ///
478 /// # Cancel safety
479 ///
480 /// Not cancel-safe: dropping the future after the message is on the wire abandons the
481 /// acknowledgement, leaving the publish in an indeterminate state.
482 pub async fn publish_ack(
483 &self,
484 msg: OutgoingMessage<'_, BytesMut>,
485 options: Option<&JetStreamOptions>,
486 ) -> Result<PublishAck, NatsError> {
487 let (connection, context) = match &self.link {
488 JetStreamLink::Nats {
489 connection,
490 context,
491 } => (connection, context),
492 #[cfg(feature = "testing")]
493 JetStreamLink::InProcess(bus) => return self.publish_in_process(bus, msg, options),
494 };
495 // Checked before the send: the context caches a client clone that would happily queue a
496 // publish into a drained connection.
497 connection.live_client(msg.name())?;
498
499 let (subject, payload, headers) = nats_parts(msg)?;
500 let mut message = PublishMessage::build().payload(payload);
501 // The application's headers go on first: `headers` replaces the map, and the protocol
502 // fields below are written into it.
503 if let Some(headers) = headers {
504 message = message.headers(headers);
505 }
506 message = self.policy.apply(message);
507 if let Some(options) = options {
508 message = options.apply(message);
509 }
510
511 context
512 .send_publish(subject, message)
513 .await
514 .map_err(|err| NatsError::Publish(Box::new(err)))?
515 .await
516 .map_err(|err| NatsError::JetStream(Box::new(err)))
517 }
518}
519
520#[cfg(feature = "testing")]
521impl JetStreamPublisher {
522 /// The in-process publish: the client's half is writing the protocol headers, and the
523 /// in-process stream reads them as a server does.
524 fn publish_in_process(
525 &self,
526 bus: &Bus,
527 msg: OutgoingMessage<'_, BytesMut>,
528 options: Option<&JetStreamOptions>,
529 ) -> Result<PublishAck, NatsError> {
530 let (subject, payload, mut headers) = msg.into_parts();
531 self.policy.write_headers(&mut headers);
532 if let Some(options) = options {
533 options.write_headers(&mut headers);
534 }
535 let stored = bus
536 .publish(
537 subject,
538 payload.freeze(),
539 &headers,
540 None,
541 PublishMode::JetStream,
542 Origin::Connection,
543 )?
544 .ok_or_else(|| NatsError::JetStream("no stream found for given subject".into()))?;
545 Ok(PublishAck {
546 stream: stored.stream,
547 sequence: stored.sequence,
548 duplicate: stored.duplicate,
549 ..PublishAck::default()
550 })
551 }
552}
553
554impl Publisher for JetStreamPublisher {
555 /// The same answer the Core publisher gives: a `JetStream` publish carries a `Bytes` the
556 /// client keeps.
557 type Payload = Take;
558
559 type Error = NatsError;
560 type Options = JetStreamOptions;
561
562 /// # Cancel safety
563 ///
564 /// Not cancel-safe; see [`publish_ack`](Self::publish_ack).
565 async fn publish(
566 &self,
567 msg: OutgoingMessage<'_, BytesMut>,
568 options: Option<&Self::Options>,
569 ) -> Result<(), Self::Error> {
570 self.publish_ack(msg, options).await.map(|_ack| ())
571 }
572}