Skip to main content

netget/server/nats/
jetstream.rs

1//! JetStream on NetGet's NATS server, when started with `jetstream: true`.
2//!
3//! JetStream is an API carried over ordinary NATS request/reply: a client publishes JSON to
4//! `$JS.API.<operation>` with a reply inbox, publishes to a stream's subjects with a reply inbox
5//! and waits for a publish ack, pulls with `$JS.API.CONSUMER.MSG.NEXT.<stream>.<consumer>`, and
6//! acknowledges by publishing to the `$JS.ACK.…` reply subject each delivery carried.
7//!
8//! Rust owns the subjects, the response envelopes (each `type`, the defaults a client needs to
9//! decode one), the ack subjects, the end-of-batch status headers, and the table of which
10//! subjects belong to which stream — learned from the stream configs the handler accepted,
11//! which is routing, like the subscription table. **No message is stored here**: the handler
12//! keeps streams' contents (in its memory, or a script's file) and answers every pull.
13use super::actions::{check_token, decode_payload, encode_headers};
14use super::subject_matches;
15use crate::llm::actions::protocol_trait::ActionResult;
16use crate::llm::actions::{ActionDefinition, Parameter};
17use crate::protocol::log_template::LogTemplate;
18use crate::protocol::{Event, EventType};
19use anyhow::{ensure, Context, Result};
20use serde_json::{json, Map, Value};
21use std::collections::BTreeMap;
22use std::sync::{LazyLock, Mutex};
23
24/// Most messages one pull may deliver.
25pub const MAX_BATCH: u64 = 256;
26/// Most streams whose subjects the server routes.
27pub const MAX_STREAMS: usize = 1024;
28
29/// Server-wide JetStream state: which subjects each stream captures.
30#[derive(Default, Debug)]
31pub struct Shared {
32    streams: Mutex<BTreeMap<String, Vec<String>>>,
33}
34
35/// What a published subject means to JetStream.
36#[derive(Debug, Clone, PartialEq)]
37pub enum Kind {
38    /// `$JS.API.<operation…>`: the operation and the stream and consumer it names.
39    Api {
40        operation: String,
41        stream: Option<String>,
42        consumer: Option<String>,
43    },
44    /// A pull request.
45    Pull { stream: String, consumer: String },
46    /// An acknowledgement on a delivery's reply subject.
47    Ack {
48        stream: String,
49        consumer: String,
50        delivered: u64,
51        stream_seq: u64,
52        consumer_seq: u64,
53    },
54    /// A message on a subject a stream captures.
55    Publish { stream: String },
56}
57
58impl Shared {
59    pub fn classify(&self, subject: &str) -> Option<Kind> {
60        let tokens: Vec<&str> = subject.split('.').collect();
61        if let Some(rest) = subject.strip_prefix("$JS.API.") {
62            let t: Vec<&str> = rest.split('.').collect();
63            return Some(match t.as_slice() {
64                ["CONSUMER", "MSG", "NEXT", s, c] => Kind::Pull {
65                    stream: s.to_string(),
66                    consumer: c.to_string(),
67                },
68                ["INFO"] => api("INFO", None, None),
69                ["STREAM", op @ ("NAMES" | "LIST")] => api(&format!("STREAM.{op}"), None, None),
70                ["STREAM", op @ ("CREATE" | "UPDATE" | "INFO" | "DELETE" | "PURGE"), s] => {
71                    api(&format!("STREAM.{op}"), Some(s), None)
72                }
73                ["CONSUMER", op @ ("NAMES" | "LIST"), s] => {
74                    api(&format!("CONSUMER.{op}"), Some(s), None)
75                }
76                ["CONSUMER", "CREATE", s] => api("CONSUMER.CREATE", Some(s), None),
77                ["CONSUMER", "CREATE", s, c, ..] | ["CONSUMER", "DURABLE", "CREATE", s, c] => {
78                    api("CONSUMER.CREATE", Some(s), Some(c))
79                }
80                ["CONSUMER", op @ ("INFO" | "DELETE"), s, c] => {
81                    api(&format!("CONSUMER.{op}"), Some(s), Some(c))
82                }
83                _ => api(rest, None, None),
84            });
85        }
86        if tokens.len() == 9 && tokens[0] == "$JS" && tokens[1] == "ACK" {
87            let n = |i: usize| tokens[i].parse::<u64>().ok();
88            return Some(Kind::Ack {
89                stream: tokens[2].to_string(),
90                consumer: tokens[3].to_string(),
91                delivered: n(4)?,
92                stream_seq: n(5)?,
93                consumer_seq: n(6)?,
94            });
95        }
96        let streams = self.streams.lock().ok()?;
97        streams
98            .iter()
99            .find(|(_, subjects)| subjects.iter().any(|p| subject_matches(p, subject)))
100            .map(|(name, _)| Kind::Publish {
101                stream: name.clone(),
102            })
103    }
104
105    fn remember(&self, stream: &str, config: &Value) {
106        let subjects = config["subjects"]
107            .as_array()
108            .map(|a| {
109                a.iter()
110                    .filter_map(Value::as_str)
111                    .map(str::to_string)
112                    .collect::<Vec<_>>()
113            })
114            .filter(|s| !s.is_empty())
115            .unwrap_or_else(|| vec![stream.to_string()]);
116        if let Ok(mut streams) = self.streams.lock() {
117            if streams.len() < MAX_STREAMS || streams.contains_key(stream) {
118                streams.insert(stream.to_string(), subjects);
119            }
120        }
121    }
122
123    fn forget(&self, stream: &str) {
124        if let Ok(mut streams) = self.streams.lock() {
125            streams.remove(stream);
126        }
127    }
128}
129
130fn api(operation: &str, stream: Option<&&str>, consumer: Option<&&str>) -> Kind {
131    Kind::Api {
132        operation: operation.to_string(),
133        stream: stream.map(|s| s.to_string()),
134        consumer: consumer.map(|s| s.to_string()),
135    }
136}
137
138/// The `type` a response to `operation` carries.
139fn response_type(operation: &str) -> Option<&'static str> {
140    Some(match operation {
141        "INFO" => "io.nats.jetstream.api.v1.account_info_response",
142        "STREAM.CREATE" => "io.nats.jetstream.api.v1.stream_create_response",
143        "STREAM.UPDATE" => "io.nats.jetstream.api.v1.stream_update_response",
144        "STREAM.INFO" => "io.nats.jetstream.api.v1.stream_info_response",
145        "STREAM.DELETE" => "io.nats.jetstream.api.v1.stream_delete_response",
146        "STREAM.PURGE" => "io.nats.jetstream.api.v1.stream_purge_response",
147        "STREAM.NAMES" => "io.nats.jetstream.api.v1.stream_names_response",
148        "STREAM.LIST" => "io.nats.jetstream.api.v1.stream_list_response",
149        "CONSUMER.CREATE" => "io.nats.jetstream.api.v1.consumer_create_response",
150        "CONSUMER.INFO" => "io.nats.jetstream.api.v1.consumer_info_response",
151        "CONSUMER.DELETE" => "io.nats.jetstream.api.v1.consumer_delete_response",
152        "CONSUMER.NAMES" => "io.nats.jetstream.api.v1.consumer_names_response",
153        "CONSUMER.LIST" => "io.nats.jetstream.api.v1.consumer_list_response",
154        _ => return None,
155    })
156}
157
158const ZERO_TIME: &str = "0001-01-01T00:00:00Z";
159
160fn now() -> String {
161    chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, true)
162}
163
164fn stream_state() -> Value {
165    json!({"messages": 0, "bytes": 0, "first_seq": 0, "first_ts": ZERO_TIME,
166           "last_seq": 0, "last_ts": ZERO_TIME, "consumer_count": 0})
167}
168
169/// A stream's info with every field a client decodes present.
170fn stream_info(stream: &str, config: &Value, given: &Map<String, Value>) -> Value {
171    let mut config = given
172        .get("config")
173        .cloned()
174        .unwrap_or_else(|| config.clone());
175    if !config.is_object() {
176        config = json!({});
177    }
178    config["name"] = json!(stream);
179    for (k, v) in [
180        ("retention", json!("limits")),
181        ("storage", json!("file")),
182        ("discard", json!("old")),
183        ("num_replicas", json!(1)),
184        ("max_msgs", json!(-1)),
185        ("max_bytes", json!(-1)),
186        ("max_age", json!(0)),
187        ("max_consumers", json!(-1)),
188    ] {
189        if config.get(k).is_none_or(Value::is_null) {
190            config[k] = v;
191        }
192    }
193    if config.get("subjects").is_none_or(Value::is_null) {
194        config["subjects"] = json!([stream]);
195    }
196    let mut state = stream_state();
197    if let Some(Value::Object(s)) = given.get("state") {
198        for (k, v) in s {
199            state[k] = v.clone();
200        }
201    }
202    json!({"config": config, "created": given.get("created").cloned().unwrap_or_else(|| json!(now())), "state": state})
203}
204
205/// A consumer's info with every field a client decodes present.
206fn consumer_info(
207    stream: &str,
208    consumer: &str,
209    request: &Value,
210    given: &Map<String, Value>,
211) -> Value {
212    let mut config = given
213        .get("config")
214        .cloned()
215        .unwrap_or_else(|| request["config"].clone());
216    if !config.is_object() {
217        config = json!({});
218    }
219    for (k, v) in [
220        ("deliver_policy", json!("all")),
221        ("ack_policy", json!("explicit")),
222        ("replay_policy", json!("instant")),
223        ("ack_wait", json!(30_000_000_000u64)),
224        ("max_deliver", json!(-1)),
225        ("max_ack_pending", json!(1000)),
226        ("max_waiting", json!(512)),
227        ("num_replicas", json!(0)),
228    ] {
229        if config.get(k).is_none_or(Value::is_null) {
230            config[k] = v;
231        }
232    }
233    let mut out = json!({
234        "stream_name": stream, "name": consumer, "created": now(), "config": config,
235        "delivered": {"consumer_seq": 0, "stream_seq": 0},
236        "ack_floor": {"consumer_seq": 0, "stream_seq": 0},
237        "num_ack_pending": 0, "num_redelivered": 0, "num_waiting": 0, "num_pending": 0,
238    });
239    for (k, v) in given {
240        if k != "config" {
241            out[k] = v.clone();
242        }
243    }
244    out
245}
246
247/// The full API response for `operation`, from the handler's (possibly partial) `response`.
248fn build_response(
249    operation: &str,
250    stream: Option<&str>,
251    consumer: Option<&str>,
252    request: &Value,
253    given: &Map<String, Value>,
254) -> Value {
255    let stream = stream.unwrap_or_default();
256    let mut out = match operation {
257        "INFO" => json!({"memory": 0, "storage": 0, "reserved_memory": 0, "reserved_storage": 0,
258            "streams": 0, "consumers": 0,
259            "limits": {"max_memory": -1, "max_storage": -1, "max_streams": -1, "max_consumers": -1,
260                       "max_ack_pending": -1, "memory_max_stream_bytes": -1,
261                       "storage_max_stream_bytes": -1, "max_bytes_required": false},
262            "api": {"total": 0, "errors": 0}}),
263        "STREAM.CREATE" | "STREAM.UPDATE" | "STREAM.INFO" => stream_info(stream, request, given),
264        "STREAM.DELETE" | "CONSUMER.DELETE" => json!({"success": true}),
265        "STREAM.PURGE" => json!({"success": true, "purged": 0}),
266        "STREAM.NAMES" => json!({"total": 0, "offset": 0, "limit": 1024, "streams": []}),
267        "STREAM.LIST" => {
268            json!({"total": 0, "offset": 0, "limit": 256, "streams": [], "missing": []})
269        }
270        "CONSUMER.NAMES" => json!({"total": 0, "offset": 0, "limit": 1024, "consumers": []}),
271        "CONSUMER.LIST" => json!({"total": 0, "offset": 0, "limit": 256, "consumers": []}),
272        "CONSUMER.CREATE" | "CONSUMER.INFO" => {
273            let name = consumer
274                .map(str::to_string)
275                .or_else(|| request["config"]["name"].as_str().map(str::to_string))
276                .or_else(|| {
277                    request["config"]["durable_name"]
278                        .as_str()
279                        .map(str::to_string)
280                })
281                .unwrap_or_else(|| "ephemeral".into());
282            consumer_info(stream, &name, request, given)
283        }
284        _ => json!({}),
285    };
286    if !operation.starts_with("STREAM.C")
287        && !operation.starts_with("STREAM.U")
288        && operation != "STREAM.INFO"
289        && !operation.starts_with("CONSUMER.C")
290        && operation != "CONSUMER.INFO"
291    {
292        for (k, v) in given {
293            out[k] = v.clone();
294        }
295    }
296    if let Some(t) = response_type(operation) {
297        out["type"] = json!(t);
298    }
299    // Totals follow the lists the handler gave.
300    for key in ["streams", "consumers"] {
301        if let Some(n) = out[key].as_array().map(Vec::len) {
302            if operation.ends_with("NAMES") || operation.ends_with("LIST") {
303                out["total"] = json!(n);
304            }
305        }
306    }
307    out
308}
309
310fn error_response(operation: &str, code: u64, err_code: u64, description: &str) -> Value {
311    let mut out =
312        json!({"error": {"code": code, "err_code": err_code, "description": description}});
313    if let Some(t) = response_type(operation) {
314        out["type"] = json!(t);
315    }
316    out
317}
318
319/// One delivery to `inbox` on subscription `sid`, acknowledged on the subject it carries.
320#[allow(clippy::too_many_arguments)]
321fn delivery(
322    inbox: &str,
323    sid: &str,
324    stream: &str,
325    consumer: &str,
326    m: &Value,
327    consumer_seq: u64,
328    pending: u64,
329) -> Result<Vec<u8>> {
330    let stream_seq = m["stream_seq"]
331        .as_u64()
332        .filter(|n| *n > 0)
333        .context("each message needs a positive stream_seq")?;
334    let delivered = m["delivered"].as_u64().unwrap_or(1).max(1);
335    let consumer_seq = m["consumer_seq"].as_u64().unwrap_or(consumer_seq);
336    let ts = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
337    let ack = format!(
338        "$JS.ACK.{stream}.{consumer}.{delivered}.{stream_seq}.{consumer_seq}.{ts}.{pending}"
339    );
340    let subject = m["subject"]
341        .as_str()
342        .context("each message needs a subject")?;
343    check_token(subject, "subject")?;
344    let payload = decode_payload(m)?;
345    let headers = m["headers"].as_object().filter(|h| !h.is_empty());
346    let mut out = Vec::new();
347    // A delivery is addressed to the pull's inbox; its original subject travels as the
348    // subject the client reads back from the ack metadata and headers. NATS delivers JetStream
349    // messages on their own subject through the inbox subscription, so the MSG line names the
350    // stored subject and the inbox subscription's sid.
351    let _ = inbox;
352    match headers {
353        Some(h) => {
354            let block = encode_headers(h)?;
355            out.extend(
356                format!(
357                    "HMSG {subject} {sid} {ack} {} {}\r\n",
358                    block.len(),
359                    block.len() + payload.len()
360                )
361                .into_bytes(),
362            );
363            out.extend(block);
364        }
365        None => out.extend(format!("MSG {subject} {sid} {ack} {}\r\n", payload.len()).into_bytes()),
366    }
367    out.extend(payload);
368    out.extend(b"\r\n");
369    Ok(out)
370}
371
372/// A status-only message (`NATS/1.0 404 No Messages`), as the real server ends a pull.
373fn status(inbox: &str, sid: &str, code: u16, text: &str, pending: u64) -> Vec<u8> {
374    let block = if code == 408 {
375        format!("NATS/1.0 {code} {text}\r\nNats-Pending-Messages: {pending}\r\nNats-Pending-Bytes: 0\r\n\r\n")
376    } else {
377        format!("NATS/1.0 {code} {text}\r\n\r\n")
378    };
379    let mut out = format!("HMSG {inbox} {sid} {} {}\r\n", block.len(), block.len()).into_bytes();
380    out.extend(block.into_bytes());
381    out.extend(b"\r\n");
382    out
383}
384
385/// A plain reply to `inbox`.
386pub fn reply(inbox: &str, sid: &str, body: &[u8]) -> Vec<u8> {
387    let mut out = format!("MSG {inbox} {sid} {}\r\n", body.len()).into_bytes();
388    out.extend(body);
389    out.extend(b"\r\n");
390    out
391}
392
393// ---------------------------------------------------------------------------
394// Events and actions
395// ---------------------------------------------------------------------------
396
397fn p(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
398    Parameter {
399        name: name.into(),
400        type_hint: kind.into(),
401        description: description.into(),
402        required,
403    }
404}
405
406fn a(
407    name: &str,
408    description: &str,
409    parameters: Vec<Parameter>,
410    example: Value,
411    log: &str,
412) -> ActionDefinition {
413    ActionDefinition {
414        name: name.into(),
415        description: description.into(),
416        parameters,
417        example,
418        log_template: Some(LogTemplate::new().with_info(log)),
419    }
420}
421
422fn reply_action() -> ActionDefinition {
423    a(
424        "nats_js_reply",
425        "Answer a JetStream API request. Give only what you decide (e.g. {\"streams\": [\"ORDERS\"]} for STREAM.NAMES, {\"state\": {\"messages\": 3, \"last_seq\": 3}} for STREAM.INFO); the response type and every other field a client needs are filled in.",
426        vec![p("response", "object", "The response fields you decide", true)],
427        json!({"type":"nats_js_reply","response":{"streams":["ORDERS"]}}),
428        "-> JetStream reply {preview(response,100)}",
429    )
430}
431
432fn error_action() -> ActionDefinition {
433    a(
434        "nats_js_error",
435        "Refuse a JetStream request with an API error, e.g. 404/10059 stream not found, 404/10014 consumer not found.",
436        vec![
437            p("code", "number", "HTTP-like status: 400, 404, 409, 500 or 503", true),
438            p("err_code", "number", "JetStream error code, e.g. 10059 (stream not found), 10014 (consumer not found), 10058 (stream name in use)", false),
439            p("description", "string", "Explanation, at most 256 bytes", true),
440        ],
441        json!({"type":"nats_js_error","code":404,"err_code":10059,"description":"stream not found"}),
442        "-> JetStream error {code} {description}",
443    )
444}
445
446fn ack_action() -> ActionDefinition {
447    a(
448        "nats_js_ack",
449        "Acknowledge the publish as stored at stream sequence seq.",
450        vec![
451            p(
452                "seq",
453                "number",
454                "The stream sequence this message was stored at (1, 2, …)",
455                true,
456            ),
457            p(
458                "duplicate",
459                "boolean",
460                "True if this was a duplicate of an already stored message",
461                false,
462            ),
463        ],
464        json!({"type":"nats_js_ack","seq":1}),
465        "-> JetStream publish ack seq={seq}",
466    )
467}
468
469fn deliver_action() -> ActionDefinition {
470    a(
471        "nats_js_deliver",
472        "Deliver up to batch messages for the pull; fewer (or none) ends it early with a status, as an expired pull does.",
473        vec![
474            p("messages", "array", "Each {subject, payload, encoding? (utf8|hex), headers?, stream_seq, consumer_seq?, delivered?}", true),
475            p("pending", "number", "Messages still waiting after these (default 0)", false),
476        ],
477        json!({"type":"nats_js_deliver","messages":[{"subject":"orders.new","payload":"{\"id\":1}","stream_seq":1}]}),
478        "-> JetStream deliver {preview(messages,100)}",
479    )
480}
481
482fn noted_action() -> ActionDefinition {
483    a(
484        "nats_js_noted",
485        "Take note of an acknowledgement; nothing is sent beyond confirming it if the client asked.",
486        vec![],
487        json!({"type":"nats_js_noted"}),
488        "-> JetStream ack noted",
489    )
490}
491
492pub fn actions() -> Vec<ActionDefinition> {
493    vec![
494        reply_action(),
495        error_action(),
496        ack_action(),
497        deliver_action(),
498        noted_action(),
499    ]
500}
501
502pub static API_EVENT: LazyLock<EventType> = LazyLock::new(|| {
503    EventType::new(
504        "nats_js_api",
505        "A JetStream API request ($JS.API.…): stream and consumer management and account info. NetGet stores nothing — you own the streams, their messages and consumers.",
506        reply_action().example.clone(),
507    )
508    .with_parameters(vec![
509        p("operation", "string", "INFO, STREAM.CREATE/UPDATE/INFO/DELETE/PURGE/NAMES/LIST, CONSUMER.CREATE/INFO/DELETE/NAMES/LIST", true),
510        p("stream", "string", "The stream the request names, if any", false),
511        p("consumer", "string", "The consumer the request names, if any", false),
512        p("request", "object", "The request's JSON body (e.g. the stream or consumer config)", true),
513    ])
514    .with_actions(vec![reply_action(), error_action()])
515});
516
517pub static PUBLISH_EVENT: LazyLock<EventType> = LazyLock::new(|| {
518    EventType::new(
519        "nats_js_publish",
520        "A message was published to a subject a stream captures. Store it (remember it) and ack with its sequence, or refuse.",
521        ack_action().example.clone(),
522    )
523    .with_parameters(vec![
524        p("stream", "string", "The stream whose subjects matched", true),
525        p("subject", "string", "The subject published to", true),
526        p("payload", "string", "Message body (text, or hex per payload_encoding)", true),
527        p("payload_encoding", "string", "How payload is encoded: utf8 or hex", true),
528        p("headers", "object", "Message headers", true),
529        p("wants_ack", "boolean", "Whether the publisher waits for an ack", true),
530    ])
531    .with_actions(vec![ack_action(), error_action()])
532});
533
534pub static PULL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
535    EventType::new(
536        "nats_js_pull",
537        "A consumer pulled messages. Deliver the next ones you hold for it (at most batch).",
538        deliver_action().example.clone(),
539    )
540    .with_parameters(vec![
541        p("stream", "string", "The JetStream stream's name", true),
542        p("consumer", "string", "The pull consumer's name", true),
543        p("batch", "number", "Most messages wanted", true),
544        p(
545            "no_wait",
546            "boolean",
547            "The client wants an immediate answer, even an empty one",
548            true,
549        ),
550        p(
551            "expires_ms",
552            "number",
553            "How long the client will wait",
554            true,
555        ),
556    ])
557    .with_actions(vec![deliver_action()])
558});
559
560pub static ACK_EVENT: LazyLock<EventType> = LazyLock::new(|| {
561    EventType::new(
562        "nats_js_acked",
563        "A consumer acknowledged a delivery (ack, nak, in-progress or term).",
564        noted_action().example.clone(),
565    )
566    .with_parameters(vec![
567        p("stream", "string", "The JetStream stream's name", true),
568        p("consumer", "string", "The pull consumer's name", true),
569        p(
570            "stream_seq",
571            "number",
572            "Stream sequence of the acknowledged message",
573            true,
574        ),
575        p(
576            "consumer_seq",
577            "number",
578            "Consumer sequence of the delivery",
579            true,
580        ),
581        p(
582            "delivered",
583            "number",
584            "How many times it had been delivered",
585            true,
586        ),
587        p("kind", "string", "ack, nak, progress, term or next", true),
588    ])
589    .with_actions(vec![noted_action()])
590});
591
592pub fn event_types() -> Vec<EventType> {
593    vec![
594        API_EVENT.clone(),
595        PUBLISH_EVENT.clone(),
596        PULL_EVENT.clone(),
597        ACK_EVENT.clone(),
598    ]
599}
600
601/// Validate one JetStream action; `None` when the name is not JetStream's.
602pub fn execute(action: &Value) -> Option<Result<ActionResult>> {
603    let name = action["type"].as_str()?;
604    let checked = match name {
605        "nats_js_reply" => action["response"]
606            .is_object()
607            .then_some(())
608            .context("response must be an object"),
609        "nats_js_error" => (|| {
610            let code = action["code"].as_u64().context("code required")?;
611            ensure!(
612                [400, 404, 409, 500, 503].contains(&code),
613                "code must be 400, 404, 409, 500 or 503"
614            );
615            let d = action["description"]
616                .as_str()
617                .context("description required")?;
618            ensure!(
619                d.len() <= 256 && !d.contains(['\r', '\n']),
620                "description at most 256 bytes, one line"
621            );
622            Ok(())
623        })(),
624        "nats_js_ack" => action["seq"]
625            .as_u64()
626            .filter(|n| *n > 0)
627            .map(|_| ())
628            .context("seq must be a positive integer"),
629        "nats_js_deliver" => (|| {
630            let messages = action["messages"]
631                .as_array()
632                .context("messages must be an array")?;
633            ensure!(
634                messages.len() as u64 <= MAX_BATCH,
635                "at most {MAX_BATCH} messages"
636            );
637            for m in messages {
638                delivery("_", "1", "S", "C", m, 1, 0)?;
639            }
640            Ok(())
641        })(),
642        "nats_js_noted" => Ok(()),
643        _ => return None,
644    };
645    Some(checked.map(|_| ActionResult::Custom {
646        name: name.to_string(),
647        data: action.clone(),
648    }))
649}
650
651// ---------------------------------------------------------------------------
652// Answering
653// ---------------------------------------------------------------------------
654
655/// What the handler said, flattened to JetStream actions.
656fn answers(result: crate::llm::actions::executor::ExecutionResult) -> Result<Vec<(String, Value)>> {
657    ensure!(result.failures.is_empty(), "the handler's actions failed");
658    let mut out = Vec::new();
659    let mut stack = result.protocol_results;
660    while let Some(r) = stack.pop() {
661        match r {
662            ActionResult::Custom { name, data } if name.starts_with("nats_js_") => {
663                out.push((name, data))
664            }
665            ActionResult::Multiple(items) => stack.extend(items),
666            _ => {}
667        }
668    }
669    out.reverse();
670    Ok(out)
671}
672
673pub struct Frame<'a> {
674    pub subject: &'a str,
675    pub reply_to: Option<&'a str>,
676    pub headers: &'a BTreeMap<String, String>,
677    pub payload: &'a [u8],
678}
679
680/// Ask the handler about one JetStream frame and return the bytes to write.
681/// `sid_for` finds the subscription a reply subject reaches.
682pub async fn answer(
683    js: &Shared,
684    kind: Kind,
685    frame: Frame<'_>,
686    sid_for: impl Fn(&str) -> Option<String>,
687    ask: impl AsyncFnOnce(Event) -> Result<crate::llm::actions::executor::ExecutionResult>,
688    log: &crate::logging::emit::Log<'_>,
689) -> Vec<u8> {
690    let target = frame
691        .reply_to
692        .and_then(|r| sid_for(r).map(|sid| (r.to_string(), sid)));
693    let outcome = |op: &str, decision: &str| {
694        let line = format!("NATS JetStream operation={op} decision={decision}");
695        if decision.starts_with("fail_closed") {
696            log.error(line)
697        } else {
698            log.info(line)
699        }
700    };
701    let failure_text = |e: Option<&anyhow::Error>| match e {
702        Some(e) => crate::utils::wire_failure::prefixed_wire_failure_text(e),
703        None => crate::utils::WireFailure::Unavailable.prefixed_text(),
704    };
705    let send = |body: Value| -> Vec<u8> {
706        match &target {
707            Some((inbox, sid)) => reply(inbox, sid, body.to_string().as_bytes()),
708            None => Vec::new(),
709        }
710    };
711    match kind {
712        Kind::Api {
713            operation,
714            stream,
715            consumer,
716        } => {
717            let request: Value = if frame.payload.is_empty() {
718                json!({})
719            } else {
720                match serde_json::from_slice(frame.payload) {
721                    Ok(v) => v,
722                    Err(_) => {
723                        outcome(&operation, "fail_closed_bad_request");
724                        return send(error_response(
725                            &operation,
726                            400,
727                            10025,
728                            "bad request: the body is not JSON",
729                        ));
730                    }
731                }
732            };
733            if response_type(&operation).is_none() {
734                outcome(&operation, "unsupported");
735                return send(error_response(
736                    &operation,
737                    400,
738                    10025,
739                    "this JetStream API is not supported",
740                ));
741            }
742            for name in [&stream, &consumer].into_iter().flatten() {
743                if check_token(name, "name").is_err() || name.contains(['*', '>']) {
744                    return send(error_response(&operation, 400, 10025, "invalid name"));
745                }
746            }
747            let event = Event::new(
748                &API_EVENT,
749                json!({"operation": operation, "stream": stream, "consumer": consumer, "request": request}),
750            );
751            let result = match ask(event).await.and_then(answers) {
752                Ok(r) => r,
753                Err(e) => {
754                    outcome(&operation, "fail_closed_llm_error");
755                    return send(error_response(
756                        &operation,
757                        503,
758                        10000,
759                        failure_text(Some(&e)),
760                    ));
761                }
762            };
763            match result.as_slice() {
764                [(n, d)] if n == "nats_js_reply" => {
765                    let given = d["response"].as_object().cloned().unwrap_or_default();
766                    let body = build_response(
767                        &operation,
768                        stream.as_deref(),
769                        consumer.as_deref(),
770                        &request,
771                        &given,
772                    );
773                    match (operation.as_str(), stream.as_deref()) {
774                        ("STREAM.CREATE" | "STREAM.UPDATE", Some(s)) => {
775                            js.remember(s, &body["config"])
776                        }
777                        ("STREAM.DELETE", Some(s)) => js.forget(s),
778                        _ => {}
779                    }
780                    outcome(&operation, "model_answer");
781                    send(body)
782                }
783                [(n, d)] if n == "nats_js_error" => {
784                    outcome(&operation, "model_reject");
785                    send(error_response(
786                        &operation,
787                        d["code"].as_u64().unwrap_or(500),
788                        d["err_code"].as_u64().unwrap_or(10000),
789                        d["description"].as_str().unwrap_or_default(),
790                    ))
791                }
792                [] => {
793                    outcome(&operation, "fail_closed_model_silent");
794                    send(error_response(&operation, 503, 10000, failure_text(None)))
795                }
796                _ => {
797                    outcome(&operation, "fail_closed_invalid_reply");
798                    send(error_response(&operation, 503, 10000, failure_text(None)))
799                }
800            }
801        }
802        Kind::Publish { stream } => {
803            let (payload, encoding) = if frame
804                .payload
805                .iter()
806                .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
807            {
808                (String::from_utf8_lossy(frame.payload).to_string(), "utf8")
809            } else {
810                (hex::encode(frame.payload), "hex")
811            };
812            let event = Event::new(
813                &PUBLISH_EVENT,
814                json!({"stream": stream, "subject": frame.subject, "payload": payload,
815                       "payload_encoding": encoding, "headers": frame.headers,
816                       "wants_ack": frame.reply_to.is_some()}),
817            );
818            let fail = |text: &str| json!({"error": {"code": 503, "err_code": 10000, "description": text}});
819            let result = match ask(event).await.and_then(answers) {
820                Ok(r) => r,
821                Err(e) => {
822                    outcome("PUBLISH", "fail_closed_llm_error");
823                    return send(fail(failure_text(Some(&e))));
824                }
825            };
826            match result.as_slice() {
827                [(n, d)] if n == "nats_js_ack" => {
828                    outcome("PUBLISH", "model_answer");
829                    let mut ack = json!({"stream": stream, "seq": d["seq"]});
830                    if d["duplicate"] == true {
831                        ack["duplicate"] = json!(true);
832                    }
833                    send(ack)
834                }
835                [(n, d)] if n == "nats_js_error" => {
836                    outcome("PUBLISH", "model_reject");
837                    send(
838                        json!({"error": {"code": d["code"], "err_code": d["err_code"].as_u64().unwrap_or(10000),
839                                          "description": d["description"]}}),
840                    )
841                }
842                _ => {
843                    outcome("PUBLISH", "fail_closed_invalid_reply");
844                    send(fail(failure_text(None)))
845                }
846            }
847        }
848        Kind::Pull { stream, consumer } => {
849            let request: Value = serde_json::from_slice(frame.payload).unwrap_or(json!({}));
850            let batch = request["batch"].as_u64().unwrap_or(1).clamp(1, MAX_BATCH);
851            let no_wait = request["no_wait"].as_bool().unwrap_or(false);
852            let expires_ms = request["expires"].as_u64().unwrap_or(0) / 1_000_000;
853            let Some((inbox, sid)) = target else {
854                log.warn(format!("NATS JetStream pull on {stream}.{consumer} has no subscribed inbox to deliver to"));
855                return Vec::new();
856            };
857            let event = Event::new(
858                &PULL_EVENT,
859                json!({"stream": stream, "consumer": consumer, "batch": batch,
860                       "no_wait": no_wait, "expires_ms": expires_ms}),
861            );
862            let (messages, pending) = match ask(event).await.and_then(answers) {
863                Ok(r) => match r.as_slice() {
864                    [(n, d)] if n == "nats_js_deliver" => {
865                        outcome("PULL", "model_answer");
866                        (
867                            d["messages"].as_array().cloned().unwrap_or_default(),
868                            d["pending"].as_u64().unwrap_or(0),
869                        )
870                    }
871                    [] => {
872                        outcome("PULL", "model_silent");
873                        (Vec::new(), 0)
874                    }
875                    _ => {
876                        outcome("PULL", "fail_closed_invalid_reply");
877                        (Vec::new(), 0)
878                    }
879                },
880                // Nothing is fabricated: the pull ends empty.
881                Err(_) => {
882                    outcome("PULL", "fail_closed_llm_error");
883                    (Vec::new(), 0)
884                }
885            };
886            let mut out = Vec::new();
887            let count = (messages.len() as u64).min(batch);
888            for (i, m) in messages.iter().take(batch as usize).enumerate() {
889                let left = pending + count - i as u64 - 1;
890                match delivery(&inbox, &sid, &stream, &consumer, m, i as u64 + 1, left) {
891                    Ok(bytes) => out.extend(bytes),
892                    Err(e) => log.warn(format!("NATS JetStream delivery dropped: {e:#}")),
893                }
894            }
895            if count < batch {
896                out.extend(if no_wait && count == 0 {
897                    status(&inbox, &sid, 404, "No Messages", 0)
898                } else {
899                    status(&inbox, &sid, 408, "Request Timeout", batch - count)
900                });
901            }
902            out
903        }
904        Kind::Ack {
905            stream,
906            consumer,
907            delivered,
908            stream_seq,
909            consumer_seq,
910        } => {
911            let body = String::from_utf8_lossy(frame.payload);
912            let kind = match body.split_whitespace().next().unwrap_or("+ACK") {
913                "+ACK" | "" => "ack",
914                "-NAK" => "nak",
915                "+WPI" => "progress",
916                "+TERM" => "term",
917                "+NXT" => "next",
918                _ => "ack",
919            };
920            let event = Event::new(
921                &ACK_EVENT,
922                json!({"stream": stream, "consumer": consumer, "stream_seq": stream_seq,
923                       "consumer_seq": consumer_seq, "delivered": delivered, "kind": kind}),
924            );
925            if let Err(e) = ask(event).await.and_then(answers) {
926                log.warn(format!("NATS JetStream ack handler failed: {e:#}"));
927            }
928            outcome("ACK", "noted");
929            // A synchronous ack waits for an empty reply.
930            match &target {
931                Some((inbox, sid)) => reply(inbox, sid, b""),
932                None => Vec::new(),
933            }
934        }
935    }
936}