Skip to main content

ruststream_nats/
context.rs

1//! Native `JetStream` metadata of a delivery, read by a handler through `Ctx<K>`.
2//!
3//! A handler binds one field as a parameter with the core's `Ctx<K>` extractor and a key from
4//! [`keys`], or types its context parameter as `Context<'_, JetStreamContext>`.
5//!
6//! - [JetStreamContext][context]: [`JetStreamContext`], the stream, the consumer, the stream and
7//!   consumer sequences, the delivery count and the pending count of a delivery, under the keys
8//!   [`keys::Stream`], [`keys::Consumer`], [`keys::StreamSequence`], [`keys::ConsumerSequence`],
9//!   [`keys::Delivered`] and [`keys::Pending`]. Every key reads `None` on a Core NATS delivery.
10//! - [Expectations][expectations]: [`keys::StreamSequence`] is the position a write to the
11//!   subject a handler read can expect.
12//!
13//! [context]: https://powersemmi.github.io/ruststream-nats/latest/components/context/
14//! [expectations]: https://powersemmi.github.io/ruststream-nats/latest/components/publishing/jetstream-publish/expectations/
15
16use ruststream::BuildContext;
17
18use crate::message::NatsMessage;
19
20/// Native `JetStream` per-delivery metadata, built once per delivery from the broker message.
21///
22/// Read its fields by the compile-time [`keys`] (for example
23/// [`keys::STREAM_SEQUENCE`]). On a core (non-JetStream) delivery there is no such metadata, so the
24/// context is empty and every key reads `None`; the same handler therefore works on both kinds of
25/// subscription.
26///
27/// The context owns its fields (it copies them out of the delivery), so it does not borrow the
28/// message: numbers are a stack copy and the stream/consumer names are cloned once per delivery.
29///
30/// # Examples
31///
32/// ```
33/// # mod demo {
34/// use ruststream::schemars::JsonSchema;
35/// use ruststream_nats::context::{JetStreamContext, keys};
36/// use ruststream_nats::prelude::*;
37/// use serde::Deserialize;
38///
39/// #[derive(Deserialize, JsonSchema)]
40/// # #[schemars(crate = "ruststream::schemars")]
41/// struct Order {
42///     id: u64,
43/// }
44///
45/// struct Ledger;
46///
47/// impl Handle<Order, (), (), JetStreamContext> for Ledger {
48///     async fn handle(
49///         &self,
50///         order: &Order,
51///         _outs: &(),
52///         ctx: &mut Context<'_, JetStreamContext>,
53///     ) -> Result<(), HandlerOutcome> {
54///         if let Some(sequence) = ctx.context(keys::STREAM_SEQUENCE) {
55///             tracing::info!(order.id, sequence, "entered in the ledger");
56///         }
57///         Ok(())
58///     }
59/// }
60///
61/// #[ruststream::app]
62/// fn app() -> impl App {
63///     RustStream::new(AppInfo::new("ledger", "0.1.0")).with_broker(
64///         NatsBroker::new("nats://localhost:4222"),
65///         |b| {
66///             b.include(subscriber(
67///                 JetStreamSubject::new("orders.*", "ORDERS").durable("ledger"),
68///                 Ledger,
69///             )
70///             .build());
71///         },
72///     )
73/// }
74/// # }
75/// # fn main() {}
76/// ```
77#[derive(Debug, Clone, Default, PartialEq, Eq)]
78pub struct JetStreamContext {
79    /// `None` for a core delivery, or when the `JetStream` reply subject could not be parsed.
80    info: Option<JetStreamInfo>,
81}
82
83/// The owned snapshot of one `JetStream` delivery's native metadata.
84#[derive(Debug, Clone, PartialEq, Eq)]
85struct JetStreamInfo {
86    stream: String,
87    consumer: String,
88    stream_sequence: u64,
89    consumer_sequence: u64,
90    delivered: i64,
91    pending: u64,
92}
93
94impl BuildContext<NatsMessage> for JetStreamContext {
95    fn build(msg: &NatsMessage) -> Self {
96        let info = match msg {
97            NatsMessage::JetStream(m) => m.metadata().map(|i| JetStreamInfo {
98                stream: i.stream.to_owned(),
99                consumer: i.consumer.to_owned(),
100                stream_sequence: i.stream_sequence,
101                consumer_sequence: i.consumer_sequence,
102                delivered: i.delivered,
103                pending: i.pending,
104            }),
105            NatsMessage::Core(_) => None,
106        };
107        Self { info }
108    }
109}
110
111// Compile-time guarantee that the context is buildable from the subscriber's actual delivery type,
112// so a handler declaring `Context<'_, JetStreamContext>` satisfies the runtime's
113// `Cx: BuildContext<S::Message>` mount bound.
114const _: fn() = || {
115    fn assert_build_context<C: BuildContext<M>, M>() {}
116    assert_build_context::<
117        JetStreamContext,
118        <crate::NatsSubscriber as ruststream::Subscriber>::Message,
119    >();
120};
121
122/// Compile-time [`Field`](ruststream::Field) keys, one per native `JetStream` metadatum.
123///
124/// Each key is a zero-sized selector exported both as a type (for naming in bounds) and as a
125/// `const` value (for use at the call site, `ctx.context(keys::STREAM_SEQUENCE)`). Every key reads
126/// `None` on a core delivery or when the `JetStream` reply subject could not be parsed.
127///
128/// The fields come from the `JetStream` acknowledgement subject, not from the payload or the
129/// headers, so these keys are the one way to reach them.
130///
131/// # Examples
132///
133/// ```
134/// # mod demo {
135/// use ruststream_nats::context::keys::{Delivered, StreamSequence};
136/// use ruststream_nats::prelude::*;
137/// use serde::Deserialize;
138///
139/// #[derive(Deserialize)]
140/// struct Order {
141///     id: u64,
142/// }
143///
144/// #[subscriber(JetStreamSubject::new("orders.*", "ORDERS").durable("worker"))]
145/// async fn handle(
146///     order: &Order,
147///     Ctx(sequence): Ctx<StreamSequence>,
148///     Ctx(delivered): Ctx<Delivered>,
149/// ) -> HandlerOutcome {
150///     // `None` on a core delivery; the stream sequence on a JetStream one.
151///     if let Some(sequence) = sequence {
152///         tracing::info!(order.id, sequence, "stored");
153///     }
154///     // The server-side delivery count tells a redelivery from a first delivery.
155///     if delivered.is_some_and(|n| n > 1) {
156///         tracing::warn!(order.id, "redelivered");
157///     }
158///     HandlerOutcome::ack()
159/// }
160///
161/// #[ruststream::app]
162/// fn app() -> impl App {
163///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
164///         NatsBroker::new("nats://localhost:4222"),
165///         |b| {
166///             b.include(handle);
167///         },
168///     )
169/// }
170/// # }
171/// # fn main() {}
172/// ```
173pub mod keys {
174    use ruststream::{ContextField, Field};
175
176    use super::JetStreamContext;
177
178    /// Selector for the stream sequence number; see [`STREAM_SEQUENCE`].
179    #[derive(Debug, Clone, Copy, Default)]
180    pub struct StreamSequence;
181
182    /// The monotonically increasing sequence the message holds within its stream.
183    pub const STREAM_SEQUENCE: StreamSequence = StreamSequence;
184
185    impl Field<JetStreamContext> for StreamSequence {
186        type Value<'a> = Option<u64>;
187        fn get(self, cx: &JetStreamContext) -> Option<u64> {
188            cx.info.as_ref().map(|i| i.stream_sequence)
189        }
190    }
191
192    impl ContextField for StreamSequence {
193        type Context = JetStreamContext;
194        type Value = Option<u64>;
195        fn read(self, cx: &JetStreamContext) -> Option<u64> {
196            cx.info.as_ref().map(|i| i.stream_sequence)
197        }
198    }
199
200    /// Selector for the consumer sequence number; see [`CONSUMER_SEQUENCE`].
201    #[derive(Debug, Clone, Copy, Default)]
202    pub struct ConsumerSequence;
203
204    /// The sequence the message holds within this consumer's delivery stream.
205    pub const CONSUMER_SEQUENCE: ConsumerSequence = ConsumerSequence;
206
207    impl Field<JetStreamContext> for ConsumerSequence {
208        type Value<'a> = Option<u64>;
209        fn get(self, cx: &JetStreamContext) -> Option<u64> {
210            cx.info.as_ref().map(|i| i.consumer_sequence)
211        }
212    }
213
214    impl ContextField for ConsumerSequence {
215        type Context = JetStreamContext;
216        type Value = Option<u64>;
217        fn read(self, cx: &JetStreamContext) -> Option<u64> {
218            cx.info.as_ref().map(|i| i.consumer_sequence)
219        }
220    }
221
222    /// Selector for the server-side delivery count; see [`DELIVERED`].
223    #[derive(Debug, Clone, Copy, Default)]
224    pub struct Delivered;
225
226    /// The number of times the server has delivered this message.
227    ///
228    /// `1` on first delivery, higher on a redelivery (after a `nack` or an `ack_wait` timeout).
229    /// This is the native redelivery count, not reconstructable from the payload or headers.
230    pub const DELIVERED: Delivered = Delivered;
231
232    impl Field<JetStreamContext> for Delivered {
233        type Value<'a> = Option<i64>;
234        fn get(self, cx: &JetStreamContext) -> Option<i64> {
235            cx.info.as_ref().map(|i| i.delivered)
236        }
237    }
238
239    impl ContextField for Delivered {
240        type Context = JetStreamContext;
241        type Value = Option<i64>;
242        fn read(self, cx: &JetStreamContext) -> Option<i64> {
243            cx.info.as_ref().map(|i| i.delivered)
244        }
245    }
246
247    /// Selector for the pending count; see [`PENDING`].
248    #[derive(Debug, Clone, Copy, Default)]
249    pub struct Pending;
250
251    /// The number of messages the server still has pending for this consumer behind this one.
252    pub const PENDING: Pending = Pending;
253
254    impl Field<JetStreamContext> for Pending {
255        type Value<'a> = Option<u64>;
256        fn get(self, cx: &JetStreamContext) -> Option<u64> {
257            cx.info.as_ref().map(|i| i.pending)
258        }
259    }
260
261    impl ContextField for Pending {
262        type Context = JetStreamContext;
263        type Value = Option<u64>;
264        fn read(self, cx: &JetStreamContext) -> Option<u64> {
265            cx.info.as_ref().map(|i| i.pending)
266        }
267    }
268
269    /// Selector for the stream name; see [`STREAM`].
270    #[derive(Debug, Clone, Copy, Default)]
271    pub struct Stream;
272
273    /// The name of the stream this message was delivered from.
274    pub const STREAM: Stream = Stream;
275
276    impl Field<JetStreamContext> for Stream {
277        type Value<'a> = Option<&'a str>;
278        fn get(self, cx: &JetStreamContext) -> Option<&str> {
279            cx.info.as_ref().map(|i| i.stream.as_str())
280        }
281    }
282
283    impl ContextField for Stream {
284        type Context = JetStreamContext;
285        type Value = Option<String>;
286        fn read(self, cx: &JetStreamContext) -> Option<String> {
287            cx.info.as_ref().map(|i| i.stream.clone())
288        }
289    }
290
291    /// Selector for the consumer name; see [`CONSUMER`].
292    #[derive(Debug, Clone, Copy, Default)]
293    pub struct Consumer;
294
295    /// The name of the durable (or ephemeral) consumer this message was delivered through.
296    pub const CONSUMER: Consumer = Consumer;
297
298    impl Field<JetStreamContext> for Consumer {
299        type Value<'a> = Option<&'a str>;
300        fn get(self, cx: &JetStreamContext) -> Option<&str> {
301            cx.info.as_ref().map(|i| i.consumer.as_str())
302        }
303    }
304
305    impl ContextField for Consumer {
306        type Context = JetStreamContext;
307        type Value = Option<String>;
308        fn read(self, cx: &JetStreamContext) -> Option<String> {
309            cx.info.as_ref().map(|i| i.consumer.clone())
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use ruststream::{BuildContext, Field};
317
318    use super::keys::{CONSUMER, CONSUMER_SEQUENCE, DELIVERED, PENDING, STREAM, STREAM_SEQUENCE};
319    use super::{JetStreamContext, JetStreamInfo};
320    use crate::message::{CoreMessage, NatsMessage};
321
322    fn populated() -> JetStreamContext {
323        JetStreamContext {
324            info: Some(JetStreamInfo {
325                stream: "ORDERS".to_owned(),
326                consumer: "orders-worker".to_owned(),
327                stream_sequence: 42,
328                consumer_sequence: 7,
329                delivered: 3,
330                pending: 5,
331            }),
332        }
333    }
334
335    #[test]
336    fn keys_read_populated_jetstream_fields() {
337        let cx = populated();
338        assert_eq!(STREAM_SEQUENCE.get(&cx), Some(42));
339        assert_eq!(CONSUMER_SEQUENCE.get(&cx), Some(7));
340        assert_eq!(DELIVERED.get(&cx), Some(3));
341        assert_eq!(PENDING.get(&cx), Some(5));
342        assert_eq!(STREAM.get(&cx), Some("ORDERS"));
343        assert_eq!(CONSUMER.get(&cx), Some("orders-worker"));
344    }
345
346    #[test]
347    fn context_field_keys_yield_owned_values() {
348        use ruststream::ContextField;
349
350        use super::keys::{Consumer, Delivered, Stream, StreamSequence};
351
352        let cx = populated();
353        assert_eq!(StreamSequence.read(&cx), Some(42));
354        assert_eq!(Delivered.read(&cx), Some(3));
355        assert_eq!(Stream.read(&cx), Some("ORDERS".to_owned()));
356        assert_eq!(Consumer.read(&cx), Some("orders-worker".to_owned()));
357    }
358
359    #[test]
360    fn keys_read_none_without_jetstream_info() {
361        let cx = JetStreamContext::default();
362        assert_eq!(STREAM_SEQUENCE.get(&cx), None);
363        assert_eq!(CONSUMER_SEQUENCE.get(&cx), None);
364        assert_eq!(DELIVERED.get(&cx), None);
365        assert_eq!(PENDING.get(&cx), None);
366        assert_eq!(STREAM.get(&cx), None);
367        assert_eq!(CONSUMER.get(&cx), None);
368    }
369
370    #[test]
371    fn build_over_core_message_has_no_info() {
372        // A core delivery is constructible in-process; a JetStream `async_nats::jetstream::Message`
373        // is not, so the JetStream build path is compile-validated and exercised against a real
374        // server only. Core must yield an empty context (no native JetStream metadata).
375        let msg = NatsMessage::Core(Box::new(CoreMessage::new(async_nats::Message {
376            subject: "orders.created".into(),
377            reply: None,
378            payload: bytes::Bytes::from_static(b"{}"),
379            headers: None,
380            status: None,
381            description: None,
382            length: 2,
383        })));
384        let cx = JetStreamContext::build(&msg);
385        assert_eq!(cx, JetStreamContext::default());
386        assert_eq!(STREAM_SEQUENCE.get(&cx), None);
387    }
388}