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 catalog — register_transport_type(kind, factory) — which decides what gets constructed;

  2. the module declaration — register_module(module, kind, role) — which mints the creator endpoint the SPEC is written to, decides where the connection mounts (/net/<module>/<name>), and fixes the role positionally. Since RFC-0014 S7 retired the /net:children[] creation door, this is the only way a connection comes into being — and it is why the SPEC carries no type and no role.

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

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