1use 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
24pub const MAX_BATCH: u64 = 256;
26pub const MAX_STREAMS: usize = 1024;
28
29#[derive(Default, Debug)]
31pub struct Shared {
32 streams: Mutex<BTreeMap<String, Vec<String>>>,
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub enum Kind {
38 Api {
40 operation: String,
41 stream: Option<String>,
42 consumer: Option<String>,
43 },
44 Pull { stream: String, consumer: String },
46 Ack {
48 stream: String,
49 consumer: String,
50 delivered: u64,
51 stream_seq: u64,
52 consumer_seq: u64,
53 },
54 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
138fn 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
169fn 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
205fn 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
247fn 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 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#[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 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
372fn 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
385pub 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
393fn 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
601pub 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
651fn 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
680pub 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 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 match &target {
931 Some((inbox, sid)) => reply(inbox, sid, b""),
932 None => Vec::new(),
933 }
934 }
935 }
936}