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}