A kind is a NAME, resolved twice (transport plane)

Nothing in the routing plane knows the word tcp. A connection is created from a SPEC carrying kind = <name>, and that name is looked up in two registries the application fills:

  1. the factory catalogregister_transport_type(kind, factory) — which decides what gets constructed;

  2. the module declarationregister_module(module, kind, role) — which decides where it mounts, /net/<module>/<name>.

Both are open and both are strict. The example registers a kind that exists nowhere in the library, creates a connection of it from an ordinary :children[] SPEC, watches the routing plane wire it up — and then asks for a kind nobody registered and gets SCHEMA_NOT_FOUND.

What to notice

  • The library declares no modules, not even for its own kinds (ADR-0073 §4). kTcpClientSuggestedModule is a suggestion a header offers, never a registration a constructor performs. Until the application declares it, module_for("tcp", DIAL) is refused.

  • An unregistered kind is REFUSED, never defaulted. A fallback transport would be worse than a failure: “some link came up” is indistinguishable from the right one until traffic silently goes nowhere. And nothing is registered on the way to refusing.

  • SCHEMA_NOT_FOUND is the one verdict for both halves — a missing module and a missing factory answer the same way, because from the creator’s side they are the same fact: this catalog has no such entry.

  • quic and webtransport are the shipped out-of-tree case. They live in a separate libtracer_quic target that needs msquic, and nothing in the core references them (ADR-0043). What they use to join a node is this page and nothing more — quic_transport_factory() passed to register_transport_type, exactly like the kind invented here. can is registered the same way.

  • The factory parses its own private keys. Universal keys (addr, port, role, max_frame, …) arrive already parsed in conn_settings_t; a kind’s private config (quic’s cert/key PEM paths) is the factory’s business, read out of the raw config TLV. That split is what keeps conn_settings_t lean (ADR-0043 §5) — no kind-specific field ever lands in the shared record.

  • This target needs the net plane (LIBTRACER_NET_PLANE, the default) for transport_vertex_t. It opens no socket: the kind it registers is an in-process one, so nothing here is conditional at run time.

