From 6e4b58112d22a4280a488c6964bcf01ffd6b1ff8 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 28 Sep 2026 06:23:23 +0000 Subject: [PATCH] Expand native Mermaid flowchart syntax support (#48895) ## What changed - Default `flowchart` and `graph` to top-down layout when no direction is specified. - Support solid and dashed edges with directed, undirected, or bidirectional endpoints: `-->`, `---`, `<-->`, `-.->`, `-.-`, and `<-.->`. - Accept pipe labels on these edges and spaced directed infix labels such as `A -- send --> B` and `A -. retry .-> B`. - Expand `&` groups into all source-to-target edges, including in chains. For example, `A & B --> C & D` creates four edges. Enforce the 24-edge limit after expansion and cap each group at 24 node references. ## Testing Add coverage for equivalent syntax, edge styles and endpoints, malformed input, and expansion limits. Extend rendering snapshots across all four directions and the TUI native-renderer test with the new syntax. GitOrigin-RevId: 6e51653ebf6b8b7e7e1edec0c2e09a5d9c8dec74 --- codex-rs/mermaid/README.md | 6 +- codex-rs/mermaid/src/families_tests.rs | 29 +++- codex-rs/mermaid/src/parse.rs | 95 ++++++++++-- ..._with_other_shapes_in_every_direction.snap | 145 +++++++++++------- codex-rs/mermaid/src/tests.rs | 71 ++++++++- .../tui/src/markdown_render/mermaid_tests.rs | 1 + 6 files changed, 265 insertions(+), 82 deletions(-) diff --git a/codex-rs/mermaid/README.md b/codex-rs/mermaid/README.md index b7690ddf63..e1d1e0eda6 100644 --- a/codex-rs/mermaid/README.md +++ b/codex-rs/mermaid/README.md @@ -8,7 +8,7 @@ production dependency is needed. | Family | Supported syntax | | --- | --- | -| `flowchart`, `graph` | TD/TB, BT, LR, RL; rectangle `[label]`, decision `{label}`, and stadium `([label])` nodes; directed and labeled `-->` edges; chains, branches, merges, loops | +| `flowchart`, `graph` | TD/TB (default), BT, LR, RL; rectangle `[label]`, decision `{label}`, and stadium `([label])` nodes; solid `-->`/`---`/`<-->` and dashed `-.->`/`-.-`/`<-.->` edges; pipe labels and spaced directed infix labels (`-- text -->`, `-. text .->`); `&` fan-out/fan-in, chains, branches, merges, loops | | `sequenceDiagram` | Implicit participants, `participant`/`actor`, aliases, `->`, `->>`, `-->`, `-->>`, `-x`, `--x`, self-messages, `Note over A[,B]`, nested `loop`/`alt`/`opt`/`critical`/`break`, one labeled `else` per `alt` | | `stateDiagram-v2`, `stateDiagram` | Flat states, `state "label" as ID`, descriptions, directed transitions with optional labels, initial/final `[*]`, direction declarations | | `classDiagram` | `class ID`, multiline member bodies, `ID : member`, solid/dashed links, association, inheritance, composition, aggregation, dependency, realization, quoted endpoint cardinalities, relationship labels, direction declarations | @@ -59,8 +59,8 @@ named participants. States use separate `● initial` and `◎ final` nodes. ## Limits and evaluation -All inputs are limited to 16 KiB. Graphs allow 16 nodes, 24 edges, and 16 members per -node. Sequences allow 8 participants, 64 events (including fragment boundaries), +All inputs are limited to 16 KiB. Graphs allow 16 nodes, 24 edges (after expanding +`&` groups), and 16 members per node. Each `&` group allows at most 24 node references. Sequences allow 8 participants, 64 events (including fragment boundaries), and 4 fragment levels. Source labels and identifiers are limited to 40 display cells/ASCII bytes respectively. Rendered canvases are capped at 65,536 cells, independent of the caller's maximum width. The library performs no I/O. diff --git a/codex-rs/mermaid/src/families_tests.rs b/codex-rs/mermaid/src/families_tests.rs index 9b80b8545e..70542d3bc5 100644 --- a/codex-rs/mermaid/src/families_tests.rs +++ b/codex-rs/mermaid/src/families_tests.rs @@ -156,7 +156,7 @@ fn unicode_labels_and_later_declarations() { } #[test] -fn class_relationship_endpoints() { +fn graph_relationship_endpoints() { for (operator, source_tip, target_tip, dashed) in [ ("<|--", '◁', '─', false), ("*--", '◆', '─', false), @@ -192,6 +192,33 @@ fn class_relationship_endpoints() { assert!(ports[1].contains("(many)")); assert_eq!(output.contains('┆'), dashed); } + for (operator, source_tip, target_tip, dashed) in [ + ("---", '─', '─', false), + ("-.->", '─', '◄', true), + ("-.-", '─', '─', true), + ("<-->", '◄', '◄', false), + ("<-.->", '◄', '◄', true), + ] { + let output = render( + &format!("flowchart; A{operator}|uses|B"), + /*max_width*/ 100, + ) + .unwrap(); + let ports = output + .lines() + .filter(|line| line.contains('├')) + .collect::>(); + assert!( + ports[0].contains(&format!("├{source_tip}")), + "{operator}: {output}" + ); + assert!( + ports[1].contains(&format!("├{target_tip}")), + "{operator}: {output}" + ); + assert!(ports[0].contains("uses")); + assert_eq!(output.contains('┆'), dashed); + } for source in [ "classDiagram\nclass A; A --> B : {ok}", "classDiagram\nclass A; class B {\n+id\n}", diff --git a/codex-rs/mermaid/src/parse.rs b/codex-rs/mermaid/src/parse.rs index c41d4eb17c..141ac37e80 100644 --- a/codex-rs/mermaid/src/parse.rs +++ b/codex-rs/mermaid/src/parse.rs @@ -1,4 +1,5 @@ //! Strict parser for a small flowchart grammar; every non-comment byte must be consumed. +//! Node groups expand into explicit edges within the graph edge cap. use super::Direction; use super::Edge; @@ -13,35 +14,83 @@ use unicode_width::UnicodeWidthStr; pub(super) fn parse(header: &str, body: &[&str]) -> Result { let tokens = header.split_whitespace().collect::>(); - let ["flowchart" | "graph", direction] = tokens.as_slice() else { - return Err(RenderError::Unsupported); + let direction = match tokens.as_slice() { + ["flowchart" | "graph"] => Direction::Down, + ["flowchart" | "graph", direction] => Direction::parse(direction)?, + _ => return Err(RenderError::Unsupported), }; let mut graph = Graph { - direction: Direction::parse(direction)?, + direction, ..Graph::default() }; for statement in body { let mut rest = *statement; - let mut from = node(&mut rest, &mut graph)?; + let mut from = nodes(&mut rest, &mut graph)?; while !rest.trim_start().is_empty() { - rest = rest - .trim_start() - .strip_prefix("-->") - .ok_or(RenderError::Unsupported)?; rest = rest.trim_start(); - let label = if let Some(after) = rest.strip_prefix('|') { - let (label, remaining) = delimited_label(after, "|")?; - let label = flowchart_label(label)?; - rest = remaining; - label.to_owned() + let mut label = String::new(); + let (source_tip, target_tip, dashed) = if let Some((after, source, target, dashed)) = [ + ("<-.->", '◄', '◄', true), + ("<-->", '◄', '◄', false), + ("-.->", '─', '◄', true), + ("-.-", '─', '─', true), + ("-->", '─', '◄', false), + ("---", '─', '─', false), + ] + .into_iter() + .find_map(|(token, source, target, dashed)| { + rest.strip_prefix(token) + .map(|after| (after, source, target, dashed)) + }) { + // Circle/cross tips are unsupported; without a space they are not node IDs. + if target == '─' && after.starts_with(['o', 'x']) { + return Err(RenderError::Unsupported); + } + rest = after.trim_start(); + if let Some(after) = rest.strip_prefix('|') { + let (text, remaining) = delimited_label(after, "|")?; + label = flowchart_label(text)?.to_owned(); + rest = remaining; + } + (source, target, dashed) } else { - String::new() + let (after, stem, dashed) = if let Some(after) = rest.strip_prefix("--") { + (after, "--", false) + } else if let Some(after) = rest.strip_prefix("-.") { + (after, ".-", true) + } else { + return Err(RenderError::Unsupported); + }; + // Spaced labels keep endpoint markers out of the text. Stop at the first + // closing stem instead of swallowing an unsupported edge and its target. + let (text, remaining) = after.split_once(stem).ok_or(RenderError::Unsupported)?; + if !text.starts_with(char::is_whitespace) || !text.ends_with(char::is_whitespace) { + return Err(RenderError::Unsupported); + } + rest = remaining + .strip_prefix('>') + .ok_or(RenderError::Unsupported)?; + label = flowchart_label(text.trim())?.to_owned(); + ('─', '◄', dashed) }; - let to = node(&mut rest, &mut graph)?; - if graph.edges.len() == MAX_EDGES { + let to = nodes(&mut rest, &mut graph)?; + // Check the Cartesian expansion before allocating edges, including repeated IDs. + if from.len() * to.len() > MAX_EDGES - graph.edges.len() { return Err(RenderError::Limit); } - graph.edges.push(Edge::directed(from, to, label)); + for &from in &from { + for &to in &to { + graph.edges.push(Edge { + from, + to, + label: label.clone(), + target_label: String::new(), + source_tip, + target_tip, + dashed, + }); + } + } from = to; } } @@ -51,6 +100,18 @@ pub(super) fn parse(header: &str, body: &[&str]) -> Result { Ok(graph) } +fn nodes(rest: &mut &str, graph: &mut Graph) -> Result, RenderError> { + let mut nodes = vec![node(rest, graph)?]; + while let Some(after) = rest.trim_start().strip_prefix('&') { + if nodes.len() == MAX_EDGES { + return Err(RenderError::Limit); + } + *rest = after; + nodes.push(node(rest, graph)?); + } + Ok(nodes) +} + fn node(rest: &mut &str, graph: &mut Graph) -> Result { let id = identifier(rest)?; // Reserved constructs must not be interpreted as ordinary node declarations. diff --git a/codex-rs/mermaid/src/snapshots/codex_mermaid__tests__stadiums_with_other_shapes_in_every_direction.snap b/codex-rs/mermaid/src/snapshots/codex_mermaid__tests__stadiums_with_other_shapes_in_every_direction.snap index e9e3d4da2b..fb79a2f76e 100644 --- a/codex-rs/mermaid/src/snapshots/codex_mermaid__tests__stadiums_with_other_shapes_in_every_direction.snap +++ b/codex-rs/mermaid/src/snapshots/codex_mermaid__tests__stadiums_with_other_shapes_in_every_direction.snap @@ -1,73 +1,106 @@ --- source: mermaid/src/tests.rs +assertion_line: 35 expression: "cases.join(\"\\n\\n\")" --- TD ╭─────────╮ │ 请求 │ -│ ├─────┐ -│ ├◄────╪───┐ -╰─────────╯ │ │ - │ │ - │ │ -┌─────────┐ │ │ -│ Work │ │ │ -│ ├◄────┘ │ -│ ├───────┐ │ -└─────────┘ │ │ - │ │ - │ │ -┌─────────┐ │ │ -│ ◇ Done? │ │ │ -│ ├◄──────┘ │ -│ ├─────────┘ +│ ├──go───┐ +│ ├──go───╪─┐ +│ ├◄──────╪─╪───┐ +│ ├───────╪─╪───╪─┐ +│ ├◄┄┄┄┄┄┄╪┄╪┄┄┄╪┄╪┄┄┄┐ +╰─────────╯ │ │ │ │ ┆ + │ │ │ │ ┆ + │ │ │ │ ┆ +┌─────────┐ │ │ │ │ ┆ +│ Work │ │ │ │ │ ┆ +│ ├◄──────┘ │ │ │ ┆ +│ ├─┄no┄┄┄┄┄╪┄┐ │ │ ┆ +│ ├─────────╪─╪─╪─┘ ┆ +│ ├─┄┄┄┄┄┄┄┄╪┄╪┄╪┄┄┄┐ ┆ +│ ├◄┄┄┄┄┄┄┄┄╪┄╪┄╪┄┄┄╪┄┘ +└─────────┘ │ ┆ │ ┆ + │ ┆ │ ┆ + │ ┆ │ ┆ +┌─────────┐ │ ┆ │ ┆ +│ ◇ Done? │ │ ┆ │ ┆ +│ ├◄────────┘ ┆ │ ┆ +│ ├◄┄┄┄┄┄┄┄┄┄┄┘ │ ┆ +│ ├◄────────────┘ ┆ +│ ├─┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ └─────────┘ BT ┌─────────┐ │ ◇ Done? │ -│ ├◄──────┐ -│ ├───────╪─┐ -└─────────┘ │ │ - │ │ - │ │ -┌─────────┐ │ │ -│ Work │ │ │ -│ ├◄────┐ │ │ -│ ├─────╪─┘ │ -└─────────┘ │ │ - │ │ - │ │ -╭─────────╮ │ │ -│ 请求 │ │ │ -│ ├─────┘ │ -│ ├◄────────┘ +│ ├◄────────┐ +│ ├◄┄┄┄┄┄┄┄┄╪┄┐ +│ ├◄────────╪─╪─┐ +│ ├─┄┄┄┄┄┄┄┄╪┄╪┄╪┄┄┄┐ +└─────────┘ │ ┆ │ ┆ + │ ┆ │ ┆ + │ ┆ │ ┆ +┌─────────┐ │ ┆ │ ┆ +│ Work │ │ ┆ │ ┆ +│ ├◄──────┐ │ ┆ │ ┆ +│ ├─┄no┄┄┄╪┄╪┄┘ │ ┆ +│ ├───────╪─╪───╪─┐ ┆ +│ ├─┄┄┄┄┄┄╪┄╪┄┄┄╪┄╪┄┘ +│ ├◄┄┄┄┄┄┄╪┄╪┄┄┄╪┄╪┄┄┄┐ +└─────────┘ │ │ │ │ ┆ + │ │ │ │ ┆ + │ │ │ │ ┆ +╭─────────╮ │ │ │ │ ┆ +│ 请求 │ │ │ │ │ ┆ +│ ├──go───┘ │ │ │ ┆ +│ ├──go─────┘ │ │ ┆ +│ ├◄────────────┘ │ ┆ +│ ├───────────────┘ ┆ +│ ├◄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ ╰─────────╯ LR -╭──────╮ ┌──────┐ ┌─────────┐ -│ 请求 │ │ Work │ │ ◇ Done? │ -╰┬─┬───╯ └┬─┬───┘ └┬─┬──────┘ - │ ▲ ▲ │ ▲ │ - │ │ │ │ │ │ - │ │ │ │ │ │ - │ │ │ │ │ │ - └─╪───────┘ │ │ │ - │ │ │ │ - │ └───────┘ │ - │ │ - └───────────────────┘ +╭────────────────────╮ ┌────────────────────┐ ┌────────────────┐ +│ 请求 │ │ Work │ │ ◇ Done? │ +╰┬───┬───┬───┬───┬───╯ └┬───┬───┬───┬───┬───┘ └┬───┬───┬───┬───┘ + │ │ ▲ │ ▲ ▲ │ │ │ ▲ ▲ ▲ ▲ │ + │go │go │ │ ┆ │ ┆no │ ┆ ┆ │ ┆ │ ┆ + │ │ │ │ ┆ │ ┆ │ ┆ ┆ │ ┆ │ ┆ + │ │ │ │ ┆ │ ┆ │ ┆ ┆ │ ┆ │ ┆ + └───╪───╪───╪───╪───────┘ ┆ │ ┆ ┆ │ ┆ │ ┆ + │ │ │ ┆ ┆ │ ┆ ┆ │ ┆ │ ┆ + └───╪───╪───╪───────────╪───╪───╪───╪───────┘ ┆ │ ┆ + │ │ ┆ ┆ │ ┆ ┆ ┆ │ ┆ + │ │ ┆ └┄┄┄╪┄┄┄╪┄┄┄╪┄┄┄┄┄┄┄┄┄┄┄┘ │ ┆ + │ │ ┆ │ ┆ ┆ │ ┆ + └───╪───╪───────────────╪───╪───╪───────────────┘ ┆ + │ ┆ │ ┆ ┆ ┆ + └───╪───────────────┘ ┆ ┆ ┆ + ┆ ┆ ┆ ┆ + ┆ └┄┄┄╪┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ + ┆ ┆ + └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ RL -┌─────────┐ ┌──────┐ ╭──────╮ -│ ◇ Done? │ │ Work │ │ 请求 │ -└┬─┬──────┘ └┬─┬───┘ ╰┬─┬───╯ - ▲ │ ▲ │ │ ▲ - │ │ │ │ │ │ - │ │ │ │ │ │ - │ │ │ │ │ │ - │ │ └─╪───────┘ │ - │ │ │ │ - └─╪────────────┘ │ - │ │ - └──────────────────────┘ +┌────────────────┐ ┌────────────────────┐ ╭────────────────────╮ +│ ◇ Done? │ │ Work │ │ 请求 │ +└┬───┬───┬───┬───┘ └┬───┬───┬───┬───┬───┘ ╰┬───┬───┬───┬───┬───╯ + ▲ ▲ ▲ │ ▲ │ │ │ ▲ │ │ ▲ │ ▲ + │ ┆ │ ┆ │ ┆no │ ┆ ┆ │go │go │ │ ┆ + │ ┆ │ ┆ │ ┆ │ ┆ ┆ │ │ │ │ ┆ + │ ┆ │ ┆ │ ┆ │ ┆ ┆ │ │ │ │ ┆ + │ ┆ │ ┆ └───╪───╪───╪───╪───────┘ │ │ │ ┆ + │ ┆ │ ┆ ┆ │ ┆ ┆ │ │ │ ┆ + └───╪───╪───╪───────────╪───╪───╪───╪───────────┘ │ │ ┆ + ┆ │ ┆ ┆ │ ┆ ┆ │ │ ┆ + └┄┄┄╪┄┄┄╪┄┄┄┄┄┄┄┄┄┄┄┘ │ ┆ ┆ │ │ ┆ + │ ┆ │ ┆ ┆ │ │ ┆ + └───╪───────────────╪───╪───╪───────────────┘ │ ┆ + ┆ │ ┆ ┆ │ ┆ + ┆ └───╪───╪───────────────────┘ ┆ + ┆ ┆ ┆ ┆ + └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ ┆ ┆ + ┆ ┆ + └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ diff --git a/codex-rs/mermaid/src/tests.rs b/codex-rs/mermaid/src/tests.rs index 369cfda014..12478d4622 100644 --- a/codex-rs/mermaid/src/tests.rs +++ b/codex-rs/mermaid/src/tests.rs @@ -24,8 +24,9 @@ fn stadium_declarations_and_references() { fn stadiums_with_other_shapes_in_every_direction() { let mut cases = Vec::new(); for direction in ["TD", "BT", "LR", "RL"] { - let source = - format!("flowchart {direction}; A([请求]) --> B[Work] --> C{{Done?}}; C --> A"); + let source = format!( + "flowchart {direction}; A([请求]) -- go --> B[Work] & C{{Done?}}; B -. no .-> C; C <--> A; B --- A; C -.- B; A <-.-> B" + ); let output = render(&source, /*max_width*/ 100).unwrap(); let width = output.lines().map(UnicodeWidthStr::width).max().unwrap(); assert_eq!(render(&source, width), Ok(output.clone())); @@ -36,7 +37,7 @@ fn stadiums_with_other_shapes_in_every_direction() { } #[test] -fn quoted_flowchart_labels_match_unquoted_labels() { +fn equivalent_flowchart_forms_preserve_graph() { let quoted = r#"A["Review & confirm;"] -->|"Yes & continue"| B{"Ready?"} B --> C(["Checkout"]) A[Review & confirm;]"#; @@ -45,6 +46,48 @@ A[Review & confirm;]"#; super::parse::parse("flowchart TD", "ed.lines().collect::>()).unwrap(), super::parse::parse("flowchart TD", &unquoted.lines().collect::>()).unwrap(), ); + for (infix, pipe) in [ + ("-- Yes -->", "-->|Yes|"), + ("-- \"Yes & continue\" -->", "-->|\"Yes & continue\"|"), + ("-. retry .->", "-.->|retry|"), + ] { + assert_eq!( + super::parse::parse("flowchart", &[&format!("A {infix} B --> C")]).unwrap(), + super::parse::parse("graph TB", &[&format!("A {pipe} B --> C")]).unwrap(), + ); + } + assert_eq!( + super::parse::parse("graph", &["A[Input & config] & B -- send --> C & D -.-> E"]).unwrap(), + super::parse::parse( + "flowchart TD", + &[ + "A[Input & config]", + "B", + "C", + "D", + "E", + "A -->|send| C", + "A -->|send| D", + "B -->|send| C", + "B -->|send| D", + "C -.-> E", + "D -.-> E", + ] + ) + .unwrap(), + ); + assert_eq!( + super::parse::parse("flowchart", &["A--- oB", "A-.- xB"]) + .unwrap() + .nodes, + super::parse::parse("flowchart", &["A", "oB", "xB"]) + .unwrap() + .nodes, + ); + assert_eq!( + super::parse::parse("flowchart", &["A --> B & B"]).unwrap(), + super::parse::parse("flowchart TD", &["A --> B", "A --> B"]).unwrap(), + ); assert_eq!( render(&format!("graph TD; {quoted}"), /*max_width*/ 180).unwrap(), render(&format!("graph TD; {unquoted}"), /*max_width*/ 180).unwrap(), @@ -144,6 +187,7 @@ fn quoted_flowchart_labels_reject_malformed_and_unsafe_text() { for source in [ format!("flowchart TD; A[{label}]"), format!("flowchart TD; A -->|{label}| B"), + format!("flowchart TD; A -- {label} --> B"), ] { assert_eq!( render(&source, /*max_width*/ 100), @@ -176,8 +220,20 @@ fn rejects_partial_or_unsupported_input() { r#"flowchart TD; P --> Q; A -->|"`Caption`"| B"#, "flowchart TD; subgraph X; A; end", "flowchart TD; A --> B; garbage syntax", - "flowchart TD; A -.-> B", - "flowchart TD; A & B --> C", + "flowchart TD; A --> B &", + "flowchart TD; A && B", + "flowchart TD; A -- Yes B", + "flowchart TD; A -. retry --> B", + "flowchart TD; A ==> B", + "flowchart TD; A -- hello --- B --> C", + "flowchart TD; A -. hello .- B -.-> C", + "flowchart TD; A -- hello ----> B", + "flowchart TD; A -. hello ..-> B", + "flowchart TD; A -- hello o--> B", + "flowchart TD; A --oB --> C", + "flowchart TD; A -. hello -.-> B", + "flowchart TD; A---oB", + "flowchart TD; A-.-xB", "flowchart TD; A[one]; A[two]", "flowchart TD; A[HTML]", "flowchart TD; A[]", @@ -215,9 +271,14 @@ fn rejects_partial_or_unsupported_input() { #[test] fn source_graph_and_width_limits() { + let grouped = "A & B & C & D --> E & F & G & H & I & J"; + assert!(render(&format!("graph; {grouped}"), /*max_width*/ 200).is_ok()); for source in [ " ".repeat(16 * 1024 + 1), format!("graph TD; A[{}]", "x".repeat(41)), + format!("graph; A -- {} --> B", "x".repeat(41)), + format!("graph; A --> E; {grouped}"), + format!("graph; {} --> B", ["A"; 25].join(" & ")), format!("graph TD; A[\"{}\"]", "[]".repeat(21)), format!("graph TD; A([{}])", "x".repeat(41)), format!( diff --git a/codex-rs/tui/src/markdown_render/mermaid_tests.rs b/codex-rs/tui/src/markdown_render/mermaid_tests.rs index 9127ac44ce..86bec99b67 100644 --- a/codex-rs/tui/src/markdown_render/mermaid_tests.rs +++ b/codex-rs/tui/src/markdown_render/mermaid_tests.rs @@ -21,6 +21,7 @@ fn mermaid_fences_use_native_renderer_for_every_family() { for source in [ "%% heading\nflowchart TD; A --> B", "graph LR; A --> B", + "flowchart; A -- send --> B & C; B <-.-> C", "sequenceDiagram; A->>B: request; B-->>A: response", "stateDiagram-v2; [*] --> Active; Active --> [*]", "stateDiagram; [*] --> Active; Active --> [*]",