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:
the factory catalog —
register_transport_type(kind, factory)— which decides what gets constructed;the module declaration —
register_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).
kTcpClientSuggestedModuleis 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_FOUNDis 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.quicandwebtransportare the shipped out-of-tree case. They live in a separatelibtracer_quictarget 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 toregister_transport_type, exactly like the kind invented here.canis registered the same way.The factory parses its own private keys. Universal keys (
addr,port,role,max_frame, …) arrive already parsed inconn_settings_t; a kind’s private config (quic’scert/keyPEM paths) is the factory’s business, read out of the raw config TLV. That split is what keepsconn_settings_tlean (ADR-0043 §5) — no kind-specific field ever lands in the shared record.This target needs the net plane (
LIBTRACER_NET_PLANE, the default) fortransport_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.