Source

  1/*
  2 * SPDX-License-Identifier: Apache-2.0
  3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
  4 */
  5
  6/**
  7 * @file
  8 * @brief ONE CONCEPT — a transport KIND is a run-time NAME resolved through two registries
  9 *        the application fills, so a kind the core has never heard of mounts on exactly the
 10 *        same terms as a built-in one, and a kind nobody registered is REFUSED rather than
 11 *        defaulted to something plausible.
 12 *
 13 * Nothing in the routing plane knows the word `tcp`. A connection is created from a SPEC
 14 * carrying `kind = <name>`, and that name is looked up twice:
 15 *
 16 *  1. the FACTORY catalog — `register_transport_type(kind, factory)` — which decides what
 17 *     object gets constructed. `udp`, `tcp` and `ws` are pre-registered by the default
 18 *     `transport_vertex_t` constructor only because they are compiled into the core;
 19 *     `can`, `quic` and `webtransport` ship their own factories (`can_transport_factory()`,
 20 *     `quic_transport_factory()`) and are registered by the application, through this
 21 *     identical call. An embedder's own kind is a third case of the same one.
 22 *  2. the MODULE declaration — `register_module(module, kind, role)` — which decides WHERE
 23 *     the connection mounts, `/net/<module>/<name>`. The library declares NONE of these,
 24 *     not even for its built-ins (ADR-0073 §4): `kTcpClientSuggestedModule` is a suggestion
 25 *     a header offers, never a registration a constructor performs.
 26 *
 27 * Both halves are open and both are strict, which is the actual claim: this example
 28 * registers a kind that exists nowhere in the library, creates a connection of it from an
 29 * ordinary `:children[]` SPEC, and watches the routing plane wire it up — and then asks for
 30 * a kind nobody registered and gets `SCHEMA_NOT_FOUND` instead of a default.
 31 *
 32 * `quic` and `webtransport` are the shipped instances of the out-of-tree case: they live in
 33 * a separate `libtracer_quic` target that needs msquic, and nothing in the core references
 34 * them (ADR-0043). What they use to join a node is this page and nothing more.
 35 *
 36 * Needs the net plane (`LIBTRACER_NET_PLANE`, on by default) for `transport_vertex_t`; no
 37 * sockets are opened, because the kind registered here is an in-process one. Runs under
 38 * ctest as `example_net_kind_catalog`; returns non-zero on any failed check.
 39 */
 40
 41#include <cstddef>
 42#include <cstdint>
 43#include <cstdio>
 44#include <memory>
 45#include <span>
 46#include <string>
 47#include <utility>
 48#include <vector>
 49
 50#include "libtracer/conn_spec.hpp"
 51#include "libtracer/fwd_router.hpp"
 52#include "libtracer/tracer.hpp"
 53#include "libtracer/transport_tcp.hpp"
 54#include "libtracer/transport_vertex.hpp"
 55
 56namespace {
 57
 58using tr::graph::graph_t;
 59using tr::graph::path_t;
 60using tr::net::conn_role_t;
 61
 62/** @brief Report expectation @p what and record a failure on @p ok. */
 63void check(bool& ok, bool cond, const char* what) {
 64    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 65    ok = ok && cond;
 66}
 67
 68/**
 69 * @brief The whole of a new transport kind: a `transport_t` that keeps what it is given.
 70 *
 71 * A real kind opens something. This one does not, which is the cleanest way to show that
 72 * the catalog cares about nothing except that a factory answers with a `transport_t`.
 73 */
 74class demo_link_t final : public tr::net::transport_t {
 75   public:
 76    /** @brief How many frames this link was asked to emit. */
 77    std::size_t sent = 0;
 78    void send(std::span<const std::byte> frame) override {
 79        (void)frame;
 80        ++sent;
 81    }
 82};
 83
 84}  // namespace
 85
 86int main() {
 87    bool ok = true;
 88
 89    graph_t g;
 90    tr::net::fwd_router_t router(g);
 91    // The default constructor registers the factories for the kinds this BUILD compiled —
 92    // udp/tcp/ws. It registers no module names at all, for any of them.
 93    tr::net::transport_vertex_t net(g, router);
 94
 95    // 1. A compiled-in kind is still unusable until the application says where it mounts.
 96    // This refusal is the declared-only rule: the catalog is the app's, not the library's.
 97    std::printf("the library declares no modules, not even for its own kinds:\n");
 98    const auto before = net.module_for("tcp", conn_role_t::DIAL);
 99    check(ok, !before.has_value(), "module_for('tcp', DIAL) is refused before the app declares it");
100    check(ok, !before.has_value() && before.error() == tr::graph::status_t::SCHEMA_NOT_FOUND,
101          "…as SCHEMA_NOT_FOUND — an absent catalog entry, not a malformed request");
102
103    const auto declared = net.register_module(std::string(tr::net::kTcpClientSuggestedModule),
104                                              "tcp", conn_role_t::DIAL);
105    check(ok, declared.has_value(),
106          "the application declares the module, adopting the header's "
107          "suggested name");
108    const auto after = net.module_for("tcp", conn_role_t::DIAL);
109    check(ok, after.has_value() && *after == "tcp-client", "…and now the kind resolves to it");
110
111    // 2. A kind the library has never heard of. Two calls — the same two calls `quic` makes.
112    std::printf("a kind the core does not contain:\n");
113    net.register_transport_type(
114        "demo",
115        [](const tr::net::conn_settings_t& settings, const tr::wire::tlv_t* raw_config)
116            -> tr::graph::result_t<std::unique_ptr<tr::net::transport_t>> {
117            // A real factory parses its kind-PRIVATE keys out of `raw_config` here (quic's
118            // cert/key PEM paths are the shipped example); the universal keys are already
119            // parsed into `settings`. This one needs neither, and says so.
120            (void)settings;
121            (void)raw_config;
122            return std::unique_ptr<tr::net::transport_t>(std::make_unique<demo_link_t>());
123        });
124    check(ok, net.register_module("demo-client", "demo", conn_role_t::DIAL).has_value(),
125          "register_module accepts a kind that exists only in this file");
126
127    // 3. Create one, the ordinary way: a SPEC written to the connection catalog. Nothing in
128    // this write is kind-specific except the four letters of the name.
129    const auto created =
130        g.write(path_t("/net:children[]"),
131                tr::net::conn_spec("client", "one", conn_role_t::DIAL, /*port=*/0, "demo"));
132    check(ok, created.has_value(), "SPEC{ type=client, name=one, kind=demo } created a connection");
133    check(ok, router.registry().by_name("net/demo-client/one") != nullptr,
134          "…and the routing plane wired it in under /net/<module>/<name>");
135    check(ok, g.read(path_t("/net/demo-client/one")).has_value(),
136          "…with a connection vertex addressable in the graph");
137
138    // 4. And a kind nobody registered. The refusal is the point: an unresolvable kind must
139    // not fall back to a default transport, because "some link came up" is indistinguishable
140    // from the right one until traffic silently goes nowhere.
141    std::printf("a kind nobody registered:\n");
142    const auto refused =
143        g.write(path_t("/net:children[]"),
144                tr::net::conn_spec("client", "two", conn_role_t::DIAL, /*port=*/0, "nosuch"));
145    check(ok, !refused.has_value(), "SPEC{ kind=nosuch } was refused");
146    check(ok, !refused.has_value() && refused.error() == tr::graph::status_t::SCHEMA_NOT_FOUND,
147          "…as SCHEMA_NOT_FOUND — the same verdict a missing module gives");
148    check(ok, router.registry().by_name("net/nosuch-client/two") == nullptr,
149          "…and nothing was registered on the way to refusing");
150
151    std::printf("catalog: 1 kind declared by the app, 1 kind invented here, 1 refused\n");
152    return ok ? 0 : 1;
153}

See also: connection config · transport module · transports are vertices · module catalog reference · the seam a factory has to satisfy.