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}