Skip to main content

graphrefly/
messaging.rs

1//! Graph-visible messaging application infrastructure.
2//!
3//! D279/D282/D284/D285/D276 define `messageBus` as a retained topic log plus
4//! independent subscription cursors. DynamicHub is intentionally retired: topic
5//! lifecycle, publish, cursor movement, status, and issues are graph-visible
6//! command/fact streams on one static bus surface.
7
8use std::cell::RefCell;
9use std::collections::BTreeMap;
10use std::collections::HashSet;
11use std::error::Error;
12use std::fmt;
13use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
14use std::rc::Rc;
15
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18
19use crate::ctx::Ctx;
20use crate::graph::{Graph, GraphNodeOpts};
21use crate::identity::canonical_tuple_key;
22use crate::json::JsonValue;
23use crate::node::{Core, Node, NodeOpts};
24use crate::protocol::{AnyValue, LockId};
25
26/// `constant` constant.
27pub const PROMPTS_TOPIC: &str = "prompts";
28/// `constant` constant.
29pub const RESPONSES_TOPIC: &str = "responses";
30/// `constant` constant.
31pub const INJECTIONS_TOPIC: &str = "injections";
32/// `constant` constant.
33pub const DEFERRED_TOPIC: &str = "deferred";
34/// `constant` constant.
35pub const SPAWNS_TOPIC: &str = "spawns";
36/// `constant` constant.
37pub const CONTEXT_TOPIC: &str = "context";
38/// `constant` constant.
39pub const TODOS_TOPIC: &str = "todos";
40
41/// `constant` constant.
42pub const STANDARD_TOPICS: [&str; 7] = [
43    PROMPTS_TOPIC,
44    RESPONSES_TOPIC,
45    INJECTIONS_TOPIC,
46    DEFERRED_TOPIC,
47    SPAWNS_TOPIC,
48    CONTEXT_TOPIC,
49    TODOS_TOPIC,
50];
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54/// `JsonSchemaType` variants.
55pub enum JsonSchemaType {
56    /// `String` variant.
57    String,
58    /// `Number` variant.
59    Number,
60    /// `Integer` variant.
61    Integer,
62    /// `Boolean` variant.
63    Boolean,
64    /// `Object` variant.
65    Object,
66    /// `Array` variant.
67    Array,
68    /// `Null` variant.
69    Null,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(untagged)]
74/// `JsonSchemaTypeSpec` variants.
75pub enum JsonSchemaTypeSpec {
76    /// `Single` variant.
77    Single(JsonSchemaType),
78    /// `AnyOf` variant.
79    AnyOf(Vec<JsonSchemaType>),
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(untagged)]
84/// `JsonSchemaAdditionalProperties` variants.
85pub enum JsonSchemaAdditionalProperties {
86    /// `Bool` variant.
87    Bool(bool),
88    /// `Schema` variant.
89    Schema(Box<JsonSchema>),
90}
91
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(untagged)]
94/// `JsonSchemaItems` variants.
95pub enum JsonSchemaItems {
96    /// `Schema` variant.
97    Schema(Box<JsonSchema>),
98    /// `Tuple` variant.
99    Tuple(Vec<JsonSchema>),
100}
101
102/// Minimal passive JSON Schema vocabulary for D159 messaging payload facts.
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
104#[serde(deny_unknown_fields)]
105pub struct JsonSchema {
106    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
107    /// `schema_type` field for schema type.
108    pub schema_type: Option<JsonSchemaTypeSpec>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    /// `properties` field for properties.
111    pub properties: Option<BTreeMap<String, JsonSchema>>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    /// `required` field for required.
114    pub required: Option<Vec<String>>,
115    #[serde(
116        rename = "additionalProperties",
117        skip_serializing_if = "Option::is_none"
118    )]
119    /// `additional_properties` field for additional properties.
120    pub additional_properties: Option<JsonSchemaAdditionalProperties>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    /// `items` field for items.
123    pub items: Option<JsonSchemaItems>,
124    #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
125    /// `enum_values` field for enum values.
126    pub enum_values: Option<Vec<JsonValue>>,
127    #[serde(rename = "const", skip_serializing_if = "Option::is_none")]
128    /// `const_value` field for const value.
129    pub const_value: Option<JsonValue>,
130    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
131    /// `ref_path` field for ref path.
132    pub ref_path: Option<String>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    /// `definitions` field for definitions.
135    pub definitions: Option<BTreeMap<String, JsonSchema>>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    /// `description` field for description.
138    pub description: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    /// `title` field for title.
141    pub title: Option<String>,
142}
143
144/// Passive D159 envelope for payloads that cross topic, agent, or graph boundaries.
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct TopicMessage<T> {
147    /// `id` field for id.
148    pub id: String,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    /// `schema` field for schema.
151    pub schema: Option<JsonSchema>,
152    #[serde(rename = "expiresAt", skip_serializing_if = "Option::is_none")]
153    /// `expires_at` field for expires at.
154    pub expires_at: Option<String>,
155    #[serde(rename = "correlationId", skip_serializing_if = "Option::is_none")]
156    /// `correlation_id` field for correlation id.
157    pub correlation_id: Option<String>,
158    /// `payload` field for payload.
159    pub payload: T,
160}
161
162/// Passive domain event vocabulary for messageBus/eventFlow composition (D329).
163#[derive(Clone, Debug, PartialEq)]
164pub struct EventMessage<T> {
165    /// `id` field for id.
166    pub id: String,
167    /// `type_` field for type.
168    pub type_: String,
169    /// `payload` field for payload.
170    pub payload: T,
171    /// `key` field for key.
172    pub key: Option<String>,
173    /// `subject_id` field for subject id.
174    pub subject_id: Option<String>,
175    /// `correlation_id` field for correlation id.
176    pub correlation_id: Option<String>,
177    /// `causation_id` field for causation id.
178    pub causation_id: Option<String>,
179    /// `occurred_at_ms` field for occurred at ms.
180    pub occurred_at_ms: Option<u64>,
181    /// `actor` field for actor.
182    pub actor: Option<String>,
183    /// `evidence_refs` field for evidence refs.
184    pub evidence_refs: Vec<String>,
185    /// `schema` field for schema.
186    pub schema: Option<JsonSchema>,
187    /// `metadata` field for metadata.
188    pub metadata: Option<BTreeMap<String, JsonValue>>,
189}
190
191#[derive(Clone, Debug, Default, PartialEq)]
192/// `EventMessageOptions` data container.
193pub struct EventMessageOptions {
194    /// `id` field for id.
195    pub id: String,
196    /// `key` field for key.
197    pub key: Option<String>,
198    /// `subject_id` field for subject id.
199    pub subject_id: Option<String>,
200    /// `correlation_id` field for correlation id.
201    pub correlation_id: Option<String>,
202    /// `causation_id` field for causation id.
203    pub causation_id: Option<String>,
204    /// `occurred_at_ms` field for occurred at ms.
205    pub occurred_at_ms: Option<u64>,
206    /// `actor` field for actor.
207    pub actor: Option<String>,
208    /// `evidence_refs` field for evidence refs.
209    pub evidence_refs: Vec<String>,
210    /// `schema` field for schema.
211    pub schema: Option<JsonSchema>,
212    /// `metadata` field for metadata.
213    pub metadata: Option<BTreeMap<String, JsonValue>>,
214}
215
216/// Creates or computes `event_message`.
217pub fn event_message<T>(
218    type_: impl Into<String>,
219    payload: T,
220    opts: EventMessageOptions,
221) -> EventMessage<T> {
222    let type_ = type_.into();
223    assert_non_empty(&type_, "eventMessage.type");
224    assert_non_empty(&opts.id, "eventMessage.id");
225    EventMessage {
226        id: opts.id,
227        type_,
228        payload,
229        key: opts.key,
230        subject_id: opts.subject_id,
231        correlation_id: opts.correlation_id,
232        causation_id: opts.causation_id,
233        occurred_at_ms: opts.occurred_at_ms,
234        actor: opts.actor,
235        evidence_refs: opts.evidence_refs,
236        schema: opts.schema,
237        metadata: opts.metadata,
238    }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
242/// `JsonSchemaValidationError` data container.
243pub struct JsonSchemaValidationError {
244    /// `path` field for path.
245    pub path: String,
246    /// `message` field for message.
247    pub message: String,
248}
249
250impl JsonSchemaValidationError {
251    fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
252        Self {
253            path: path.into(),
254            message: message.into(),
255        }
256    }
257}
258
259impl fmt::Display for JsonSchemaValidationError {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        write!(f, "{}: {}", self.path, self.message)
262    }
263}
264
265impl Error for JsonSchemaValidationError {}
266
267/// `JsonSchemaValidationResult` type alias.
268pub type JsonSchemaValidationResult<T> = Result<T, JsonSchemaValidationError>;
269
270/// Caller-invoked, side-effect-free validation for JSON-like payload values (D159).
271pub fn validate_json_schema(
272    schema: &JsonSchema,
273    value: &JsonValue,
274) -> JsonSchemaValidationResult<()> {
275    validate_json_schema_inner(schema, schema, value, "$", &mut Vec::new())
276}
277
278/// Creates or computes `is_json_schema_valid`.
279pub fn is_json_schema_valid(schema: &JsonSchema, value: &JsonValue) -> bool {
280    validate_json_schema(schema, value).is_ok()
281}
282
283/// Validate a passive topic message payload only when the caller explicitly asks.
284pub fn validate_topic_message_payload(
285    message: &TopicMessage<JsonValue>,
286) -> JsonSchemaValidationResult<()> {
287    if let Some(schema) = &message.schema {
288        validate_json_schema(schema, &message.payload)?;
289    }
290    Ok(())
291}
292
293fn validate_json_schema_inner(
294    schema: &JsonSchema,
295    root: &JsonSchema,
296    value: &JsonValue,
297    path: &str,
298    refs_seen: &mut Vec<(String, String)>,
299) -> JsonSchemaValidationResult<()> {
300    if let Some(ref_path) = &schema.ref_path {
301        validate_json_schema_ref(root, ref_path, value, path, refs_seen)?;
302    }
303    if let Some(const_value) = &schema.const_value {
304        if value != const_value {
305            return Err(JsonSchemaValidationError::new(
306                path,
307                "value does not match const",
308            ));
309        }
310    }
311    if let Some(enum_values) = &schema.enum_values {
312        if !enum_values.iter().any(|candidate| candidate == value) {
313            return Err(JsonSchemaValidationError::new(
314                path,
315                "value is not one of the allowed enum values",
316            ));
317        }
318    }
319    if let Some(schema_type) = &schema.schema_type {
320        validate_json_schema_type(schema_type, value, path)?;
321    }
322    if let Value::Object(object) = value {
323        validate_json_schema_object(schema, root, object, path, refs_seen)?;
324    }
325    if let Value::Array(items) = value {
326        validate_json_schema_array(schema, root, items, path, refs_seen)?;
327    }
328    Ok(())
329}
330
331fn validate_json_schema_ref(
332    root: &JsonSchema,
333    ref_path: &str,
334    value: &JsonValue,
335    path: &str,
336    refs_seen: &mut Vec<(String, String)>,
337) -> JsonSchemaValidationResult<()> {
338    let name = ref_path.strip_prefix("#/definitions/").ok_or_else(|| {
339        JsonSchemaValidationError::new(path, "only local #/definitions refs are supported")
340    })?;
341    if refs_seen
342        .iter()
343        .any(|(seen_ref, seen_path)| seen_ref == ref_path && seen_path == path)
344    {
345        return Err(JsonSchemaValidationError::new(
346            path,
347            format!("cyclic JSON schema ref '{ref_path}'"),
348        ));
349    }
350    let definitions = root.definitions.as_ref().ok_or_else(|| {
351        JsonSchemaValidationError::new(path, format!("unknown JSON schema ref '{ref_path}'"))
352    })?;
353    let referenced = definitions.get(name).ok_or_else(|| {
354        JsonSchemaValidationError::new(path, format!("unknown JSON schema ref '{ref_path}'"))
355    })?;
356    refs_seen.push((ref_path.to_owned(), path.to_owned()));
357    let result = validate_json_schema_inner(referenced, root, value, path, refs_seen);
358    refs_seen.pop();
359    result
360}
361
362fn validate_json_schema_type(
363    schema_type: &JsonSchemaTypeSpec,
364    value: &JsonValue,
365    path: &str,
366) -> JsonSchemaValidationResult<()> {
367    let ok = match schema_type {
368        JsonSchemaTypeSpec::Single(expected) => json_value_matches_type(value, *expected),
369        JsonSchemaTypeSpec::AnyOf(expected) => expected
370            .iter()
371            .any(|expected| json_value_matches_type(value, *expected)),
372    };
373    if ok {
374        Ok(())
375    } else {
376        Err(JsonSchemaValidationError::new(
377            path,
378            format!(
379                "expected {}, got {}",
380                json_schema_type_label(schema_type),
381                json_value_type_label(value)
382            ),
383        ))
384    }
385}
386
387fn json_value_matches_type(value: &JsonValue, expected: JsonSchemaType) -> bool {
388    match expected {
389        JsonSchemaType::String => value.is_string(),
390        JsonSchemaType::Number => value.is_number(),
391        JsonSchemaType::Integer => {
392            value.as_i64().is_some()
393                || value.as_u64().is_some()
394                || value
395                    .as_f64()
396                    .is_some_and(|number| number.is_finite() && number.fract() == 0.0)
397        }
398        JsonSchemaType::Boolean => value.is_boolean(),
399        JsonSchemaType::Object => value.is_object(),
400        JsonSchemaType::Array => value.is_array(),
401        JsonSchemaType::Null => value.is_null(),
402    }
403}
404
405fn json_schema_type_label(schema_type: &JsonSchemaTypeSpec) -> String {
406    match schema_type {
407        JsonSchemaTypeSpec::Single(expected) => format!("{expected:?}").to_lowercase(),
408        JsonSchemaTypeSpec::AnyOf(expected) => expected
409            .iter()
410            .map(|expected| format!("{expected:?}").to_lowercase())
411            .collect::<Vec<_>>()
412            .join("|"),
413    }
414}
415
416fn json_value_type_label(value: &JsonValue) -> &'static str {
417    match value {
418        Value::Null => "null",
419        Value::Bool(_) => "boolean",
420        Value::Number(_) => "number",
421        Value::String(_) => "string",
422        Value::Array(_) => "array",
423        Value::Object(_) => "object",
424    }
425}
426
427fn validate_json_schema_object(
428    schema: &JsonSchema,
429    root: &JsonSchema,
430    object: &serde_json::Map<String, Value>,
431    path: &str,
432    refs_seen: &mut Vec<(String, String)>,
433) -> JsonSchemaValidationResult<()> {
434    if let Some(required) = &schema.required {
435        for key in required {
436            if !object.contains_key(key) {
437                return Err(JsonSchemaValidationError::new(
438                    path,
439                    format!("missing required property '{key}'"),
440                ));
441            }
442        }
443    }
444    let empty_properties = BTreeMap::new();
445    let properties = schema.properties.as_ref().unwrap_or(&empty_properties);
446    for (key, property_schema) in properties {
447        if let Some(property_value) = object.get(key) {
448            validate_json_schema_inner(
449                property_schema,
450                root,
451                property_value,
452                &format!("{path}.{key}"),
453                refs_seen,
454            )?;
455        }
456    }
457    validate_additional_properties(schema, root, object, properties, path, refs_seen)?;
458    Ok(())
459}
460
461fn validate_additional_properties(
462    schema: &JsonSchema,
463    root: &JsonSchema,
464    object: &serde_json::Map<String, Value>,
465    properties: &BTreeMap<String, JsonSchema>,
466    path: &str,
467    refs_seen: &mut Vec<(String, String)>,
468) -> JsonSchemaValidationResult<()> {
469    let Some(additional_properties) = &schema.additional_properties else {
470        return Ok(());
471    };
472    for (key, value) in object {
473        if properties.contains_key(key) {
474            continue;
475        }
476        match additional_properties {
477            JsonSchemaAdditionalProperties::Bool(true) => {}
478            JsonSchemaAdditionalProperties::Bool(false) => {
479                return Err(JsonSchemaValidationError::new(
480                    path,
481                    format!("unexpected additional property '{key}'"),
482                ));
483            }
484            JsonSchemaAdditionalProperties::Schema(additional_schema) => {
485                validate_json_schema_inner(
486                    additional_schema,
487                    root,
488                    value,
489                    &format!("{path}.{key}"),
490                    refs_seen,
491                )?;
492            }
493        }
494    }
495    Ok(())
496}
497
498fn validate_json_schema_array(
499    schema: &JsonSchema,
500    root: &JsonSchema,
501    values: &[Value],
502    path: &str,
503    refs_seen: &mut Vec<(String, String)>,
504) -> JsonSchemaValidationResult<()> {
505    let Some(items) = &schema.items else {
506        return Ok(());
507    };
508    match items {
509        JsonSchemaItems::Schema(item_schema) => {
510            for (index, value) in values.iter().enumerate() {
511                validate_json_schema_inner(
512                    item_schema,
513                    root,
514                    value,
515                    &format!("{path}[{index}]"),
516                    refs_seen,
517                )?;
518            }
519        }
520        JsonSchemaItems::Tuple(item_schemas) => {
521            for (index, item_schema) in item_schemas.iter().enumerate() {
522                if let Some(value) = values.get(index) {
523                    validate_json_schema_inner(
524                        item_schema,
525                        root,
526                        value,
527                        &format!("{path}[{index}]"),
528                        refs_seen,
529                    )?;
530                }
531            }
532        }
533    }
534    Ok(())
535}
536
537#[derive(Debug, Clone, PartialEq)]
538/// `DataIssue` data container.
539pub struct DataIssue {
540    /// `kind` field for kind.
541    pub kind: String,
542    /// `code` field for code.
543    pub code: String,
544    /// `message` field for message.
545    pub message: String,
546    /// `severity` field for severity.
547    pub severity: String,
548    /// `source` field for source.
549    pub source: String,
550    /// `topic` field for topic.
551    pub topic: Option<String>,
552    /// `details` field for details.
553    pub details: Option<String>,
554}
555
556#[derive(Debug, Clone, PartialEq)]
557/// `MessageEnvelope` data container.
558pub struct MessageEnvelope<T = AnyValue> {
559    /// `topic` field for topic.
560    pub topic: String,
561    /// `seq` field for seq.
562    pub seq: u64,
563    /// `payload` field for payload.
564    pub payload: T,
565    /// `key` field for key.
566    pub key: Option<String>,
567    /// `timestamp_ms` field for timestamp ms.
568    pub timestamp_ms: u64,
569    /// `command_id` field for command id.
570    pub command_id: Option<String>,
571    /// `idempotency_key` field for idempotency key.
572    pub idempotency_key: Option<String>,
573}
574
575impl MessageEnvelope<AnyValue> {
576    /// Updates or reads `payload_as`.
577    pub fn payload_as<T: 'static>(&self) -> Option<Rc<T>> {
578        self.payload.clone().downcast::<T>().ok()
579    }
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583/// `MessageBusTopicPolicy` variants.
584pub enum MessageBusTopicPolicy {
585    /// `Strict` variant.
586    Strict,
587    /// `CreateAsFact` variant.
588    CreateAsFact,
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Default)]
592/// `MessageBusRetentionPolicy` data container.
593pub struct MessageBusRetentionPolicy {
594    /// `max_messages` field for max messages.
595    pub max_messages: Option<usize>,
596    /// `max_age_ms` field for max age ms.
597    pub max_age_ms: Option<u64>,
598}
599
600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
601/// `MessageBusDedupeAction` variants.
602pub enum MessageBusDedupeAction {
603    /// `Status` variant.
604    Status,
605    /// `Issue` variant.
606    Issue,
607}
608
609#[derive(Debug, Clone, PartialEq, Eq)]
610/// `MessageBusDedupePolicy` data container.
611pub struct MessageBusDedupePolicy {
612    /// `command_id` field for command id.
613    pub command_id: MessageBusDedupeAction,
614}
615
616impl Default for MessageBusDedupePolicy {
617    fn default() -> Self {
618        Self {
619            command_id: MessageBusDedupeAction::Status,
620        }
621    }
622}
623
624#[derive(Clone)]
625/// `MessageBusOptions` data container.
626pub struct MessageBusOptions {
627    /// `name` field for name.
628    pub name: String,
629    /// `topics` field for topics.
630    pub topics: Vec<String>,
631    /// `topic_policy` field for topic policy.
632    pub topic_policy: MessageBusTopicPolicy,
633    /// `retention` field for retention.
634    pub retention: MessageBusRetentionPolicy,
635    /// `dedupe` field for dedupe.
636    pub dedupe: MessageBusDedupePolicy,
637    /// `now` field for now.
638    pub now: Rc<dyn Fn() -> u64>,
639}
640
641impl Default for MessageBusOptions {
642    fn default() -> Self {
643        Self {
644            name: "messageBus".to_owned(),
645            topics: Vec::new(),
646            topic_policy: MessageBusTopicPolicy::Strict,
647            retention: MessageBusRetentionPolicy::default(),
648            dedupe: MessageBusDedupePolicy::default(),
649            now: Rc::new(|| 0),
650        }
651    }
652}
653
654impl MessageBusOptions {
655    /// Creates or computes `named`.
656    pub fn named(name: impl Into<String>) -> Self {
657        Self {
658            name: name.into(),
659            ..Self::default()
660        }
661    }
662
663    /// Updates or reads `with_topics`.
664    pub fn with_topics(mut self, topics: impl IntoIterator<Item = impl Into<String>>) -> Self {
665        self.topics = topics.into_iter().map(Into::into).collect();
666        self
667    }
668
669    /// Updates or reads `with_topic_policy`.
670    pub fn with_topic_policy(mut self, policy: MessageBusTopicPolicy) -> Self {
671        self.topic_policy = policy;
672        self
673    }
674
675    /// Updates or reads `with_retention`.
676    pub fn with_retention(mut self, retention: MessageBusRetentionPolicy) -> Self {
677        self.retention = retention;
678        self
679    }
680
681    /// Updates or reads `with_dedupe`.
682    pub fn with_dedupe(mut self, dedupe: MessageBusDedupePolicy) -> Self {
683        self.dedupe = dedupe;
684        self
685    }
686
687    /// Updates or reads `with_now`.
688    pub fn with_now(mut self, now: impl Fn() -> u64 + 'static) -> Self {
689        self.now = Rc::new(now);
690        self
691    }
692}
693
694#[derive(Debug, Clone, PartialEq)]
695/// `MessageBusCommand` variants.
696pub enum MessageBusCommand<T = AnyValue> {
697    /// `EnsureTopic` variant.
698    EnsureTopic {
699        /// `topic` field for topic.
700        topic: String,
701        /// `command_id` field for command id.
702        command_id: Option<String>,
703    },
704    /// `CloseTopic` variant.
705    CloseTopic {
706        /// `topic` field for topic.
707        topic: String,
708        /// `command_id` field for command id.
709        command_id: Option<String>,
710    },
711    /// `Publish` variant.
712    Publish {
713        /// `topic` field for topic.
714        topic: String,
715        /// `payload` field for payload.
716        payload: T,
717        /// `key` field for key.
718        key: Option<String>,
719        /// `command_id` field for command id.
720        command_id: Option<String>,
721        /// `idempotency_key` field for idempotency key.
722        idempotency_key: Option<String>,
723    },
724    /// `TopicPolicy` variant.
725    TopicPolicy {
726        /// `topic_policy` field for topic policy.
727        topic_policy: MessageBusTopicPolicy,
728        /// `command_id` field for command id.
729        command_id: Option<String>,
730    },
731    /// `Ack` variant.
732    Ack {
733        /// `topic` field for topic.
734        topic: String,
735        /// `subscription_id` field for subscription id.
736        subscription_id: String,
737        /// `seq` field for seq.
738        seq: u64,
739        /// `command_id` field for command id.
740        command_id: Option<String>,
741    },
742    /// `Seek` variant.
743    Seek {
744        /// `topic` field for topic.
745        topic: String,
746        /// `subscription_id` field for subscription id.
747        subscription_id: String,
748        /// `next_seq` field for next seq.
749        next_seq: u64,
750        /// `command_id` field for command id.
751        command_id: Option<String>,
752    },
753    /// `CloseSubscription` variant.
754    CloseSubscription {
755        /// `topic` field for topic.
756        topic: String,
757        /// `subscription_id` field for subscription id.
758        subscription_id: String,
759        /// `command_id` field for command id.
760        command_id: Option<String>,
761    },
762}
763
764impl<T> MessageBusCommand<T> {
765    fn topic(&self) -> Option<&str> {
766        match self {
767            Self::EnsureTopic { topic, .. }
768            | Self::CloseTopic { topic, .. }
769            | Self::Publish { topic, .. }
770            | Self::Ack { topic, .. }
771            | Self::Seek { topic, .. }
772            | Self::CloseSubscription { topic, .. } => Some(topic),
773            Self::TopicPolicy { .. } => None,
774        }
775    }
776
777    fn command_id(&self) -> Option<&str> {
778        match self {
779            Self::EnsureTopic { command_id, .. }
780            | Self::CloseTopic { command_id, .. }
781            | Self::Publish { command_id, .. }
782            | Self::TopicPolicy { command_id, .. }
783            | Self::Ack { command_id, .. }
784            | Self::Seek { command_id, .. }
785            | Self::CloseSubscription { command_id, .. } => command_id.as_deref(),
786        }
787    }
788}
789
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
791/// `MessageBusStatusKind` variants.
792pub enum MessageBusStatusKind {
793    /// `TopicCreated` variant.
794    TopicCreated,
795    /// `TopicClosed` variant.
796    TopicClosed,
797    /// `MessagePublished` variant.
798    MessagePublished,
799    /// `RetentionTrimmed` variant.
800    RetentionTrimmed,
801    /// `DuplicateCommand` variant.
802    DuplicateCommand,
803    /// `SubscriptionOpened` variant.
804    SubscriptionOpened,
805    /// `SubscriptionAcked` variant.
806    SubscriptionAcked,
807    /// `SubscriptionSought` variant.
808    SubscriptionSought,
809    /// `SubscriptionClosed` variant.
810    SubscriptionClosed,
811}
812
813#[derive(Debug, Clone, PartialEq)]
814/// `MessageBusStatus` data container.
815pub struct MessageBusStatus {
816    /// `kind` field for kind.
817    pub kind: MessageBusStatusKind,
818    /// `topic` field for topic.
819    pub topic: Option<String>,
820    /// `seq` field for seq.
821    pub seq: Option<u64>,
822    /// `head_seq` field for head seq.
823    pub head_seq: Option<u64>,
824    /// `subscription_id` field for subscription id.
825    pub subscription_id: Option<String>,
826    /// `next_seq` field for next seq.
827    pub next_seq: Option<u64>,
828    /// `command_id` field for command id.
829    pub command_id: Option<String>,
830    /// `issue_code` field for issue code.
831    pub issue_code: Option<String>,
832    /// `timestamp_ms` field for timestamp ms.
833    pub timestamp_ms: u64,
834    /// `details` field for details.
835    pub details: Option<String>,
836}
837
838#[derive(Debug, Clone, PartialEq, Eq)]
839/// `MessageBusCatalogEntry` data container.
840pub struct MessageBusCatalogEntry {
841    /// `topic` field for topic.
842    pub topic: String,
843    /// `closed` field for closed.
844    pub closed: bool,
845    /// `head_seq` field for head seq.
846    pub head_seq: u64,
847    /// `next_seq` field for next seq.
848    pub next_seq: u64,
849    /// `message_count` field for message count.
850    pub message_count: usize,
851}
852
853#[derive(Debug, Clone, PartialEq, Eq)]
854/// `MessageBusCatalogPage` data container.
855pub struct MessageBusCatalogPage {
856    /// `topics` field for topics.
857    pub topics: Vec<MessageBusCatalogEntry>,
858    /// `next_after_topic` field for next after topic.
859    pub next_after_topic: Option<String>,
860    /// `has_more` field for has more.
861    pub has_more: bool,
862}
863
864#[derive(Debug, Clone, PartialEq)]
865/// `MessageBusDeadLetterEntry` data container.
866pub struct MessageBusDeadLetterEntry<T = AnyValue> {
867    /// `entry_seq` field for entry seq.
868    pub entry_seq: u64,
869    /// `topic` field for topic.
870    pub topic: Option<String>,
871    /// `command` field for command.
872    pub command: Option<MessageBusCommand<T>>,
873    /// `message` field for message.
874    pub message: Option<MessageEnvelope<T>>,
875    /// `issue` field for issue.
876    pub issue: DataIssue,
877    /// `timestamp_ms` field for timestamp ms.
878    pub timestamp_ms: u64,
879}
880
881#[derive(Debug, Clone, PartialEq)]
882/// `MessageBusDeadLetterPage` data container.
883pub struct MessageBusDeadLetterPage<T = AnyValue> {
884    /// `entries` field for entries.
885    pub entries: Vec<MessageBusDeadLetterEntry<T>>,
886    /// `next_after_entry_seq` field for next after entry seq.
887    pub next_after_entry_seq: Option<u64>,
888    /// `has_more` field for has more.
889    pub has_more: bool,
890}
891
892#[derive(Debug, Clone, PartialEq)]
893/// `MessageBusTopicPage` data container.
894pub struct MessageBusTopicPage<T = AnyValue> {
895    /// `topic` field for topic.
896    pub topic: String,
897    /// `messages` field for messages.
898    pub messages: Vec<MessageEnvelope<T>>,
899    /// `from_seq` field for from seq.
900    pub from_seq: u64,
901    /// `through_seq` field for through seq.
902    pub through_seq: Option<u64>,
903    /// `next_after_seq` field for next after seq.
904    pub next_after_seq: Option<u64>,
905    /// `has_more` field for has more.
906    pub has_more: bool,
907}
908
909#[derive(Debug, Clone, PartialEq, Eq)]
910/// `MessageBusCursor` data container.
911pub struct MessageBusCursor {
912    /// `topic` field for topic.
913    pub topic: String,
914    /// `subscription_id` field for subscription id.
915    pub subscription_id: String,
916    /// `next_seq` field for next seq.
917    pub next_seq: u64,
918    /// `closed` field for closed.
919    pub closed: bool,
920    /// `retention_gap` field for retention gap.
921    pub retention_gap: bool,
922    /// `head_seq` field for head seq.
923    pub head_seq: u64,
924}
925
926#[derive(Debug, Clone, PartialEq)]
927/// `MessageBusAvailablePage` data container.
928pub struct MessageBusAvailablePage<T = AnyValue> {
929    /// `topic` field for topic.
930    pub topic: String,
931    /// `subscription_id` field for subscription id.
932    pub subscription_id: String,
933    /// `cursor` field for cursor.
934    pub cursor: MessageBusCursor,
935    /// `messages` field for messages.
936    pub messages: Vec<MessageEnvelope<T>>,
937    /// `from_seq` field for from seq.
938    pub from_seq: u64,
939    /// `through_seq` field for through seq.
940    pub through_seq: Option<u64>,
941    /// `next_after_seq` field for next after seq.
942    pub next_after_seq: Option<u64>,
943    /// `has_more` field for has more.
944    pub has_more: bool,
945}
946
947#[derive(Debug, Clone, PartialEq, Eq)]
948/// `MessageBusCatalogParams` data container.
949pub struct MessageBusCatalogParams {
950    /// `limit` field for limit.
951    pub limit: Option<usize>,
952    /// `after_topic` field for after topic.
953    pub after_topic: Option<String>,
954    /// `include_closed` field for include closed.
955    pub include_closed: bool,
956}
957
958#[derive(Debug, Clone, PartialEq, Eq)]
959/// `MessageBusDeadLetterParams` data container.
960pub struct MessageBusDeadLetterParams {
961    /// `limit` field for limit.
962    pub limit: Option<usize>,
963    /// `after_entry_seq` field for after entry seq.
964    pub after_entry_seq: Option<u64>,
965    /// `topic` field for topic.
966    pub topic: Option<String>,
967    /// `code` field for code.
968    pub code: Option<String>,
969}
970
971#[derive(Debug, Clone, PartialEq, Eq)]
972/// `MessageBusTopicParams` data container.
973pub struct MessageBusTopicParams {
974    /// `limit` field for limit.
975    pub limit: Option<usize>,
976    /// `after_seq` field for after seq.
977    pub after_seq: Option<u64>,
978}
979
980#[derive(Debug, Clone, PartialEq, Eq)]
981/// `MessageBusAvailableParams` data container.
982pub struct MessageBusAvailableParams {
983    /// `limit` field for limit.
984    pub limit: Option<usize>,
985    /// `after_seq` field for after seq.
986    pub after_seq: Option<u64>,
987}
988
989#[derive(Clone)]
990/// `MessageBusPullProjection` data container.
991pub struct MessageBusPullProjection<TPage> {
992    /// `snapshot` field for snapshot.
993    pub snapshot: Node<TPage>,
994    /// `snapshot_pull_id` field for snapshot pull id.
995    pub snapshot_pull_id: LockId,
996    /// `status` field for status.
997    pub status: Node<MessageBusStatus>,
998    /// `issues` field for issues.
999    pub issues: Node<DataIssue>,
1000}
1001
1002/// `MessageBusTopicProjection` type alias.
1003pub type MessageBusTopicProjection<T = AnyValue> = MessageBusPullProjection<MessageBusTopicPage<T>>;
1004
1005#[derive(Clone)]
1006/// `MessageBusSubscription` data container.
1007pub struct MessageBusSubscription<T = AnyValue> {
1008    /// `available` field for available.
1009    pub available: Node<MessageBusAvailablePage<T>>,
1010    /// `available_pull_id` field for available pull id.
1011    pub available_pull_id: LockId,
1012    /// `cursor` field for cursor.
1013    pub cursor: Node<MessageBusCursor>,
1014    /// `status` field for status.
1015    pub status: Node<MessageBusStatus>,
1016    /// `issues` field for issues.
1017    pub issues: Node<DataIssue>,
1018    commands: Node<MessageBusCommand<T>>,
1019    topic: String,
1020    subscription_id: String,
1021}
1022
1023#[derive(Clone)]
1024/// `MessageBus` data container.
1025pub struct MessageBus<T = AnyValue> {
1026    graph: Graph,
1027    name: Rc<String>,
1028    /// `commands` field for commands.
1029    pub commands: Node<MessageBusCommand<T>>,
1030    /// `messages` field for messages.
1031    pub messages: Node<MessageEnvelope<T>>,
1032    /// `status` field for status.
1033    pub status: Node<MessageBusStatus>,
1034    /// `issues` field for issues.
1035    pub issues: Node<DataIssue>,
1036    state: Rc<RefCell<MessageBusState<T>>>,
1037    command_sources: Rc<RefCell<Vec<Core>>>,
1038    _runtime_retain: Rc<RetainNode>,
1039}
1040
1041#[derive(Clone)]
1042/// `ToTopicBundle` data container.
1043pub struct ToTopicBundle<T> {
1044    /// `commands` field for commands.
1045    pub commands: Node<MessageBusCommand<T>>,
1046}
1047
1048struct RetainNode {
1049    release: RefCell<Option<Box<dyn FnOnce()>>>,
1050}
1051
1052impl RetainNode {
1053    fn new(release: Box<dyn FnOnce()>) -> Self {
1054        Self {
1055            release: RefCell::new(Some(release)),
1056        }
1057    }
1058}
1059
1060impl Drop for RetainNode {
1061    fn drop(&mut self) {
1062        if let Some(release) = self.release.borrow_mut().take() {
1063            release();
1064        }
1065    }
1066}
1067
1068#[derive(Clone)]
1069struct TopicState<T> {
1070    closed: bool,
1071    head_seq: u64,
1072    next_seq: u64,
1073    messages: Vec<MessageEnvelope<T>>,
1074}
1075
1076#[derive(Debug, Clone, PartialEq, Eq)]
1077struct SubscriptionState {
1078    topic: String,
1079    subscription_id: String,
1080    next_seq: u64,
1081    closed: bool,
1082    retention_gap: bool,
1083}
1084
1085struct MessageBusState<T = AnyValue> {
1086    now: Rc<dyn Fn() -> u64>,
1087    topic_policy: MessageBusTopicPolicy,
1088    retention: MessageBusRetentionPolicy,
1089    dedupe: MessageBusDedupePolicy,
1090    topics: BTreeMap<String, TopicState<T>>,
1091    subscriptions: BTreeMap<String, SubscriptionState>,
1092    seen_command_ids: HashSet<String>,
1093    seen_idempotency_keys: HashSet<String>,
1094    dead_letters: Vec<MessageBusDeadLetterEntry<T>>,
1095    dead_letter_seq: u64,
1096}
1097
1098#[derive(Clone)]
1099enum RuntimeEvent<T = AnyValue> {
1100    Message(MessageEnvelope<T>),
1101    Status(MessageBusStatus),
1102    Issue(DataIssue),
1103}
1104
1105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1106/// `MessageBusSubscriptionFrom` variants.
1107pub enum MessageBusSubscriptionFrom {
1108    /// `Earliest` variant.
1109    Earliest,
1110    /// `Latest` variant.
1111    Latest,
1112    /// `Seq` variant.
1113    Seq(u64),
1114}
1115
1116#[derive(Clone)]
1117/// `MessageBusSubscriptionOptions` data container.
1118pub struct MessageBusSubscriptionOptions {
1119    /// `topic` field for topic.
1120    pub topic: String,
1121    /// `subscription_id` field for subscription id.
1122    pub subscription_id: String,
1123    /// `from` field for from.
1124    pub from: MessageBusSubscriptionFrom,
1125    /// `name` field for name.
1126    pub name: Option<String>,
1127}
1128
1129impl MessageBusSubscriptionOptions {
1130    /// Creates or computes `new`.
1131    pub fn new(topic: impl Into<String>, subscription_id: impl Into<String>) -> Self {
1132        Self {
1133            topic: topic.into(),
1134            subscription_id: subscription_id.into(),
1135            from: MessageBusSubscriptionFrom::Earliest,
1136            name: None,
1137        }
1138    }
1139
1140    /// Updates or reads `from`.
1141    pub fn from(mut self, from: MessageBusSubscriptionFrom) -> Self {
1142        self.from = from;
1143        self
1144    }
1145
1146    /// Updates or reads `named`.
1147    pub fn named(mut self, name: impl Into<String>) -> Self {
1148        self.name = Some(name.into());
1149        self
1150    }
1151}
1152
1153impl<T: Clone + 'static> MessageBus<T> {
1154    /// Creates or computes `new`.
1155    pub fn new(graph: &Graph, topics: impl IntoIterator<Item = impl Into<String>>) -> Self {
1156        Self::with_options(graph, MessageBusOptions::default().with_topics(topics))
1157    }
1158
1159    /// Creates or computes `with_options`.
1160    pub fn with_options(graph: &Graph, opts: MessageBusOptions) -> Self {
1161        let name = Rc::new(opts.name);
1162        let command_sources = Rc::new(RefCell::new(Vec::new()));
1163        let commands = graph.node_opts::<MessageBusCommand<T>, _>(
1164            Vec::new(),
1165            message_bus_command_body::<T>(0),
1166            node_opts(format!("{name}/commands"), "messageBusCommands"),
1167        );
1168        let initial_topics = unique_topics(opts.topics);
1169        let state = Rc::new(RefCell::new(MessageBusState {
1170            now: opts.now,
1171            topic_policy: opts.topic_policy,
1172            retention: opts.retention,
1173            dedupe: opts.dedupe,
1174            topics: initial_topics
1175                .into_iter()
1176                .map(|topic| (topic, make_topic_state()))
1177                .collect(),
1178            subscriptions: BTreeMap::new(),
1179            seen_command_ids: HashSet::new(),
1180            seen_idempotency_keys: HashSet::new(),
1181            dead_letters: Vec::new(),
1182            dead_letter_seq: 0,
1183        }));
1184        let runtime_state = state.clone();
1185        let runtime = graph.node_opts::<RuntimeEvent<T>, _>(
1186            vec![commands.erased()],
1187            move |ctx| {
1188                for command in ctx.batch::<MessageBusCommand<T>>(0) {
1189                    let events = {
1190                        let mut state = runtime_state.borrow_mut();
1191                        reduce_message_bus_command(&mut state, (*command).clone())
1192                    };
1193                    for event in events {
1194                        ctx.emit(event);
1195                    }
1196                }
1197            },
1198            node_opts(format!("{name}/runtime"), "messageBusRuntime"),
1199        );
1200        let runtime_retain = Rc::new(RetainNode::new(
1201            graph.retain(&runtime, &format!("{name}.messageBus.runtime")),
1202        ));
1203        let messages = graph.node_opts::<MessageEnvelope<T>, _>(
1204            vec![runtime.erased()],
1205            move |ctx| {
1206                for event in ctx.batch::<RuntimeEvent<T>>(0) {
1207                    if let RuntimeEvent::Message(message) = event.as_ref() {
1208                        ctx.emit(message.clone());
1209                    }
1210                }
1211            },
1212            node_opts(format!("{name}/messages"), "messageBusMessages"),
1213        );
1214        let status = graph.node_opts::<MessageBusStatus, _>(
1215            vec![runtime.erased()],
1216            move |ctx| {
1217                for event in ctx.batch::<RuntimeEvent<T>>(0) {
1218                    if let RuntimeEvent::Status(status) = event.as_ref() {
1219                        ctx.emit(status.clone());
1220                    }
1221                }
1222            },
1223            node_opts(format!("{name}/status"), "messageBusStatus"),
1224        );
1225        let issues = graph.node_opts::<DataIssue, _>(
1226            vec![runtime.erased()],
1227            move |ctx| {
1228                for event in ctx.batch::<RuntimeEvent<T>>(0) {
1229                    if let RuntimeEvent::Issue(issue) = event.as_ref() {
1230                        ctx.emit(issue.clone());
1231                    }
1232                }
1233            },
1234            node_opts(format!("{name}/issues"), "messageBusIssues"),
1235        );
1236        Self {
1237            graph: graph.clone(),
1238            name,
1239            commands,
1240            messages,
1241            status,
1242            issues,
1243            state,
1244            command_sources,
1245            _runtime_retain: runtime_retain,
1246        }
1247    }
1248
1249    /// Updates or reads `ensure_topic`.
1250    pub fn ensure_topic(
1251        &self,
1252        topic: impl Into<String>,
1253        command_id: Option<String>,
1254    ) -> MessageBusCommand<T> {
1255        self.publish_command(MessageBusCommand::EnsureTopic {
1256            topic: topic.into(),
1257            command_id,
1258        })
1259    }
1260
1261    /// Updates or reads `close_topic`.
1262    pub fn close_topic(
1263        &self,
1264        topic: impl Into<String>,
1265        command_id: Option<String>,
1266    ) -> MessageBusCommand<T> {
1267        self.publish_command(MessageBusCommand::CloseTopic {
1268            topic: topic.into(),
1269            command_id,
1270        })
1271    }
1272
1273    /// Updates or reads `publish`.
1274    pub fn publish(
1275        &self,
1276        topic: impl Into<String>,
1277        payload: T,
1278        key: Option<String>,
1279        command_id: Option<String>,
1280        idempotency_key: Option<String>,
1281    ) -> MessageBusCommand<T> {
1282        self.publish_command(MessageBusCommand::Publish {
1283            topic: topic.into(),
1284            payload,
1285            key,
1286            command_id,
1287            idempotency_key,
1288        })
1289    }
1290
1291    /// Updates or reads `set_topic_policy`.
1292    pub fn set_topic_policy(
1293        &self,
1294        topic_policy: MessageBusTopicPolicy,
1295        command_id: Option<String>,
1296    ) -> MessageBusCommand<T> {
1297        self.publish_command(MessageBusCommand::TopicPolicy {
1298            topic_policy,
1299            command_id,
1300        })
1301    }
1302
1303    /// Updates or reads `topic`.
1304    pub fn topic(&self, topic: impl Into<String>) -> MessageBusTopicProjection<T> {
1305        self.topic_named(topic, None::<String>)
1306    }
1307
1308    /// Updates or reads `topic_named`.
1309    pub fn topic_named(
1310        &self,
1311        topic: impl Into<String>,
1312        name: Option<impl Into<String>>,
1313    ) -> MessageBusTopicProjection<T> {
1314        let topic = topic.into();
1315        assert_topic_key(&topic, "messageBus.topic");
1316        let snapshot_pull_id = LockId::new(format!("{}/{topic}/topicSnapshot", self.name));
1317        let state = self.state.clone();
1318        let topic_for_fn = topic.clone();
1319        let snapshot = self.graph.node_opts::<MessageBusTopicPage<T>, _>(
1320            vec![self.messages.erased()],
1321            move |ctx| {
1322                let params =
1323                    pull_params::<MessageBusTopicParams>(ctx).unwrap_or(MessageBusTopicParams {
1324                        limit: None,
1325                        after_seq: None,
1326                    });
1327                ctx.emit(topic_page(&state.borrow(), &topic_for_fn, &params));
1328            },
1329            pull_node_opts(
1330                name.map(Into::into)
1331                    .unwrap_or_else(|| format!("{}/{topic}/topic", self.name)),
1332                "messageBusTopicProjection",
1333                snapshot_pull_id.clone(),
1334            ),
1335        );
1336        MessageBusPullProjection {
1337            snapshot,
1338            snapshot_pull_id,
1339            status: self.status.clone(),
1340            issues: self.issues.clone(),
1341        }
1342    }
1343
1344    /// Updates or reads `catalog`.
1345    pub fn catalog(&self) -> MessageBusPullProjection<MessageBusCatalogPage> {
1346        self.catalog_named(None::<String>)
1347    }
1348
1349    /// Updates or reads `catalog_named`.
1350    pub fn catalog_named(
1351        &self,
1352        name: Option<impl Into<String>>,
1353    ) -> MessageBusPullProjection<MessageBusCatalogPage> {
1354        let snapshot_pull_id = LockId::new(format!("{}/catalogSnapshot", self.name));
1355        let state = self.state.clone();
1356        let snapshot = self.graph.node_opts::<MessageBusCatalogPage, _>(
1357            vec![self.status.erased()],
1358            move |ctx| {
1359                let params = pull_params::<MessageBusCatalogParams>(ctx).unwrap_or(
1360                    MessageBusCatalogParams {
1361                        limit: None,
1362                        after_topic: None,
1363                        include_closed: false,
1364                    },
1365                );
1366                ctx.emit(catalog_page(&state.borrow(), &params));
1367            },
1368            pull_node_opts(
1369                name.map(Into::into)
1370                    .unwrap_or_else(|| format!("{}/catalog", self.name)),
1371                "messageBusCatalog",
1372                snapshot_pull_id.clone(),
1373            ),
1374        );
1375        MessageBusPullProjection {
1376            snapshot,
1377            snapshot_pull_id,
1378            status: self.status.clone(),
1379            issues: self.issues.clone(),
1380        }
1381    }
1382
1383    /// Updates or reads `dead_letter`.
1384    pub fn dead_letter(&self) -> MessageBusPullProjection<MessageBusDeadLetterPage<T>> {
1385        self.dead_letter_named(None::<String>)
1386    }
1387
1388    /// Updates or reads `dead_letter_named`.
1389    pub fn dead_letter_named(
1390        &self,
1391        name: Option<impl Into<String>>,
1392    ) -> MessageBusPullProjection<MessageBusDeadLetterPage<T>> {
1393        let snapshot_pull_id = LockId::new(format!("{}/deadLetterSnapshot", self.name));
1394        let state = self.state.clone();
1395        let snapshot = self.graph.node_opts::<MessageBusDeadLetterPage<T>, _>(
1396            vec![self.issues.erased(), self.status.erased()],
1397            move |ctx| {
1398                let params = pull_params::<MessageBusDeadLetterParams>(ctx).unwrap_or(
1399                    MessageBusDeadLetterParams {
1400                        limit: None,
1401                        after_entry_seq: None,
1402                        topic: None,
1403                        code: None,
1404                    },
1405                );
1406                ctx.emit(dead_letter_page(&state.borrow(), &params));
1407            },
1408            pull_node_opts(
1409                name.map(Into::into)
1410                    .unwrap_or_else(|| format!("{}/deadLetter", self.name)),
1411                "messageBusDeadLetter",
1412                snapshot_pull_id.clone(),
1413            ),
1414        );
1415        MessageBusPullProjection {
1416            snapshot,
1417            snapshot_pull_id,
1418            status: self.status.clone(),
1419            issues: self.issues.clone(),
1420        }
1421    }
1422
1423    /// Updates or reads `subscription`.
1424    pub fn subscription(&self, opts: MessageBusSubscriptionOptions) -> MessageBusSubscription<T> {
1425        assert_topic_key(&opts.topic, "messageBus.subscription");
1426        assert_non_empty(&opts.subscription_id, "messageBus.subscriptionId");
1427        let (sub, opened, issue) = ensure_subscription(&mut self.state.borrow_mut(), &opts);
1428        let available_pull_id = LockId::new(format!(
1429            "{}/{}/{}/available",
1430            self.name, opts.topic, opts.subscription_id
1431        ));
1432        let projection_name = opts
1433            .name
1434            .clone()
1435            .unwrap_or_else(|| format!("{}/{}/{}", self.name, opts.topic, opts.subscription_id));
1436        let available_state = self.state.clone();
1437        let available_sub = sub.clone();
1438        let available = self.graph.node_opts::<MessageBusAvailablePage<T>, _>(
1439            vec![self.messages.erased(), self.status.erased()],
1440            move |ctx| {
1441                let params = pull_params::<MessageBusAvailableParams>(ctx).unwrap_or(
1442                    MessageBusAvailableParams {
1443                        limit: None,
1444                        after_seq: None,
1445                    },
1446                );
1447                ctx.emit(available_page(
1448                    &available_state.borrow(),
1449                    &available_sub,
1450                    &params,
1451                ));
1452            },
1453            pull_node_opts(
1454                format!("{projection_name}/available"),
1455                "messageBusSubscriptionAvailable",
1456                available_pull_id.clone(),
1457            ),
1458        );
1459        let cursor_state = self.state.clone();
1460        let cursor_sub = sub.clone();
1461        let cursor = self.graph.node_opts::<MessageBusCursor, _>(
1462            vec![self.status.erased()],
1463            move |ctx| {
1464                for status in ctx.batch::<MessageBusStatus>(0) {
1465                    let subscription_moved = status.topic.as_deref()
1466                        == Some(cursor_sub.topic.as_str())
1467                        && status.subscription_id.as_deref()
1468                            == Some(cursor_sub.subscription_id.as_str())
1469                        && matches!(
1470                            status.kind,
1471                            MessageBusStatusKind::SubscriptionOpened
1472                                | MessageBusStatusKind::SubscriptionAcked
1473                                | MessageBusStatusKind::SubscriptionSought
1474                                | MessageBusStatusKind::SubscriptionClosed
1475                        );
1476                    let retention_changed = status.topic.as_deref()
1477                        == Some(cursor_sub.topic.as_str())
1478                        && status.kind == MessageBusStatusKind::RetentionTrimmed;
1479                    if subscription_moved || retention_changed {
1480                        ctx.emit(cursor_snapshot(&cursor_state.borrow(), &cursor_sub));
1481                    }
1482                }
1483            },
1484            node_opts(
1485                format!("{projection_name}/cursor"),
1486                "messageBusSubscriptionCursor",
1487            ),
1488        );
1489        if opened {
1490            let opened_status = status_fact(
1491                &self.state.borrow(),
1492                MessageBusStatusKind::SubscriptionOpened,
1493                StatusFields {
1494                    topic: Some(sub.topic.clone()),
1495                    subscription_id: Some(sub.subscription_id.clone()),
1496                    next_seq: Some(sub.next_seq),
1497                    details: Some(format!("from={:?}", opts.from)),
1498                    ..StatusFields::default()
1499                },
1500            );
1501            self.status.set(opened_status);
1502        }
1503        if let Some(issue) = issue {
1504            self.issues.set(issue);
1505        }
1506        MessageBusSubscription {
1507            available,
1508            available_pull_id,
1509            cursor,
1510            status: self.status.clone(),
1511            issues: self.issues.clone(),
1512            commands: self.commands.clone(),
1513            topic: sub.topic,
1514            subscription_id: sub.subscription_id,
1515        }
1516    }
1517
1518    fn publish_command(&self, command: MessageBusCommand<T>) -> MessageBusCommand<T> {
1519        self.commands.set(command.clone());
1520        command
1521    }
1522}
1523
1524impl<T: Clone + 'static> MessageBusSubscription<T> {
1525    /// Updates or reads `ack`.
1526    pub fn ack(&self, seq: u64, command_id: Option<String>) -> MessageBusCommand<T> {
1527        let command = MessageBusCommand::Ack {
1528            topic: self.topic.clone(),
1529            subscription_id: self.subscription_id.clone(),
1530            seq,
1531            command_id,
1532        };
1533        self.commands.set(command.clone());
1534        command
1535    }
1536
1537    /// Updates or reads `seek`.
1538    pub fn seek(&self, next_seq: u64, command_id: Option<String>) -> MessageBusCommand<T> {
1539        let command = MessageBusCommand::Seek {
1540            topic: self.topic.clone(),
1541            subscription_id: self.subscription_id.clone(),
1542            next_seq,
1543            command_id,
1544        };
1545        self.commands.set(command.clone());
1546        command
1547    }
1548
1549    /// Updates or reads `close`.
1550    pub fn close(&self, command_id: Option<String>) -> MessageBusCommand<T> {
1551        let command = MessageBusCommand::CloseSubscription {
1552            topic: self.topic.clone(),
1553            subscription_id: self.subscription_id.clone(),
1554            command_id,
1555        };
1556        self.commands.set(command.clone());
1557        command
1558    }
1559}
1560
1561/// Creates or computes `message_bus`.
1562pub fn message_bus<T: Clone + 'static>(graph: &Graph, opts: MessageBusOptions) -> MessageBus<T> {
1563    MessageBus::with_options(graph, opts)
1564}
1565
1566/// Creates or computes `to_topic`.
1567pub fn to_topic<T: Clone + 'static>(
1568    graph: &Graph,
1569    source: &Node<T>,
1570    bus: &MessageBus<T>,
1571    topic: impl Into<String>,
1572    name: impl Into<String>,
1573) -> ToTopicBundle<T> {
1574    let topic = topic.into();
1575    assert_topic_key(&topic, "toTopic");
1576    assert!(
1577        source.erased().same_graph(&bus.commands.erased()),
1578        "toTopic: bus and source graph must match"
1579    );
1580    assert!(
1581        !is_reachable_upstream(&source.erased(), &bus.commands.erased()),
1582        "toTopic: source already depends on bus command path"
1583    );
1584    let topic_for_fn = topic.clone();
1585    let commands = graph.node_opts::<MessageBusCommand<T>, _>(
1586        vec![source.erased()],
1587        move |ctx| {
1588            for value in ctx.batch::<T>(0) {
1589                ctx.emit(MessageBusCommand::Publish {
1590                    topic: topic_for_fn.clone(),
1591                    payload: (*value).clone(),
1592                    key: None,
1593                    command_id: None,
1594                    idempotency_key: None,
1595                });
1596            }
1597        },
1598        node_opts(name.into(), "messageBusToTopic"),
1599    );
1600    let command_source = commands.erased();
1601    bus.command_sources
1602        .borrow_mut()
1603        .push(command_source.clone());
1604    let command_sources = bus.command_sources.borrow().clone();
1605    let command_source_count = command_sources.len();
1606    let rewire = catch_unwind(AssertUnwindSafe(|| {
1607        bus.commands.replace_deps(
1608            command_sources,
1609            message_bus_command_body::<T>(command_source_count),
1610        );
1611    }));
1612    if let Err(panic) = rewire {
1613        bus.command_sources
1614            .borrow_mut()
1615            .retain(|source| !source.ptr_eq(&command_source));
1616        graph.release_nodes(&[commands.erased()], "toTopic failed command wiring");
1617        resume_unwind(panic);
1618    }
1619    ToTopicBundle { commands }
1620}
1621
1622pub(crate) fn attach_message_bus_deferred_command_sink<T: Clone + 'static>(
1623    graph: &Graph,
1624    bus: &MessageBus<T>,
1625    commands: &Node<MessageBusCommand<T>>,
1626) -> Box<dyn FnOnce()> {
1627    assert!(
1628        commands.erased().same_graph(&bus.commands.erased()),
1629        "messageBus: command sink graph must match"
1630    );
1631    let _ = graph;
1632    let bus_commands = bus.commands.erased();
1633    commands.subscribe(move |msg| {
1634        if let crate::protocol::Message::Data(value) = msg {
1635            if let Ok(command) = value.clone().downcast::<MessageBusCommand<T>>() {
1636                bus_commands.request_down_next(vec![crate::protocol::Message::Data(Rc::new(
1637                    (*command).clone(),
1638                ))]);
1639            }
1640        }
1641    })
1642}
1643
1644fn message_bus_command_body<T: Clone + 'static>(
1645    command_source_count: usize,
1646) -> impl Fn(&Ctx) + 'static {
1647    move |ctx| {
1648        for index in 0..command_source_count {
1649            for command in ctx.batch::<MessageBusCommand<T>>(index) {
1650                ctx.emit((*command).clone());
1651            }
1652        }
1653    }
1654}
1655
1656fn reduce_message_bus_command<T: Clone + 'static>(
1657    state: &mut MessageBusState<T>,
1658    command: MessageBusCommand<T>,
1659) -> Vec<RuntimeEvent<T>> {
1660    if let Some(topic) = command.topic() {
1661        if let Some(error) = validate_topic_key(topic, "messageBus") {
1662            return reject_command(state, command, error);
1663        }
1664    }
1665    if let Some(subscription_id) = command_subscription_id(&command) {
1666        if subscription_id.is_empty() {
1667            return reject_command(
1668                state,
1669                command,
1670                "subscriptionId must be a non-empty string".to_owned(),
1671            );
1672        }
1673    }
1674    if let Some(command_id) = command.command_id() {
1675        if state.seen_command_ids.contains(command_id) {
1676            return duplicate_command_events(state, command, "duplicate commandId");
1677        }
1678        state.seen_command_ids.insert(command_id.to_owned());
1679    }
1680    if let MessageBusCommand::Publish {
1681        topic,
1682        idempotency_key: Some(idempotency_key),
1683        ..
1684    } = &command
1685    {
1686        if state
1687            .seen_idempotency_keys
1688            .contains(&idempotency_key_for(topic, idempotency_key))
1689        {
1690            return duplicate_command_events(state, command, "duplicate idempotencyKey");
1691        }
1692    }
1693    match command {
1694        MessageBusCommand::EnsureTopic { topic, command_id } => {
1695            ensure_topic(state, topic, command_id)
1696        }
1697        MessageBusCommand::CloseTopic { topic, command_id } => {
1698            close_topic(state, topic, command_id)
1699        }
1700        MessageBusCommand::TopicPolicy {
1701            topic_policy,
1702            command_id,
1703        } => {
1704            state.topic_policy = topic_policy;
1705            let _ = command_id;
1706            Vec::new()
1707        }
1708        MessageBusCommand::Publish { .. } => publish_message(state, command),
1709        MessageBusCommand::Ack { .. } => ack_subscription(state, command),
1710        MessageBusCommand::Seek { .. } => seek_subscription(state, command),
1711        MessageBusCommand::CloseSubscription { .. } => close_subscription(state, command),
1712    }
1713}
1714
1715fn publish_message<T: Clone + 'static>(
1716    state: &mut MessageBusState<T>,
1717    command: MessageBusCommand<T>,
1718) -> Vec<RuntimeEvent<T>> {
1719    let MessageBusCommand::Publish {
1720        topic,
1721        payload,
1722        key,
1723        command_id,
1724        idempotency_key,
1725    } = command
1726    else {
1727        return Vec::new();
1728    };
1729    let mut events = Vec::new();
1730    if !state.topics.contains_key(&topic) {
1731        if state.topic_policy != MessageBusTopicPolicy::CreateAsFact {
1732            return issue_events(
1733                state,
1734                Some(MessageBusCommand::Publish {
1735                    topic,
1736                    payload,
1737                    key,
1738                    command_id,
1739                    idempotency_key,
1740                }),
1741                None,
1742                "unknown-topic",
1743                "unknown topic",
1744            );
1745        }
1746        events.extend(ensure_topic(state, topic.clone(), command_id.clone()));
1747    }
1748    let Some(topic_state) = state.topics.get(&topic) else {
1749        return issue_events::<T>(state, None, None, "unknown-topic", "unknown topic");
1750    };
1751    if topic_state.closed {
1752        return issue_events(
1753            state,
1754            Some(MessageBusCommand::Publish {
1755                topic,
1756                payload,
1757                key,
1758                command_id,
1759                idempotency_key,
1760            }),
1761            None,
1762            "closed-topic",
1763            "closed topic",
1764        );
1765    }
1766    let timestamp_ms = timestamp_or_zero(state);
1767    let topic_state = state
1768        .topics
1769        .get_mut(&topic)
1770        .expect("topic checked immediately above");
1771    let message = MessageEnvelope {
1772        topic: topic.clone(),
1773        seq: topic_state.next_seq,
1774        payload,
1775        key,
1776        timestamp_ms,
1777        command_id: command_id.clone(),
1778        idempotency_key: idempotency_key.clone(),
1779    };
1780    topic_state.next_seq += 1;
1781    if let Some(idempotency_key) = idempotency_key {
1782        state
1783            .seen_idempotency_keys
1784            .insert(idempotency_key_for(&topic, &idempotency_key));
1785    }
1786    topic_state.messages.push(message.clone());
1787    events.extend([
1788        RuntimeEvent::Message(message.clone()),
1789        RuntimeEvent::Status(status_fact(
1790            state,
1791            MessageBusStatusKind::MessagePublished,
1792            StatusFields {
1793                topic: Some(topic.clone()),
1794                seq: Some(message.seq),
1795                command_id,
1796                ..StatusFields::default()
1797            },
1798        )),
1799    ]);
1800    events.extend(trim_retention(state, &topic));
1801    events
1802}
1803
1804fn ensure_topic<T: Clone + 'static>(
1805    state: &mut MessageBusState<T>,
1806    topic: String,
1807    command_id: Option<String>,
1808) -> Vec<RuntimeEvent<T>> {
1809    state
1810        .topics
1811        .entry(topic.clone())
1812        .or_insert_with(make_topic_state);
1813    vec![RuntimeEvent::Status(status_fact(
1814        state,
1815        MessageBusStatusKind::TopicCreated,
1816        StatusFields {
1817            topic: Some(topic),
1818            command_id,
1819            ..StatusFields::default()
1820        },
1821    ))]
1822}
1823
1824fn close_topic<T: Clone + 'static>(
1825    state: &mut MessageBusState<T>,
1826    topic: String,
1827    command_id: Option<String>,
1828) -> Vec<RuntimeEvent<T>> {
1829    let Some(topic_state) = state.topics.get_mut(&topic) else {
1830        return issue_events(
1831            state,
1832            Some(MessageBusCommand::CloseTopic { topic, command_id }),
1833            None,
1834            "unknown-topic",
1835            "unknown topic",
1836        );
1837    };
1838    topic_state.closed = true;
1839    vec![RuntimeEvent::Status(status_fact(
1840        state,
1841        MessageBusStatusKind::TopicClosed,
1842        StatusFields {
1843            topic: Some(topic),
1844            command_id,
1845            ..StatusFields::default()
1846        },
1847    ))]
1848}
1849
1850fn trim_retention<T: Clone + 'static>(
1851    state: &mut MessageBusState<T>,
1852    topic_name: &str,
1853) -> Vec<RuntimeEvent<T>> {
1854    let now = timestamp_or_zero(state);
1855    let Some(topic) = state.topics.get_mut(topic_name) else {
1856        return Vec::new();
1857    };
1858    let before_head = topic.head_seq;
1859    if let Some(max_age_ms) = state.retention.max_age_ms {
1860        topic
1861            .messages
1862            .retain(|message| now.saturating_sub(message.timestamp_ms) <= max_age_ms);
1863    }
1864    if let Some(max_messages) = state.retention.max_messages {
1865        if max_messages == 0 {
1866            return issue_events::<T>(
1867                state,
1868                None,
1869                None,
1870                "policy-rejected",
1871                "retention.maxMessages must be positive",
1872            );
1873        }
1874        if topic.messages.len() > max_messages {
1875            let trim_count = topic.messages.len() - max_messages;
1876            topic.messages.drain(0..trim_count);
1877        }
1878    }
1879    topic.head_seq = topic
1880        .messages
1881        .first()
1882        .map_or(topic.next_seq, |message| message.seq);
1883    if topic.head_seq == before_head {
1884        return Vec::new();
1885    }
1886    let head_seq = topic.head_seq;
1887    let trim_count = head_seq.saturating_sub(before_head);
1888    let mut events = vec![RuntimeEvent::Status(status_fact(
1889        state,
1890        MessageBusStatusKind::RetentionTrimmed,
1891        StatusFields {
1892            topic: Some(topic_name.to_owned()),
1893            head_seq: Some(head_seq),
1894            details: Some(format!("trimCount={trim_count}")),
1895            ..StatusFields::default()
1896        },
1897    ))];
1898    let affected = state
1899        .subscriptions
1900        .values_mut()
1901        .filter(|sub| {
1902            !sub.closed && sub.topic == topic_name && sub.next_seq < head_seq && !sub.retention_gap
1903        })
1904        .map(|sub| {
1905            sub.retention_gap = true;
1906            sub.subscription_id.clone()
1907        })
1908        .collect::<Vec<_>>();
1909    for subscription_id in affected {
1910        events.extend(issue_events::<T>(
1911            state,
1912            None,
1913            None,
1914            "retention-gap",
1915            format!("subscription '{subscription_id}' is before retained headSeq"),
1916        ));
1917    }
1918    events
1919}
1920
1921fn ack_subscription<T: Clone + 'static>(
1922    state: &mut MessageBusState<T>,
1923    command: MessageBusCommand<T>,
1924) -> Vec<RuntimeEvent<T>> {
1925    let MessageBusCommand::Ack {
1926        topic,
1927        subscription_id,
1928        seq,
1929        command_id,
1930    } = command
1931    else {
1932        return Vec::new();
1933    };
1934    let Some(topic_state) = state.topics.get(&topic) else {
1935        return issue_events::<T>(state, None, None, "unknown-topic", "unknown topic");
1936    };
1937    let key = subscription_key(&topic, &subscription_id);
1938    let Some(sub) = state.subscriptions.get_mut(&key) else {
1939        return issue_events::<T>(
1940            state,
1941            None,
1942            None,
1943            "unknown-subscription",
1944            "unknown subscription",
1945        );
1946    };
1947    if sub.closed {
1948        return issue_events::<T>(
1949            state,
1950            None,
1951            None,
1952            "subscription-closed",
1953            "subscription is closed",
1954        );
1955    }
1956    if sub.retention_gap {
1957        return issue_events::<T>(
1958            state,
1959            None,
1960            None,
1961            "retention-gap",
1962            "subscription must seek before ack",
1963        );
1964    }
1965    if seq < sub.next_seq {
1966        return issue_events::<T>(
1967            state,
1968            None,
1969            None,
1970            "source-cursor-stale",
1971            "ack is behind subscription cursor",
1972        );
1973    }
1974    if seq >= topic_state.next_seq {
1975        return issue_events::<T>(
1976            state,
1977            None,
1978            None,
1979            "cursor-out-of-range",
1980            "ack is beyond topic tail",
1981        );
1982    }
1983    sub.next_seq = seq + 1;
1984    vec![RuntimeEvent::Status(status_fact(
1985        state,
1986        MessageBusStatusKind::SubscriptionAcked,
1987        StatusFields {
1988            topic: Some(topic),
1989            subscription_id: Some(subscription_id),
1990            next_seq: Some(seq + 1),
1991            command_id,
1992            ..StatusFields::default()
1993        },
1994    ))]
1995}
1996
1997fn seek_subscription<T: Clone + 'static>(
1998    state: &mut MessageBusState<T>,
1999    command: MessageBusCommand<T>,
2000) -> Vec<RuntimeEvent<T>> {
2001    let MessageBusCommand::Seek {
2002        topic,
2003        subscription_id,
2004        next_seq,
2005        command_id,
2006    } = command
2007    else {
2008        return Vec::new();
2009    };
2010    let Some(topic_state) = state.topics.get(&topic) else {
2011        return issue_events::<T>(state, None, None, "unknown-topic", "unknown topic");
2012    };
2013    let key = subscription_key(&topic, &subscription_id);
2014    let Some(sub) = state.subscriptions.get_mut(&key) else {
2015        return issue_events::<T>(
2016            state,
2017            None,
2018            None,
2019            "unknown-subscription",
2020            "unknown subscription",
2021        );
2022    };
2023    if sub.closed {
2024        return issue_events::<T>(
2025            state,
2026            None,
2027            None,
2028            "subscription-closed",
2029            "subscription is closed",
2030        );
2031    }
2032    if next_seq < topic_state.head_seq {
2033        return issue_events::<T>(
2034            state,
2035            None,
2036            None,
2037            "retention-gap",
2038            "seek is before retained headSeq",
2039        );
2040    }
2041    if next_seq > topic_state.next_seq {
2042        return issue_events::<T>(
2043            state,
2044            None,
2045            None,
2046            "cursor-out-of-range",
2047            "seek is beyond topic tail",
2048        );
2049    }
2050    sub.next_seq = next_seq;
2051    sub.retention_gap = false;
2052    vec![RuntimeEvent::Status(status_fact(
2053        state,
2054        MessageBusStatusKind::SubscriptionSought,
2055        StatusFields {
2056            topic: Some(topic),
2057            subscription_id: Some(subscription_id),
2058            next_seq: Some(next_seq),
2059            command_id,
2060            ..StatusFields::default()
2061        },
2062    ))]
2063}
2064
2065fn close_subscription<T: Clone + 'static>(
2066    state: &mut MessageBusState<T>,
2067    command: MessageBusCommand<T>,
2068) -> Vec<RuntimeEvent<T>> {
2069    let MessageBusCommand::CloseSubscription {
2070        topic,
2071        subscription_id,
2072        command_id,
2073    } = command
2074    else {
2075        return Vec::new();
2076    };
2077    if !state.topics.contains_key(&topic) {
2078        return issue_events::<T>(state, None, None, "unknown-topic", "unknown topic");
2079    }
2080    let key = subscription_key(&topic, &subscription_id);
2081    let Some(sub) = state.subscriptions.get_mut(&key) else {
2082        return issue_events::<T>(
2083            state,
2084            None,
2085            None,
2086            "unknown-subscription",
2087            "unknown subscription",
2088        );
2089    };
2090    sub.closed = true;
2091    let next_seq = sub.next_seq;
2092    vec![RuntimeEvent::Status(status_fact(
2093        state,
2094        MessageBusStatusKind::SubscriptionClosed,
2095        StatusFields {
2096            topic: Some(topic),
2097            subscription_id: Some(subscription_id),
2098            next_seq: Some(next_seq),
2099            command_id,
2100            ..StatusFields::default()
2101        },
2102    ))]
2103}
2104
2105fn issue_events<T: Clone + 'static>(
2106    state: &mut MessageBusState<T>,
2107    command: Option<MessageBusCommand<T>>,
2108    message: Option<MessageEnvelope<T>>,
2109    code: &str,
2110    issue_message: impl Into<String>,
2111) -> Vec<RuntimeEvent<T>> {
2112    let topic = command
2113        .as_ref()
2114        .and_then(MessageBusCommand::topic)
2115        .map(str::to_owned)
2116        .or_else(|| message.as_ref().map(|message| message.topic.clone()));
2117    let issue = DataIssue {
2118        kind: "issue".to_owned(),
2119        code: code.to_owned(),
2120        message: issue_message.into(),
2121        severity: "error".to_owned(),
2122        source: "messageBus".to_owned(),
2123        topic: topic.clone(),
2124        details: None,
2125    };
2126    let entry = MessageBusDeadLetterEntry {
2127        entry_seq: state.dead_letter_seq + 1,
2128        topic: topic.clone(),
2129        command,
2130        message,
2131        issue: issue.clone(),
2132        timestamp_ms: timestamp_or_zero(state),
2133    };
2134    state.dead_letter_seq = entry.entry_seq;
2135    state.dead_letters.push(entry);
2136    vec![RuntimeEvent::Issue(issue)]
2137}
2138
2139fn reject_command<T: Clone + 'static>(
2140    state: &mut MessageBusState<T>,
2141    command: MessageBusCommand<T>,
2142    reason: String,
2143) -> Vec<RuntimeEvent<T>> {
2144    issue_events(state, Some(command), None, "malformed-command", reason)
2145}
2146
2147fn duplicate_command_events<T: Clone + 'static>(
2148    state: &mut MessageBusState<T>,
2149    command: MessageBusCommand<T>,
2150    message: &str,
2151) -> Vec<RuntimeEvent<T>> {
2152    let status = RuntimeEvent::Status(status_fact(
2153        state,
2154        MessageBusStatusKind::DuplicateCommand,
2155        StatusFields {
2156            topic: command.topic().map(str::to_owned),
2157            command_id: command.command_id().map(str::to_owned),
2158            ..StatusFields::default()
2159        },
2160    ));
2161    if state.dedupe.command_id == MessageBusDedupeAction::Issue {
2162        return issue_events(state, Some(command), None, "duplicate-command", message);
2163    }
2164    vec![status]
2165}
2166
2167#[derive(Default)]
2168struct StatusFields {
2169    topic: Option<String>,
2170    seq: Option<u64>,
2171    head_seq: Option<u64>,
2172    subscription_id: Option<String>,
2173    next_seq: Option<u64>,
2174    command_id: Option<String>,
2175    details: Option<String>,
2176}
2177
2178fn status_fact<T>(
2179    state: &MessageBusState<T>,
2180    kind: MessageBusStatusKind,
2181    fields: StatusFields,
2182) -> MessageBusStatus {
2183    MessageBusStatus {
2184        kind,
2185        topic: fields.topic,
2186        seq: fields.seq,
2187        head_seq: fields.head_seq,
2188        subscription_id: fields.subscription_id,
2189        next_seq: fields.next_seq,
2190        command_id: fields.command_id,
2191        issue_code: None,
2192        timestamp_ms: timestamp_or_zero(state),
2193        details: fields.details,
2194    }
2195}
2196
2197fn catalog_page<T>(
2198    state: &MessageBusState<T>,
2199    params: &MessageBusCatalogParams,
2200) -> MessageBusCatalogPage {
2201    let limit = positive_limit(params.limit);
2202    let topics = state
2203        .topics
2204        .iter()
2205        .filter(|(topic, value)| {
2206            (params.include_closed || !value.closed)
2207                && params
2208                    .after_topic
2209                    .as_ref()
2210                    .is_none_or(|after| *topic > after)
2211        })
2212        .collect::<Vec<_>>();
2213    let has_more = topics.len() > limit;
2214    let page = topics
2215        .into_iter()
2216        .take(limit)
2217        .map(|(topic, value)| MessageBusCatalogEntry {
2218            topic: topic.clone(),
2219            closed: value.closed,
2220            head_seq: value.head_seq,
2221            next_seq: value.next_seq,
2222            message_count: value.messages.len(),
2223        })
2224        .collect::<Vec<_>>();
2225    let next_after_topic = if has_more {
2226        page.last().map(|entry| entry.topic.clone())
2227    } else {
2228        None
2229    };
2230    MessageBusCatalogPage {
2231        topics: page,
2232        next_after_topic,
2233        has_more,
2234    }
2235}
2236
2237fn topic_page<T: Clone>(
2238    state: &MessageBusState<T>,
2239    topic_name: &str,
2240    params: &MessageBusTopicParams,
2241) -> MessageBusTopicPage<T> {
2242    let start = params.after_seq.map_or_else(
2243        || {
2244            state
2245                .topics
2246                .get(topic_name)
2247                .map_or(1, |topic| topic.head_seq)
2248        },
2249        |seq| seq + 1,
2250    );
2251    let limit = positive_limit(params.limit);
2252    let all = state
2253        .topics
2254        .get(topic_name)
2255        .map(|topic| {
2256            topic
2257                .messages
2258                .iter()
2259                .filter(|message| message.seq >= start)
2260                .cloned()
2261                .collect::<Vec<_>>()
2262        })
2263        .unwrap_or_default();
2264    let has_more = all.len() > limit;
2265    let messages = all.into_iter().take(limit).collect::<Vec<_>>();
2266    let through_seq = messages.last().map(|message| message.seq);
2267    MessageBusTopicPage {
2268        topic: topic_name.to_owned(),
2269        messages,
2270        from_seq: start,
2271        through_seq,
2272        next_after_seq: if has_more { through_seq } else { None },
2273        has_more,
2274    }
2275}
2276
2277fn available_page<T: Clone>(
2278    state: &MessageBusState<T>,
2279    sub_key: &SubscriptionState,
2280    params: &MessageBusAvailableParams,
2281) -> MessageBusAvailablePage<T> {
2282    let key = subscription_key(&sub_key.topic, &sub_key.subscription_id);
2283    let sub = state.subscriptions.get(&key).unwrap_or(sub_key);
2284    let cursor = cursor_snapshot(state, sub);
2285    let start = params.after_seq.map_or(sub.next_seq, |seq| seq + 1);
2286    let all = if sub.retention_gap {
2287        Vec::new()
2288    } else {
2289        state
2290            .topics
2291            .get(&sub.topic)
2292            .map(|topic| {
2293                topic
2294                    .messages
2295                    .iter()
2296                    .filter(|message| message.seq >= start)
2297                    .cloned()
2298                    .collect::<Vec<_>>()
2299            })
2300            .unwrap_or_default()
2301    };
2302    let limit = positive_limit(params.limit);
2303    let has_more = all.len() > limit;
2304    let messages = all.into_iter().take(limit).collect::<Vec<_>>();
2305    let through_seq = messages.last().map(|message| message.seq);
2306    MessageBusAvailablePage {
2307        topic: sub.topic.clone(),
2308        subscription_id: sub.subscription_id.clone(),
2309        cursor,
2310        messages,
2311        from_seq: start,
2312        through_seq,
2313        next_after_seq: if has_more { through_seq } else { None },
2314        has_more,
2315    }
2316}
2317
2318fn dead_letter_page<T: Clone>(
2319    state: &MessageBusState<T>,
2320    params: &MessageBusDeadLetterParams,
2321) -> MessageBusDeadLetterPage<T> {
2322    let limit = positive_limit(params.limit);
2323    let entries = state
2324        .dead_letters
2325        .iter()
2326        .filter(|entry| {
2327            params
2328                .after_entry_seq
2329                .is_none_or(|after| entry.entry_seq > after)
2330                && params
2331                    .topic
2332                    .as_ref()
2333                    .is_none_or(|topic| entry.topic.as_ref() == Some(topic))
2334                && params
2335                    .code
2336                    .as_ref()
2337                    .is_none_or(|code| &entry.issue.code == code)
2338        })
2339        .cloned()
2340        .collect::<Vec<_>>();
2341    let has_more = entries.len() > limit;
2342    let page = entries.into_iter().take(limit).collect::<Vec<_>>();
2343    let next_after_entry_seq = if has_more {
2344        page.last().map(|entry| entry.entry_seq)
2345    } else {
2346        None
2347    };
2348    MessageBusDeadLetterPage {
2349        entries: page,
2350        next_after_entry_seq,
2351        has_more,
2352    }
2353}
2354
2355fn cursor_snapshot<T>(state: &MessageBusState<T>, sub: &SubscriptionState) -> MessageBusCursor {
2356    let key = subscription_key(&sub.topic, &sub.subscription_id);
2357    let sub = state.subscriptions.get(&key).unwrap_or(sub);
2358    MessageBusCursor {
2359        topic: sub.topic.clone(),
2360        subscription_id: sub.subscription_id.clone(),
2361        next_seq: sub.next_seq,
2362        closed: sub.closed,
2363        retention_gap: sub.retention_gap,
2364        head_seq: state
2365            .topics
2366            .get(&sub.topic)
2367            .map_or(1, |topic| topic.head_seq),
2368    }
2369}
2370
2371fn ensure_subscription<T>(
2372    state: &mut MessageBusState<T>,
2373    opts: &MessageBusSubscriptionOptions,
2374) -> (SubscriptionState, bool, Option<DataIssue>) {
2375    let key = subscription_key(&opts.topic, &opts.subscription_id);
2376    if let Some(existing) = state.subscriptions.get(&key) {
2377        return (existing.clone(), false, None);
2378    }
2379    let topic_range = state
2380        .topics
2381        .get(&opts.topic)
2382        .map(|topic| (topic.head_seq, topic.next_seq));
2383    let next_seq = match opts.from {
2384        MessageBusSubscriptionFrom::Earliest => topic_range.map_or(1, |(head_seq, _)| head_seq),
2385        MessageBusSubscriptionFrom::Latest => topic_range.map_or(1, |(_, next_seq)| next_seq),
2386        MessageBusSubscriptionFrom::Seq(seq) => seq,
2387    };
2388    let issue = topic_range.and_then(|(head_seq, tail_seq)| {
2389        if next_seq < head_seq || next_seq > tail_seq {
2390            Some(DataIssue {
2391                kind: "issue".to_owned(),
2392                code: "cursor-out-of-range".to_owned(),
2393                message: format!(
2394                    "subscription start seq {next_seq} is outside retained range {}..={}",
2395                    head_seq, tail_seq
2396                ),
2397                severity: "error".to_owned(),
2398                source: "messageBus".to_owned(),
2399                topic: Some(opts.topic.clone()),
2400                details: Some(format!("subscriptionId={}", opts.subscription_id)),
2401            })
2402        } else {
2403            None
2404        }
2405    });
2406    let next_seq = if issue.is_some() {
2407        topic_range.map_or(1, |(head_seq, _)| head_seq)
2408    } else {
2409        next_seq
2410    };
2411    if let Some(issue) = issue.clone() {
2412        let entry = MessageBusDeadLetterEntry {
2413            entry_seq: state.dead_letter_seq + 1,
2414            topic: Some(opts.topic.clone()),
2415            command: None,
2416            message: None,
2417            issue,
2418            timestamp_ms: timestamp_or_zero(state),
2419        };
2420        state.dead_letter_seq = entry.entry_seq;
2421        state.dead_letters.push(entry);
2422    }
2423    let sub = SubscriptionState {
2424        topic: opts.topic.clone(),
2425        subscription_id: opts.subscription_id.clone(),
2426        next_seq,
2427        closed: false,
2428        retention_gap: topic_range.is_some_and(|(head_seq, _)| next_seq < head_seq),
2429    };
2430    state.subscriptions.insert(key, sub.clone());
2431    (sub, true, issue)
2432}
2433
2434fn pull_params<T: Clone + 'static>(ctx: &Ctx) -> Option<T> {
2435    ctx.pull()
2436        .and_then(|pull| pull.params::<T>())
2437        .map(|params| (*params).clone())
2438}
2439
2440fn make_topic_state<T>() -> TopicState<T> {
2441    TopicState {
2442        closed: false,
2443        head_seq: 1,
2444        next_seq: 1,
2445        messages: Vec::new(),
2446    }
2447}
2448
2449fn unique_topics(topics: Vec<String>) -> Vec<String> {
2450    let mut seen = HashSet::new();
2451    let mut unique = Vec::with_capacity(topics.len());
2452    for topic in topics {
2453        assert_topic_key(&topic, "messageBus");
2454        assert!(seen.insert(topic.clone()), "messageBus: duplicate topic");
2455        unique.push(topic);
2456    }
2457    unique.sort();
2458    unique
2459}
2460
2461fn assert_topic_key(topic: &str, owner: &str) {
2462    if let Some(error) = validate_topic_key(topic, owner) {
2463        panic!("{error}");
2464    }
2465}
2466
2467fn validate_topic_key(topic: &str, owner: &str) -> Option<String> {
2468    if topic.is_empty() {
2469        return Some(format!("{owner}: topic must be a non-empty string"));
2470    }
2471    None
2472}
2473
2474fn assert_non_empty(value: &str, owner: &str) {
2475    assert!(!value.is_empty(), "{owner}: must be a non-empty string");
2476}
2477
2478fn positive_limit(limit: Option<usize>) -> usize {
2479    let limit = limit.unwrap_or(100);
2480    assert!(limit > 0, "messageBus: limit must be positive");
2481    limit
2482}
2483
2484fn command_subscription_id<T>(command: &MessageBusCommand<T>) -> Option<&str> {
2485    match command {
2486        MessageBusCommand::Ack {
2487            subscription_id, ..
2488        }
2489        | MessageBusCommand::Seek {
2490            subscription_id, ..
2491        }
2492        | MessageBusCommand::CloseSubscription {
2493            subscription_id, ..
2494        } => Some(subscription_id),
2495        _ => None,
2496    }
2497}
2498
2499fn subscription_key(topic: &str, subscription_id: &str) -> String {
2500    canonical_tuple_key(&[topic, subscription_id])
2501}
2502
2503fn idempotency_key_for(topic: &str, idempotency_key: &str) -> String {
2504    canonical_tuple_key(&[topic, idempotency_key])
2505}
2506
2507fn timestamp_or_zero<T>(state: &MessageBusState<T>) -> u64 {
2508    catch_unwind(AssertUnwindSafe(|| (state.now)())).unwrap_or(0)
2509}
2510
2511fn node_opts(name: impl Into<String>, factory: impl Into<String>) -> GraphNodeOpts {
2512    let mut opts = GraphNodeOpts::named(name);
2513    opts.node = NodeOpts {
2514        factory: Some(factory.into()),
2515        complete_when_deps_complete: false,
2516        error_when_deps_error: false,
2517        ..opts.node
2518    };
2519    opts
2520}
2521
2522fn pull_node_opts(
2523    name: impl Into<String>,
2524    factory: impl Into<String>,
2525    pull_id: LockId,
2526) -> GraphNodeOpts {
2527    let mut opts = node_opts(name, factory);
2528    opts.node.pull_id = Some(pull_id);
2529    opts.node.partial = true;
2530    opts
2531}
2532
2533fn is_reachable_upstream(from: &Core, target: &Core) -> bool {
2534    let mut seen = HashSet::new();
2535    let mut stack = vec![from.clone()];
2536    while let Some(node) = stack.pop() {
2537        if node.ptr_eq(target) {
2538            return true;
2539        }
2540        if !seen.insert(node.identity_key()) {
2541            continue;
2542        }
2543        stack.extend(node.deps());
2544    }
2545    false
2546}
2547
2548#[cfg(test)]
2549mod clean_slate_tests {
2550    use super::*;
2551    use crate::graph::graph;
2552    use crate::protocol::{Message, PullDemand};
2553
2554    #[test]
2555    fn message_bus_core_exposes_clean_slate_nodes() {
2556        let g = graph();
2557        let _bus = message_bus::<String>(
2558            &g,
2559            MessageBusOptions::named("bus")
2560                .with_topics(["orders"])
2561                .with_now(|| 10),
2562        );
2563        let snap = g.describe();
2564        for id in [
2565            "bus/commands",
2566            "bus/runtime",
2567            "bus/messages",
2568            "bus/status",
2569            "bus/issues",
2570        ] {
2571            assert!(snap.nodes.iter().any(|node| node.id == id), "{id}");
2572        }
2573        assert!(!snap.nodes.iter().any(|node| node.id.contains("dynamicHub")));
2574    }
2575
2576    #[test]
2577    fn unknown_topic_is_strict_issue_without_retained_message() {
2578        let g = graph();
2579        let bus = message_bus::<String>(&g, MessageBusOptions::named("bus").with_now(|| 20));
2580        let _messages = bus.messages.subscribe(|_| {});
2581        let _issues = bus.issues.subscribe(|_| {});
2582        let _status = bus.status.subscribe(|_| {});
2583
2584        bus.publish(
2585            "missing",
2586            "payload".to_owned(),
2587            None,
2588            Some("c1".to_owned()),
2589            None,
2590        );
2591
2592        assert!(bus.messages.cache().is_none());
2593        assert_eq!(bus.issues.cache().unwrap().code, "unknown-topic");
2594        assert!(bus.status.cache().is_none());
2595    }
2596
2597    #[test]
2598    fn catalog_topic_and_dead_letter_are_pull_read_only_projections() {
2599        let g = graph();
2600        let bus = message_bus::<String>(
2601            &g,
2602            MessageBusOptions::named("bus")
2603                .with_topics(["orders"])
2604                .with_now(|| 30),
2605        );
2606        let catalog = bus.catalog();
2607        let topic = bus.topic("orders");
2608        let dead = bus.dead_letter();
2609        let _catalog = catalog.snapshot.subscribe(|_| {});
2610        let _topic = topic.snapshot.subscribe(|_| {});
2611        let _dead = dead.snapshot.subscribe(|_| {});
2612
2613        bus.publish("orders", "o1".to_owned(), None, None, None);
2614        bus.publish("missing", "x".to_owned(), None, None, None);
2615        catalog.snapshot.up(vec![Message::Pull(PullDemand::new(
2616            catalog.snapshot_pull_id.clone(),
2617        ))]);
2618        topic
2619            .snapshot
2620            .up(vec![Message::Pull(PullDemand::with_params(
2621                topic.snapshot_pull_id.clone(),
2622                MessageBusTopicParams {
2623                    limit: Some(1),
2624                    after_seq: None,
2625                },
2626            ))]);
2627        dead.snapshot.up(vec![Message::Pull(PullDemand::with_params(
2628            dead.snapshot_pull_id.clone(),
2629            MessageBusDeadLetterParams {
2630                limit: None,
2631                after_entry_seq: None,
2632                topic: None,
2633                code: Some("unknown-topic".to_owned()),
2634            },
2635        ))]);
2636
2637        assert_eq!(catalog.snapshot.cache().unwrap().topics[0].topic, "orders");
2638        assert_eq!(topic.snapshot.cache().unwrap().messages[0].payload, "o1");
2639        assert_eq!(
2640            dead.snapshot.cache().unwrap().entries[0].issue.code,
2641            "unknown-topic"
2642        );
2643    }
2644
2645    #[test]
2646    fn available_pull_does_not_move_cursor_ack_seek_close_do() {
2647        let g = graph();
2648        let bus = message_bus::<String>(
2649            &g,
2650            MessageBusOptions::named("bus")
2651                .with_topics(["orders"])
2652                .with_now(|| 40),
2653        );
2654        let sub = bus.subscription(MessageBusSubscriptionOptions::new("orders", "s1"));
2655        let _available = sub.available.subscribe(|_| {});
2656        let _cursor = sub.cursor.subscribe(|_| {});
2657
2658        bus.publish("orders", "o1".to_owned(), None, None, None);
2659        bus.publish("orders", "o2".to_owned(), None, None, None);
2660        sub.available.up(vec![Message::Pull(PullDemand::with_params(
2661            sub.available_pull_id.clone(),
2662            MessageBusAvailableParams {
2663                limit: Some(1),
2664                after_seq: Some(1),
2665            },
2666        ))]);
2667
2668        let page = sub.available.cache().unwrap();
2669        assert_eq!(page.messages[0].seq, 2);
2670        assert_eq!(page.cursor.next_seq, 1);
2671        let opened_cursor = sub.cursor.cache().unwrap();
2672        assert_eq!(opened_cursor.next_seq, 1);
2673        assert!(!opened_cursor.retention_gap);
2674
2675        sub.ack(1, None);
2676        assert_eq!(sub.cursor.cache().unwrap().next_seq, 2);
2677        sub.seek(1, None);
2678        assert_eq!(sub.cursor.cache().unwrap().next_seq, 1);
2679        sub.close(None);
2680        assert!(sub.cursor.cache().unwrap().closed);
2681    }
2682
2683    #[test]
2684    fn retention_count_advances_head_and_marks_gap_until_seek() {
2685        let g = graph();
2686        let bus = message_bus::<String>(
2687            &g,
2688            MessageBusOptions::named("bus")
2689                .with_topics(["orders"])
2690                .with_retention(MessageBusRetentionPolicy {
2691                    max_messages: Some(1),
2692                    max_age_ms: None,
2693                }),
2694        );
2695        let sub = bus.subscription(MessageBusSubscriptionOptions::new("orders", "s1"));
2696        let _cursor = sub.cursor.subscribe(|_| {});
2697        let _issues = bus.issues.subscribe(|_| {});
2698
2699        bus.publish("orders", "o1".to_owned(), None, None, None);
2700        bus.publish("orders", "o2".to_owned(), None, None, None);
2701
2702        assert_eq!(bus.issues.cache().unwrap().code, "retention-gap");
2703        assert!(sub.cursor.cache().unwrap().retention_gap);
2704        sub.seek(2, None);
2705        let cursor = sub.cursor.cache().unwrap();
2706        assert_eq!(cursor.next_seq, 2);
2707        assert!(!cursor.retention_gap);
2708    }
2709
2710    #[test]
2711    fn invalid_subscription_start_seq_is_visible_issue_not_impossible_cursor() {
2712        let g = graph();
2713        let bus = message_bus::<String>(
2714            &g,
2715            MessageBusOptions::named("bus")
2716                .with_topics(["orders"])
2717                .with_retention(MessageBusRetentionPolicy {
2718                    max_messages: Some(1),
2719                    max_age_ms: None,
2720                }),
2721        );
2722        let _issues = bus.issues.subscribe(|_| {});
2723        let dead = bus.dead_letter();
2724        let _dead = dead.snapshot.subscribe(|_| {});
2725
2726        bus.publish("orders", "o1".to_owned(), None, None, None);
2727        bus.publish("orders", "o2".to_owned(), None, None, None);
2728        let sub = bus.subscription(
2729            MessageBusSubscriptionOptions::new("orders", "late")
2730                .from(MessageBusSubscriptionFrom::Seq(1)),
2731        );
2732        let _cursor = sub.cursor.subscribe(|_| {});
2733        dead.snapshot.up(vec![Message::Pull(PullDemand::with_params(
2734            dead.snapshot_pull_id.clone(),
2735            MessageBusDeadLetterParams {
2736                limit: None,
2737                after_entry_seq: None,
2738                topic: Some("orders".to_owned()),
2739                code: Some("cursor-out-of-range".to_owned()),
2740            },
2741        ))]);
2742
2743        assert_eq!(bus.issues.cache().unwrap().code, "cursor-out-of-range");
2744        assert_eq!(sub.cursor.cache().unwrap().next_seq, 2);
2745        assert_eq!(
2746            dead.snapshot.cache().unwrap().entries[0].issue.code,
2747            "cursor-out-of-range"
2748        );
2749    }
2750}