Skip to main content

ruststream_nats/
context.rs

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