This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch wiretap-hand-off
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 70a2297ce995353b498565334749b6a66ecd1bd3
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 23:19:38 2026 +0100

    chore: camel-jbang - a route reached by a wireTap is handed off to, not 
called
    
    A wireTap sends a copy of the message on another thread: the route goes on
    without waiting for the tapped route, and does not get its failures back.
    The route readers recorded a wireTap like a to, so a wireTap to a direct
    endpoint was a call (↳ in About, "calls" in the summary). They now mark
    the endpoint a wireTap sends to, and its link is a hand-off; a route that
    also calls it with a to still calls it.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |  2 +-
 .../jbang/core/commands/ai/JavaRouteReader.java    |  6 ++-
 .../jbang/core/commands/ai/ProjectOverview.java    | 14 +++--
 .../dsl/jbang/core/commands/ai/ProjectRoutes.java  | 26 ++++++++--
 .../core/commands/ai/ProjectOverviewTest.java      | 60 ++++++++++++++++++++++
 .../src/main/resources/tui/help/overview.md        |  2 +-
 6 files changed, 98 insertions(+), 12 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index e72d184e60fc..ab7c0d28f6f1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -28,7 +28,7 @@ image::jbang/camel-tui-about.png[About the chaos demo: its 
purpose, what comes i
 main source file (a YAML `#` comment, a Javadoc or `//` comment in Java, an 
XML comment), else the descriptions of the
 routes.
 * *In*, *Routes* and *Out* say what starts it, its routes (a route called with 
`direct` sits under its caller with `↳`,
-one handed off with `seda` with `⇢`) and what it talks to, with the exchanges 
since it started and a sparkline of the
+one handed off with `seda` or a `wireTap` with `⇢`) and what it talks to, with 
the exchanges since it started and a sparkline of the
 last 5 minutes. *Up* and *Down* select a line, and *Enter* opens a route 
drilled down on the Diagram, or an endpoint with
 its details on the Endpoint tab.
 * *Last 5m* has the exchanges and how they ended, with the cause that happened 
most, and *What happened* a strip per route
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
index d9cfdab05f0e..6e0fac5ef8a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
@@ -54,6 +54,7 @@ import org.apache.camel.model.SwitchCaseDefinition;
 import org.apache.camel.model.SwitchDefinition;
 import org.apache.camel.model.ToDynamicDefinition;
 import org.apache.camel.model.TryDefinition;
+import org.apache.camel.model.WireTapDefinition;
 import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition;
 import org.apache.camel.model.errorhandler.NoErrorHandlerDefinition;
 import org.apache.camel.model.language.ConstantExpression;
@@ -236,8 +237,9 @@ final class JavaRouteReader {
             return;
         }
         boolean error = onError || p instanceof CatchDefinition || p 
