Skip to main content

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///
452/// A service reaches one wherever a mount site names [`JetStreamPublish`]: a reply, an `Out` slot,
453/// or a startup hook that publishes outside any delivery.
454///
455/// # Examples
456///
457/// ```
458/// # mod demo {
459/// use std::io;
460///
461/// use ruststream::OutgoingMessage;
462/// use ruststream_nats::prelude::*;
463/// use serde::{Deserialize, Serialize};
464///
465/// #[derive(Deserialize)]
466/// struct Order {
467///     id: u64,
468/// }
469///
470/// #[derive(Serialize, Outgoing)]
471/// #[outgoing(name = "confirmations")]
472/// struct Confirmation {
473///     id: u64,
474/// }
475///
476/// #[derive(Serialize, Outgoing)]
477/// #[outgoing(name = "archive.orders")]
478/// struct Archived {
479///     id: u64,
480/// }
481///
482/// #[derive(OutSlot)]
483/// #[publishes(Archived)]
484/// struct Archive;
485///
486/// #[subscriber(JetStreamSubject::new("orders.*", "ORDERS").durable("confirmer"), reply)]
487/// async fn confirm(order: &Order) -> Confirmation {
488///     Confirmation { id: order.id }
489/// }
490///
491/// #[subscriber(JetStreamSubject::new("orders.*", "ORDERS").durable("archiver"))]
492/// async fn archive(
493///     order: &Order,
494///     Out(out): Out<impl Publisher<Options = JetStreamOptions>, Archive>,
495/// ) -> HandlerOutcome {
496///     let archived = Archived { id: order.id };
497///     if out
498///         .message(&archived)
499///         .message_id(format!("order-{}", order.id))
500///         .publish()
501///         .await
502///         .is_err()
503///     {
504///         return HandlerOutcome::retry();
505///     }
506///     HandlerOutcome::ack()
507/// }
508///
509/// #[ruststream::app]
510/// fn app() -> impl App {
511///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
512///         NatsBroker::new("nats://localhost:4222"),
513///         |b| {
514///             b.include(confirm)
515///                 .out_reply(JetStreamPublish::default().expect_stream("CONFIRMATIONS"));
516///
517///             b.include(archive)
518///                 .out(Archive, JetStreamPublish::default().expect_stream("ARCHIVE"))
519///                 .build();
520///
521///             b.after_startup(
522///                 JetStreamPublish::default().expect_stream("ORDERS"),
523///                 async move |publisher| -> io::Result<()> {
524///                     let ack = publisher
525///                         .publish_ack(OutgoingMessage::new("orders.created", br#"{"id":1}"#), None)
526///                         .await
527///                         .map_err(io::Error::other)?;
528///                     tracing::info!(stream = %ack.stream, ack.sequence, "seeded");
529///                     Ok(())
530///                 },
531///             );
532///         },
533///     )
534/// }
535/// # }
536/// # fn main() {}
537/// ```
538#[derive(Clone)]
539pub struct JetStreamPublisher {
540    link: JetStreamLink,
541    policy: JetStreamPublish,
542}
543
544impl Debug for JetStreamPublisher {
545    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
546        f.debug_struct("JetStreamPublisher")
547            .field("policy", &self.policy)
548            .finish_non_exhaustive()
549    }
550}
551
552impl JetStreamPublisher {
553    /// Publishes into the stream and returns the acknowledgement: the stream the message landed
554    /// in, its sequence there, and whether the deduplication window recognised it as a duplicate.
555    ///
556    /// [`Publisher::publish`] is this call with the acknowledgement discarded. `options` is what
557    /// this one message states about itself; pass `None` to publish on the policy alone.
558    ///
559    /// # Errors
560    ///
561    /// Returns [`NatsError::Closed`] when the broker has shut down, [`NatsError::Publish`] when
562    /// the message cannot be sent, and [`NatsError::JetStream`] when the stream rejects it (no
563    /// such stream, or an expectation did not hold).
564    ///
565    /// # Cancel safety
566    ///
567    /// Not cancel-safe: dropping the future after the message is on the wire abandons the
568    /// acknowledgement, leaving the publish in an indeterminate state.
569    pub async fn publish_ack(
570        &self,
571        msg: OutgoingMessage<'_, BytesMut>,
572        options: Option<&JetStreamOptions>,
573    ) -> Result<PublishAck, NatsError> {
574        let (connection, context) = match &self.link {
575            JetStreamLink::Nats {
576                connection,
577                context,
578            } => (connection, context),
579            #[cfg(feature = "testing")]
580            JetStreamLink::InProcess(bus) => return self.publish_in_process(bus, msg, options),
581        };
582        // Checked before the send: the context caches a client clone that would happily queue a
583        // publish into a drained connection.
584        connection.live_client(msg.name())?;
585
586        let (subject, payload, headers) = nats_parts(msg)?;
587        let mut message = PublishMessage::build().payload(payload);
588        // The application's headers go on first: `headers` replaces the map, and the protocol
589        // fields below are written into it.
590        if let Some(headers) = headers {
591            message = message.headers(headers);
592        }
593        message = self.policy.apply(message);
594        if let Some(options) = options {
595            message = options.apply(message);
596        }
597
598        context
599            .send_publish(subject, message)
600            .await
601            .map_err(|err| NatsError::Publish(Box::new(err)))?
602            .await
603            .map_err(|err| NatsError::JetStream(Box::new(err)))
604    }
605}
606
607#[cfg(feature = "testing")]
608impl JetStreamPublisher {
609    /// The in-process publish: the client's half is writing the protocol headers, and the
610    /// in-process stream reads them as a server does.
611    fn publish_in_process(
612        &self,
613        bus: &Bus,
614        msg: OutgoingMessage<'_, BytesMut>,
615        options: Option<&JetStreamOptions>,
616    ) -> Result<PublishAck, NatsError> {
617        let (subject, payload, mut headers) = msg.into_parts();
618        self.policy.write_headers(&mut headers);
619        if let Some(options) = options {
620            options.write_headers(&mut headers);
621        }
622        let stored = bus
623            .publish(
624                subject,
625                payload.freeze(),
626                &headers,
627                None,
628                PublishMode::JetStream,
629                Origin::Connection,
630            )?
631            .ok_or_else(|| NatsError::JetStream("no stream found for given subject".into()))?;
632        Ok(PublishAck {
633            stream: stored.stream,
634            sequence: stored.sequence,
635            duplicate: stored.duplicate,
636            ..PublishAck::default()
637        })
638    }
639}
640
641impl Publisher for JetStreamPublisher {
642    /// The same answer the Core publisher gives: a `JetStream` publish carries a `Bytes` the
643    /// client keeps.
644    type Payload = Take;
645
646    type Error = NatsError;
647    type Options = JetStreamOptions;
648
649    /// # Cancel safety
650    ///
651    /// Not cancel-safe; see [`publish_ack`](Self::publish_ack).
652    async fn publish(
653        &self,
654        msg: OutgoingMessage<'_, BytesMut>,
655        options: Option<&Self::Options>,
656    ) -> Result<(), Self::Error> {
657        self.publish_ack(msg, options).await.map(|_ack| ())
658    }
659}