instanceof OnExceptionDefinition;
-        if (p instanceof ToDynamicDefinition d) {
-            // toD and wireTap
+        if (p instanceof WireTapDefinition<?> w) {
+            add(produces, mark(ProjectRoutes.endpoint(w.getUri(), null, true, 
catalog).asTapped(), error));
+        } else if (p instanceof ToDynamicDefinition d) {
             add(produces, mark(ProjectRoutes.endpoint(d.getUri(), null, true, 
catalog), error));
         } else if (p instanceof SendDefinition<?> s) {
             add(produces, mark(ProjectRoutes.endpoint(s.getUri(), null, false, 
catalog), error));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
index 611e8a901ec7..a75c786b7553 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
@@ -296,11 +296,19 @@ public final class ProjectOverview {
                         continue;
                     }
                     String id = r.key() + "->" + target.key() + "@" + e.key();
+                    String kind = linkKind(e.scheme(), catalog);
+                    if (e.tapped() && "call".equals(kind)) {
+                        // a wireTap sends a copy on another thread: the route 
does not wait, nor get its failures
+                        kind = "async";
+                    }
                     Link known = links.get(id);
                     if (known == null) {
-                        links.put(id, new Link(r.key(), target.key(), e.uri(), 
linkKind(e.scheme(), catalog), e.onError()));
-                    } else if (known.onError() && !e.onError()) {
-                        links.put(id, new Link(known.from(), known.to(), 
known.endpoint(), known.kind(), false));
+                        links.put(id, new Link(r.key(), target.key(), e.uri(), 
kind, e.onError()));
+                    } else if (known.onError() && !e.onError() || 
"call".equals(kind) && !"call".equals(known.kind())) {
+                        // sent to outside failure handling too, or called as 
well as tapped
+                        links.put(id, new Link(
+                                known.from(), known.to(), known.endpoint(),
+                                "call".equals(kind) ? kind : known.kind(), 
known.onError() && e.onError()));
                     }
                 }
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
index 68a831ab5bad..8514869af540 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
@@ -104,16 +104,27 @@ public final class ProjectRoutes {
     private ProjectRoutes() {
     }
 
-    /** An endpoint a route uses, as written, with its identity for matching 
the other end. */
-    public record Endpoint(String uri, String scheme, String key, boolean 
dynamic, String label, boolean onError) {
+    /**
+     * An endpoint a route uses, as written, with its identity for matching 
the other end.
+     *
+     * @param onError sent to while handling a failure
+     * @param tapped  sent a copy of the message by a wireTap: the route goes 
on without waiting for it
+     */
+    public record Endpoint(String uri, String scheme, String key, boolean 
dynamic, String label, boolean onError,
+            boolean tapped) {
 
         public Endpoint(String uri, String scheme, String key, boolean 
dynamic, String label) {
-            this(uri, scheme, key, dynamic, label, false);
+            this(uri, scheme, key, dynamic, label, false, false);
         }
 
         /** The same endpoint, sent to while handling a failure (doCatch, 
onException, a dead letter channel). */
         Endpoint asOnError() {
-            return onError ? this : new Endpoint(uri, scheme, key, dynamic, 
label, true);
+            return onError ? this : new Endpoint(uri, scheme, key, dynamic, 
label, true, tapped);
+        }
+
+        /** The same endpoint, sent a copy of the message by a wireTap. */
+        Endpoint asTapped() {
+            return tapped ? this : new Endpoint(uri, scheme, key, dynamic, 
label, onError, true);
         }
     }
 
@@ -756,7 +767,7 @@ public final class ProjectRoutes {
                             // enrich with an expression: known only at runtime
                             e = dynamicEndpoint(key);
                         }
-                        add(POLL_KEYS.contains(key) ? consumes : sent, e);
+                        add(POLL_KEYS.contains(key) ? consumes : sent, 
"wireTap".equals(key) ? tappedOf(e) : e);
                     } else if ("kamelet".equals(key)) {
                         String name = value instanceof ScalarNode s ? 
s.getValue()
                                 : value instanceof MappingNode km ? 
str(value(km, "name")) : null;
@@ -880,6 +891,10 @@ public final class ProjectRoutes {
         return e != null ? e.asOnError() : null;
     }
 
+    private static Endpoint tappedOf(Endpoint e) {
+        return e != null ? e.asTapped() : null;
+    }
+
     private static void add(List<Endpoint> list, Endpoint e) {
         if (e != null) {
             list.add(e);
@@ -957,6 +972,7 @@ public final class ProjectRoutes {
                 if (PRODUCER_KEYS.contains(name)) {
                     String uri = uriOrConstant(s);
                     Endpoint ep = uri != null ? endpoint(uri, null, 
!"to".equals(name), catalog) : dynamicEndpoint(name);
+                    ep = "wireTap".equals(name) ? tappedOf(ep) : ep;
                     add(produces, handlesFailure(s, e) ? onErrorOf(ep) : ep);
                 } else if (POLL_KEYS.contains(name)) {
                     String uri = uriOrConstant(s);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
index ae452c58fa4a..40d1bd330003 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
@@ -216,6 +216,66 @@ class ProjectOverviewTest {
         }
     }
 
+    @Test
+    void aWireTapHandsOffInEveryDsl() {
+        // a wireTap sends a copy on another thread: the route does not wait 
for the tapped one, nor get its failures
+        Map<String, String> sources = Map.of(
+                "orders.camel.yaml", """
+                        - route:
+                            id: yaml-orders
+                            from:
+                              uri: direct:yaml
+                              steps:
+                                - wireTap:
+                                    uri: direct:audit
+                        """,
+                "orders.xml", """
+                        <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                          <route id="xml-orders">
+                            <from uri="direct:xml"/>
+                            <wireTap uri="direct:audit"/>
+                          </route>
+                        </routes>
+                        """,
+                "Orders.java", """
+                        public class Orders extends RouteBuilder {
+                            public void configure() {
+                                from("direct:java").routeId("java-orders")
+                                    .wireTap("direct:audit");
+                            }
+                        }
+                        """,
+                "both.camel.yaml", """
+                        - route:
+                            id: tapped-and-called
+                            from:
+                              uri: direct:both
+                              steps:
+                                - wireTap:
+                                    uri: direct:audit
+                                - to:
+                                    uri: direct:audit
+                        """,
+                "audit.camel.yaml", """
+                        - route:
+                            id: audit
+                            from:
+                              uri: direct:audit
+                              steps:
+                                - to:
+                                    uri: log:audit
+                        """);
+        Overview o = ProjectOverview.analyze(Path.of("orders"), sources, 
CATALOG);
+        for (String id : List.of("yaml-orders", "xml-orders", "java-orders")) {
+            assertThat(o.route(id).produces()).as(id).singleElement()
+                    .satisfies(e -> assertThat(e.tapped()).isTrue());
+            assertThat(o.links()).as(id).contains(new Link(id, "audit", 
"direct:audit", "async"));
+        }
+        // a route that also calls it waits for it
+        assertThat(o.links()).contains(new Link("tapped-and-called", "audit", 
"direct:audit", "call"))
+                .doesNotContain(new Link("tapped-and-called", "audit", 
"direct:audit", "async"));
+    }
+
     @Test
     void linksRoutesOverSharedEndpoints() {
         List<Link> links = overview().links();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index 2ea670c691f2..a7e9943d64dd 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -21,7 +21,7 @@ happened to it, with no AI:
   descriptions of its routes.
 - **In, Routes, Out** — what starts it (timers, HTTP, consumers), its
   routes (a route called with `direct:` sits under its caller with `↳`,
-  one handed off with `seda:` with `⇢`) and what it talks to, from its
+  one handed off with `seda:` or a `wireTap` with `⇢`) and what it talks to, 
from its
   sources. Each line has the exchanges since it started (for an
   endpoint, the sends to it), and In and Out a sparkline of the last 5
   minutes with the rate now, or `idle`. Out also has the systems the

Reply via email to